跳转至

rag/retrieval

📌 基本名词

检索增强生成(Retrieval Augmented Generation): 检索增强生成,将传统信息检索系统(如数据库)与生成式大语言模型进行结合,实现智能信息检索和生成。

切片(Chunking / Slicing): 将大规模数据集或复杂结构按特定规则分割为更小、更易处理的部分。

具体作用:

  • 克服长度限制
  • 提高检索精度,但需要度控制粒度: 切片太小可能丢失上下文,太大又可能引入噪音。
  • 提供高密度、低噪音的有效信息,优化答案生成。

元数据(Metadata): “关于数据的数据”,用于描述数据的属性、结构、来源、用途等信息。它本身不包含数据的具体内容,而是提供数据的管理和检索依据。

向量化(Embedding): 把文字变成计算机能理解的一串数字向量。

召回(Recall): 从海量候选集中筛选出与目标相关的子集,作为后续排序或生成的输入。

透传(Transparent Transmission): 数据在传输过程中不进行任何处理或修改,保持原始格式和内容,如同“透明管道”。

📌 数据流

【离线阶段】
原始文档 → 切片 → 附加元数据 → Embedding → 向量数据库
                ↓
          (记录位置/权限等)

【在线阶段】
用户问题 → Embedding → 向量检索 + 元数据过滤 (召回) → 透传原文 → LLM → 答案

离线建库阶段(把知识变成可检索的索引)

目标: 把原始文档加工成“向量 + 元数据”的结构化索引,存进数据库。

第1层: 切片 (Slicing)

  • 输入: 原始文档(PDF、Word、Markdown、数据库记录等)
  • 处理: 按固定长度(如512 token)或按语义边界(段落、句子)分割成块,相邻块保留重叠(如64 token)。同时记录每个切片在原文中的位置(页码、章节、偏移量)。
  • 输出: 一个切片列表,每个切片包含文本内容 + 位置信息。

第2层: 元数据 (Metadata) 附加

  • 输入: 上一步输出的切片,以及文档本身的属性(作者、创建时间、文档类型、部门、权限等级等)
  • 处理: 为每个切片打上标签。例如:
  • 文档级元数据: doc_id=123, department=财务, security_level=机密
  • 切片级元数据: page=5, chunk_index=12, heading="合同违约责任"
  • 输出: 带有丰富元数据的切片对象。

注意: 元数据 不参与向量化,但在检索时可以用于 过滤(比如只查“财务部”的文档)。

第3层: Embedding(向量化)

  • 输入: 每个切片的 文本内容(纯文字,不包括元数据标签)
  • 处理: 调用 Embedding 模型(如 text-embedding-ada-002),把文本映射成一个固定维度的浮点数数组,比如 [0.12, -0.45, 0.73, ...](常见维度: 768、1024、1536)。
  • 输出: 每个切片的 向量

第4层: 存储(向量数据库)

  • 输入: 每个切片的向量、原始文本、元数据
  • 处理: 存入向量数据库(如 Milvus、Pinecone、Elasticsearch)。数据库会为向量建立索引(如 HNSW),方便后续快速近似最近邻检索。
  • 输出: 持久化的索引。

在线检索阶段(用户提问 → 召回相关信息)

第5层: 用户问题 Embedding

  • 输入: 用户输入的问句(如“去年财务部的合同违约条款是怎么写的?”)
  • 处理: 调用 同一个 Embedding 模型,把问句也变成一个向量(query_vector)。
  • 输出: 问句向量。

第6层: 召回 (Recall) —— 核心检索步骤

  • 输入: 问句向量,以及可选的元数据过滤条件(如 department=财务 + security_level<=机密
  • 处理:
  • 向量数据库收到 query_vector,通过向量相似度(余弦距离、点积)找到最相似的 Top-K 个切片向量(例如 K=20)。
  • 同时应用 元数据过滤: 只考虑满足部门、时间、权限等条件的切片。这一步通常发生在向量检索之前或之后(取决于数据库支持,最好是前置过滤)。
  • 输出: Top-K 个切片的 原始文本 + 对应的元数据。

召回的目标是 快且全,不要求完全精确,但要保证相关的内容都被捞上来。

第7层: 透传 (Transparent Transmission)

  • 输入: 召回的 Top-K 切片文本(以及附带的元数据)
  • 处理: 不做任何修改、总结、重写或格式化。只是把这些原始切片文本按顺序拼接成一个上下文块,可能加上分隔符,但内容一字不改。
  • 输出: 干净的、可被大模型直接使用的上下文字符串。

透传的价值在于 保证事实无损。如果在这个环节对文本做了摘要或改写,可能会引入错误或丢失细节。

第8层: 大模型生成答案

  • 输入: 用户原始问题 + 透传过来的上下文
  • 处理: 组装成 prompt(例如: 基于以下资料回答问题: \n{上下文}\n问题: {用户问题}),调用 LLM。
  • 输出: 最终的自然语言答案。

案例

一份 2024年财务合同.pdf,第8页有一句“违约方需支付合同金额20%的违约金”。

离线处理:

  1. 切片: 把这句单独或与邻近句子切在一起(chunk_123)。
  2. 元数据: doc_id=789, department=财务, page=8, chunk_text="违约方需支付..."
  3. Embedding: 计算这个文本的向量 [0.2, -0.5, ...]
  4. 存储

在线问答:

  1. 用户问: “财务合同的违约金比例是多少?”
  2. 问题 Embedding → 向量检索。
  3. 召回: 找到 chunk_123(相似度最高),同时过滤 department=财务
  4. 透传: 把原文 "违约方需支付合同金额20%的违约金" 原样传给 LLM。
  5. LLM 回答: “根据资料,违约金比例为合同金额的20%。”

常见问题/痛点

切分粒度怎么定?

没有绝对的标准,但有一个决策流程和经验范围。

1.主要影响因子

  • 业务场景: 问答型(FAQ/错误码)用小粒度;摘要/综述型用大粒度
  • 文档类型: 结构化的手册、合同条款 → 按段落/章节切;非结构化的对话、邮件 → 按固定长度+重叠切。
  • Embedding 模型的长度限制: 主流模型输入上限 256~8192 token,一般建议不超过限制的 75%。
  • LLM 上下文窗口: 窗口越大,可以容忍更大粒度,但注意引入噪音。

2.经验起点(基于 token 数,用 cl100k 或近似)

场景 切片大小(token) 重叠(token) 说明
精确 Q&A(如错误码、定义) 128~256 16~32 保证每个切片只含一个知识点
一般企业文档(合同、手册) 512 64 最通用的平衡点
长篇幅叙述(报告、论文) 768~1024 128 保留上下文,依赖 LLM 长上下文能力

实际粒度按字符数控制更直观: 512 token ≈ 300~400 中文字符;1024 token ≈ 600~800 中文字符。

3.确定粒度的具体步骤(实验驱动)

  1. 取样: 从真实文档中取 50~100 个不同片段的样本。
  2. 人工标注: 标注出理想的检索单元(一个完整答案/事实应该覆盖多长的文本)。
  3. 尝试 3~5 种粒度: 例如 256、512、1024,分别建索引。
  4. 用典型问题测试召回效果: 看是否能把“正确答案所在的片段”排在前 3~5 位。
  5. 选择召回率最高且不显著增加噪音的粒度。

召回内容不匹配怎么办?

“不匹配”分两类: 查不到(漏召)查不准(低相关/噪音)。对策不同。

1.漏召(应该有的切片没被召回)

原因: 切片太小导致关键信息被拆散;Embedding 模型语义理解偏差;查询词与文档词汇不匹配(同义词、缩写)。

解决方案:

  • 增大切片粒度或增大重叠,避免信息被截断。
  • 混合检索: 结合 关键词检索(BM25) 和向量检索,然后融合(RRF 重排)。
  • 查询改写: 用 LLM 把用户问题扩写成多个不同表述,分别检索后合并。
  • 调整 Embedding 模型: 换用更适合企业领域(如法律、医疗)的微调模型。

2.低相关(召回了大量无关内容)

原因: 切片粒度太大,一个切片包含多主题;元数据过滤不足;相似度阈值太低。

解决方案:

  • 缩小粒度,使每个切片语义更聚焦。
  • 加强元数据过滤: 检索时先限定部门、时间、文档类型。
  • 提高相似度阈值: 只保留余弦相似度 > 0.7 的结果(阈值需实测调优)。
  • 重排序(Rerank): 召回 Top-K(如 50)后,用一个更强的 cross-encoder 模型重新打分,只保留 Top-N(如 5)。

语义丢失严重怎么办?

“语义丢失”指切片后,原本需要跨段落理解的信息被破坏了(比如“然而”“综上所述”这种转折关系被切断)。

根本原因

  • 硬切分(按固定字符数)打断了连贯的语义单元。
  • 重叠不足,导致指代词(“它”“这些”)的先行词被切到上一块。

解决方案

策略 具体做法
基于语义边界切分 按句子边界、段落边界、Markdown/HTML 标题切,而不是死按固定长度。使用 langchainRecursiveCharacterTextSplitter,以段落、句号为优先分隔符。
增加重叠 重叠大小设为切片大小的 10%~25%,例如 512 token 切片重叠 64~128 token。保证上下文连接。
滑动窗口召回 检索时不仅返回匹配的切片,还返回其前后相邻的切片(例如各 1 个),还原上下文。
父子切片结构 把文档切成大块(父切片),再细切成小块(子切片)。检索时用子切片(精确),传给 LLM 时连带父切片(完整上下文)。
使用长上下文模型 如果 LLM 支持 128k token,可以放弃细粒度切片,直接把整段章节甚至整篇文档放入 context。但要注意成本与噪音。

📌 怎么测试

端到端和分模块测试

1.准备数据

  • 从真实业务中收集 100~300 个问答对(用户问题 + 标准答案 + 答案所在的源文档及具体位置)。
  • 目标至少覆盖: 简单事实查询、多跳推理、否定/边界条件、误导性问题。

2.模块化测试项

测试层级 测试内容 测试方法 通过标准
索引正确性 切片是否覆盖所有文档?元数据是否正确附着? 抽样检查: 原始文档某句话,在库里是否能通过文档ID+位置准确找到对应切片。 抽样准确率 ≥ 99%
召回测试 对于测试集的问题,Top-5 召回是否包含正确答案所在的切片? 批量跑召回,计算 Recall@5(前5条中包含正确答案的比例)。 ≥ 85%
重排序测试(如果有 rerank) 正确切片是否被提升到 Top-1 或 Top-3? 计算 MRR(平均倒数排名)和 Hit@1 根据业务要求,一般 Hit@1 ≥ 60%
端到端答案正确性 LLM 最终生成的答案是否与标准答案语义一致、无幻觉? 人工评分或使用 LLM-as-Judge(如 GPT-4 对比打分)。打分维度: 相关、正确、完整、无害 综合准确率 ≥ 80%
延迟与稳定性 检索+生成的总耗时,以及高并发下的错误率。 压测工具,模拟 10~50 QPS。 P99 < 5秒,错误率 < 1%
安全与权限 用户 A 是否能看到用户 B 的私有文档? 构造不同权限的测试账号,尝试通过提问越权访问。 无越权

3.测试人员实用脚本思路(伪代码)

# 批量测试召回
for question, expected_chunk_id in test_set:
    results = retriever.retrieve(question, top_k=5)
    if expected_chunk_id in [r.id for r in results]:
        recall_hit += 1
recall_at_5 = recall_hit / len(test_set)

4.典型问题及测试 Checklist

  • 同义词测试: “营收” vs “收入” → 应召回相同文档。
  • 缩写测试: “LLM” vs “大语言模型” → 应能匹配。
  • 多跳问题: “A 公司的 CEO 在 B 项目中的角色?” → 需要两个切片联合。
  • 时间敏感: “去年的销售数据” → 元数据过滤年份。
  • 拒答测试: 问不相关的问题(如天气),系统应回答“资料中无相关信息”,而不是瞎编。