Skip to content

RAG 完整链路 ​

RAG 解决什么问题 ​

RAG(Retrieval-Augmented Generation,检索增强生成)在生成答案前,先从外部知识库检索相关资料,再把资料和问题一起交给模型。它适合处理模型训练中未覆盖的私有文档、更新后的制度,以及需要核对出处的细节。

text
闭卷问答:
  用户问题 -> 模型凭训练记忆回答

RAG 问答:
  用户问题 -> 查资料 -> 把资料交给模型 -> 模型基于资料回答

RAG 能缓解这些问题:

  • 知识截止:模型训练后发生的新信息,它本来不知道。
  • 私有知识:公司手册、产品文档、项目记录、个人笔记,模型没训练过。
  • 细节错误:制度、金额、版本、日期等信息可以从原文核对,减少依赖模型记忆。
  • 可溯源:回答后能告诉用户“根据哪份资料、哪一页、哪一段”。

RAG 的效果仍受各环节影响:

  • 检索不到正确资料,模型仍然答不准。
  • 检索到了错误或过期资料,答案也会错。
  • 文档切分太差,模型只看到半截证据。
  • 即使 prompt 要求依据资料,模型也可能遗漏限制或补出没有依据的结论。

向量检索是 RAG 常用的检索方式,也可以使用全文搜索、数据库查询等方式取得资料。本文接着 Embedding 基础,以向量知识库为主介绍完整流程。

两个阶段、六个步骤 ​

一条完整 RAG 链路分成两个阶段。

离线阶段:建库 ​

离线阶段通常不是每次用户提问都跑,而是在文档新增、更新、删除时执行。

text
原始文档 -> 加载 -> 清洗 -> 分块 -> 向量化 -> 写入向量库

在线阶段:问答 ​

在线阶段每次用户提问都会跑。

text
用户问题 -> 检索 -> 拼上下文 -> 模型生成答案
             ↑
     可加入查询改写、业务过滤和重排

按实现顺序,可以分成六步:

步骤名称做什么
1加载 load读取 PDF、Markdown、网页、数据库记录等原始资料
2清洗 clean去除无关噪声,修复抽取后的文本结构
3分块 chunk把长文档切成适合检索的小片段
4向量化并入库 embed & store每个 chunk 生成 embedding,连同原文和元数据写入向量库
5检索 retrieve用户问题也转成向量,从库里找相关 chunk
6生成 generate将选中的证据放入上下文,让模型基于资料回答

第一步:加载文档 ​

加载时需要处理不同格式的文本结构,并保留后续引用所需的位置信息。

来源常见问题处理建议
Markdown / txt标题、列表、代码块结构容易被打散保留标题层级和段落边界
PDF页眉页脚、页码、分栏、表格抽取混乱抽取后人工抽查几页,保留页码
网页导航栏、广告、版权声明、相关推荐混进正文只保留正文区域,去掉样板噪声
Word / 飞书 / Notion标题、表格、评论、附件关系复杂尽量转成结构化 Markdown 再处理
数据库记录字段短、上下文少拼上字段名和业务上下文
表格单元格脱离表头后意义丢失每行拼入表头,必要时按行或按主题建 chunk

加载时要尽量保留来源信息:

json
{
  "source": "employee-handbook.pdf",
  "page": 12,
  "title": "员工手册",
  "section": "假期政策",
  "updated_at": "2026-08-09"
}

这些 metadata 后面会用于过滤、引用和排查问题。

第二步:清洗文档 ​

清洗要去掉干扰检索的噪声,同时保留能回答问题的事实。

常见清洗动作:

  • 去掉重复页眉页脚、目录、版权声明、导航栏。
  • 合并 PDF 抽取后被错误断开的句子。
  • 保留标题层级,让 chunk 知道自己属于哪一节。
  • 规范空格、换行和异常字符。
  • 删除明显无意义的样板文本,如“点击这里查看更多”。
  • 给表格补表头,让每行独立可读。

一个例子:

text
原始抽取:
  第 12 页
  员工手册 2026
  年假
  入职满 1 年
  5 天
  入职满 3 年
  7 天
  员工手册 2026

清洗后:
  标题:员工手册 / 假期政策 / 年假
  内容:员工入职满 1 年享有 5 天年假;入职满 3 年享有 7 天年假。
  来源:employee-handbook.pdf,第 12 页

清洗不要过度。比如制度里的“除外”“不得”“仅限”“生效日期”很关键,不能为了简洁删掉。

第三步:分块 ​

RAG 检索的最小单位通常不是整篇文档,而是 chunk。

chunk 太大:

  • 检索命中后夹带大量无关内容。
  • prompt 里塞不了几条证据。
  • 模型看见一堆文字,反而抓不住关键句。

chunk 太小:

  • 一句话被切断,证据不完整。
  • 条件和结论分在两个 chunk,模型只看到半截。
  • 标题、表头、限定条件丢失。

同一段文档:

text
原文:
  退货政策:商品签收后 7 天内可无理由退货,需保持包装完好。
  生鲜类目不支持无理由退货。退款将在审核通过后 3 个工作日内原路返回。

切太小:
  块 A:商品签收后 7 天内可无理由退货
  块 B:需保持包装完好
  块 C:生鲜类目不支持无理由退货

问题:
  用户问“生鲜能不能 7 天无理由退货?”
  如果只召回块 A,就会答错。

合理分块:
  块 A:退货政策:商品签收后 7 天内可无理由退货,需保持包装完好。
        生鲜类目不支持无理由退货。
  块 B:退款时效:退款将在审核通过后 3 个工作日内原路返回。

每个 chunk 应尽量包含完整的条件和结论。相邻块保留重叠内容(overlap)可以减少边界处的信息丢失,但仍需检查条款中的例外是否被切开。

常见分块策略 ​

策略做法适合场景风险
固定长度切分每 300-800 token 切一块,带 overlap快速原型、纯文本资料可能切断语义
按段落切分按自然段、列表、标题边界切文档结构清楚的资料段落长短不均
按标题层级切分每个标题小节作为基本单位Markdown、手册、知识库小节太长时仍要二次切
语义切分根据语义变化切块长文章、复杂说明实现复杂,成本更高
表格按行切分每行带上表头和上下文价格表、政策表、清单行太短时语义不足
父子块切分小块用于检索,大块用于回答长文档、需要上下文的问答实现稍复杂

一个实用起点:

  • 普通说明文档:chunk_size=500-800 token,overlap=50-100 token。
  • FAQ:一问一答作为一个 chunk。
  • 制度文档:按条款切,保留上级标题。
  • 表格:每行拼上表头,必要时再按主题合并。
  • 代码文档:按函数、类、标题和代码块边界切,不要把代码块切碎。

块长需要结合文档结构、embedding 输入上限和检索评测调整。这里的 token 是模型分词后的单位;如果切分器按字符计数,chunk_size=500 就不等于 500 token。评测时要检查正确证据能否被召回,以及命中后是否保留了完整条件。

chunk 的文本与元数据 ​

除文本和向量外,chunk 还需要保存标识与来源。下面展示文本和元数据,省略向量字段:

json
{
  "id": "employee-handbook:p12:chunk-03",
  "text": "员工入职满 1 年享有 5 天年假,满 3 年享有 7 天年假。",
  "metadata": {
    "source": "employee-handbook.pdf",
    "page": 12,
    "title": "员工手册",
    "section": "假期政策 / 年假",
    "doc_version": "2026-08",
    "updated_at": "2026-08-09",
    "chunk_version": "v1",
    "embedding_model": "text-embedding-3-small",
    "dimension": 1536
  }
}

metadata 的作用:

  • 过滤:只查某个部门、产品线、时间范围、文档类型。
  • 引用:回答里能附来源和页码。
  • 排查:答案错了,能找到具体是哪一块资料造成的。
  • 重建:换模型或改分块策略时,知道哪些 chunk 要重新生成。

为片段保留文档背景 ​

分块后,有些句子仍依赖上级标题才能确定含义。例如《2026 年境内差旅办法》的“报销提交期限”一节被切出这样一句:

员工应在行程结束后 30 个自然日内提交。

用户问“2026 年国内出差的报销期限”时,这个片段缺少年份、适用范围和报销主题,可能难以匹配。可以在建立索引前,把文档标题和章节路径拼到原文前面:

text
文档:2026 年境内差旅办法
章节:报销提交期限
原文:员工应在行程结束后 30 个自然日内提交。

用拼接后的文本生成 embedding;如果同时使用 BM25,也让这些背景词参与索引。只把标题存进 metadata,通常不会自动影响文本向量或全文匹配,需要显式选择参与索引的字段。

标题和章节路径无法解释的跨段指代,还可以由模型读取文档后,为每个片段生成简短背景。Anthropic 的 Contextual Retrieval 就采用这种方式,再将背景与原文一起用于向量和 BM25 检索。模型生成会增加建库成本,也可能误解主体或版本,因此可以先用标题拼接,确有信息缺失时再引入生成步骤。

原文与补充背景应分别保存。回答时提供原文及可核对的标题、适用范围,引用仍定位到原文;生成的背景不能作为新增事实的依据。背景应来自权限兼容的资料,来源更新后也需检查是否重新生成。这里补充背景是为了改善检索匹配;命中后需要读取更完整的条款时,可以使用父子块或句子窗口。

第四步:向量化并入库 ​

每个 chunk 都要经过 embedding 模型变成向量,再写入向量数据库。

text
chunk text -> embedding model -> vector -> vector store

入库时要注意:

  • 查询侧和文档侧必须使用兼容的 embedding 配置,包括模型版本、维度和预处理。模型若要求不同的 query/document 前缀,要分别遵守。
  • 换 embedding 模型或维度,通常要重建整个向量库。
  • 入库要可重复执行,同一文档更新后能删除旧 chunk 或覆盖旧 chunk。
  • 大量文档应批量 embedding,减少请求开销,并按服务限流设置批量大小。
  • 文档删除后,向量库里的旧 chunk 也要删除,否则会答出过期内容。

建库伪代码:

python
def build_index(documents):
    for doc in documents:
        text = load(doc)
        cleaned = clean(text)
        chunks = split_into_chunks(cleaned, metadata=doc.metadata)
        for chunk in chunks:
            vector = embed(chunk.text)
            vector_store.upsert(
                id=chunk.id,
                vector=vector,
                text=chunk.text,
                metadata=chunk.metadata,
            )

upsert 只会写入或覆盖指定 ID,不会自动删除新版本中已不存在的块。文档更新时应记录新旧块的归属,清理失效记录;批量入库还需要失败重试、进度记录和版本管理,避免查询到只写入一半的版本。

第五步:在线查询 ​

在线查询以检索为起点,根据需要加入查询改写、过滤和重排:

text
用户问题
  -> 可选:问题改写 / 补全上下文
  -> query embedding
  -> metadata 过滤
  -> 向量检索 / 关键词检索 / 混合检索
  -> 可选:reranker 重排
  -> 取证据片段
  -> 拼 prompt
  -> 生成答案

查询改写 ​

用户问题经常很短,直接检索效果不好。

text
用户:这个能报吗?

如果当前对话上文提到“出差打车发票”,更适合检索的 query 是:

text
差旅 打车 发票 报销 条件

查询改写通常包括:

  • 把代词换成实体。
  • 补上当前对话里的上下文。
  • 加上可能出现在文档里的同义词。

但也别改写过头。用户问“Python 3.12 是否支持某功能”,不要改成泛泛的“Python 功能支持情况”,精确版本会丢。

metadata 过滤 ​

metadata 过滤用于限定检索范围,例如部门、文档类型和租户。过滤条件可由后端生成,再交给检索引擎执行。

例子:

json
{
  "doc_type": "policy",
  "department": "hr",
  "tenant_id": "company-a",
  "doc_version": "2026-08"
}

例如,年假问题可以限制在 HR 制度中查询,租户条件则用于隔离不同公司的资料。业务范围和访问权限需要分别处理,不能只凭一个部门标签判断用户是否有权访问。

权限范围必须由工具或后端根据登录身份确定。无权限资料不能进入模型上下文。查询制度时还要区分文档更新时间和制度生效时间,不能简单地把最新上传的文档当作适用版本。

向量检索、关键词检索和混合检索 ​

向量检索擅长语义相似。关键词检索擅长精确词。

查询类型更适合
“自动猫粮机怎么选”找“宠物喂食器选购”向量检索
订单号 A1002、错误码 ERR_CONN_RESET关键词检索
“生鲜商品能不能 7 天无理由退货”混合检索
“Python 3.12 这个版本的限制”混合检索

两路检索可以按下面的方式组合,候选数量再通过评测调整:

text
dense 向量检索 top 20
BM25 / 全文检索 top 20
合并去重
reranker 重排
取 top 3-8 给模型

重排序 ​

top-k 中可能混入主题相似、却无法回答当前问题的片段。例如用户问退款期限,检索结果却排在前面的是退货申请条件。

重排序(reranking)对已召回的候选重新打分。常见的 cross-encoder 会同时读取问题和片段,比单独编码后比较向量更适合判断两者的具体关联。

text
向量检索 top 20 -> reranker 打分 -> 取 top 5

什么时候尤其该加 rerank:

  • 文档很多,top-k 经常混入相似但无用的片段。
  • 问题需要精确条件,如金额、日期、例外规则。
  • 用户问题很短,向量检索容易发散。
  • 资料里有大量重复模板,如很多政策文档都写“适用范围”。

rerank 会增加延迟和计算成本,通常先召回几十条候选再重排。它只能调整候选顺序,无法补回未被召回的证据。混合检索的融合方式见混合检索与 RRF,重排模型的选择见重排序。

第六步:组装上下文并生成答案 ​

选择和组织证据 ​

检索返回的是候选片段。放入 prompt 前,还需要去重、筛选和组织,避免无关内容占用上下文。

一个基本模板:

text
你是知识库问答助手。请只依据【资料】回答【问题】。
如果资料中没有答案,请说“资料中未提及”,不要猜测。
涉及时间、金额、条件、例外情况时,必须保留原文限制。
回答后列出使用的来源。

【资料】
[1] 来源:员工手册,第 12 页
员工入职满 1 年享有 5 天年假,满 3 年享有 7 天年假。

[2] 来源:员工手册,第 13 页
年假需提前 3 个工作日在系统中提交申请。

【问题】
我入职两年了,今年能休几天年假?

拼上下文时要做几件事:

  • 去掉重复 chunk。
  • 选择与问题相关、在适用时间内有效、且用户有权访问的资料。
  • 每条资料带来源编号,方便回答引用。
  • 为系统指令、问题、对话历史和输出预留空间,剩余 token 预算用于证据。
  • 保留否定、例外、适用范围和生效日期。

如果多个 chunk 来自同一版本的同一小节,可以合并重叠内容,减少重复文本占用的 token。

生成答案与核对引用 ​

上面的 prompt 已经限定回答依据。资料之间存在冲突时,还应要求模型分别列出来源,不要把不同版本的条款合并成一条规则。

一个好的回答:

text
你入职两年,属于“满 1 年但未满 3 年”,今年可休 5 天年假。
如果要申请,需提前 3 个工作日在系统中提交。

来源:[1] 员工手册第 12 页;[2] 员工手册第 13 页。

一个差的回答:

text
一般来说,入职两年通常可以休 5-10 天左右,具体看公司规定。

后一个回答没有引用证据,还把明确的制度说成了模糊常识。

生成后需要核对来源编号是否存在,以及对应原文能否支持答案。编号检查可以用代码完成,事实支持关系仍需评测或抽查;仅在 prompt 中写“不要猜测”不能保证答案正确。

资料不足时的处理 ​

用户问:

text
我们公司宠物假能休几天?

如果本次检索没有找到相关规定,可以回答:

text
本次检索未找到“宠物假”的相关规定,无法确认可休天数。

也可以请用户补充对应制度文件,再针对新资料查询。

检索未命中不等于知识库中一定没有这项制度。检索服务超时也应报告查询失败,不能当作正常的无结果处理。

一次端到端问答长什么样 ​

text
用户问:
  签收几天内能退货?生鲜也可以吗?

1. 查询改写:
  退货政策 签收后几天 无理由退货 生鲜类目

2. 检索 top-5:
  命中块 A:
    商品签收后 7 天内可无理由退货,需保持包装完好。
    生鲜类目不支持无理由退货。
    来源:售后政策,第 2 页

  命中块 B:
    退款将在审核通过后 3 个工作日内原路返回。
    来源:售后政策,第 3 页

3. rerank:
  块 A 排第一,因为它同时回答“几天”和“生鲜是否可以”。

4. 拼 prompt:
  资料:[块 A]
  问题:签收几天内能退货?生鲜也可以吗?

5. 模型回答:
  普通商品签收后 7 天内可无理由退货,但需要保持包装完好。
  生鲜类目不支持无理由退货。
  来源:售后政策第 2 页。

块 A 已包含退货期限和例外条件;块 B 讲退款到账时间,与当前问题无关,可以不放进上下文。

RAG 质量怎么评估 ​

不要只看最终答案。RAG 至少要拆成四层评估:

层级评估什么例子
文档处理文档是否加载完整、清洗是否删错内容PDF 表格有没有乱、页码还在不在
检索召回正确证据是否出现在 top-k 里top-5 里有没有年假规则
重排质量最有用的证据是否排在前面能回答问题的 chunk 是否排到 top-3
回答忠实度答案是否只依据资料、有没有漏条件是否保留“生鲜不支持”

一组最小评测集可以这样准备:

  • 10 个资料里有明确答案的问题。
  • 5 个资料里没有答案的问题。
  • 5 个需要保留例外条件的问题。
  • 5 个需要精确编号、日期、版本的问题。
  • 5 个容易被相似文档干扰的问题。

每条评测记录至少包含:

json
{
  "question": "入职两年今年能休几天年假?",
  "expected_sources": ["employee-handbook:p12"],
  "must_include": ["5 天", "满 1 年", "未满 3 年"],
  "must_not_include": ["7 天", "10 天"]
}

这样才能知道问题出在哪:是没检索到、检索到了但排序差,还是模型看到了证据却答错。

排查常见问题 ​

  • 只看 demo,不做评测:问一个问题答对,不代表系统可靠。
  • chunk 没来源:答案错了追不回原文,也没法引用。
  • 分块太随意:条件和结论分离,导致模型只看到半截事实。
  • 只用向量检索查精确编号:订单号、错误码、版本号更适合关键词或 metadata 过滤。
  • top-k 越大越好:召回更多也会带来更多噪声,模型可能被干扰。
  • 不处理过期文档:新旧制度都在库里,模型可能引用旧规则。
  • 资料冲突不暴露:两份文档说法不同,模型强行合成一个答案。
  • 没有权限过滤:把用户无权看的资料塞进 prompt,已经算泄露。
  • prompt 不限制依据资料回答:模型可能把检索资料和自身记忆混在一起。
  • 没有空结果策略:检索不到时仍要求生成完整答案,容易出现无依据的结论。

排错时依次检查:正确证据是否进入候选集、是否被选入最终上下文、答案是否忠实于证据。候选里没有证据,查文档处理、过滤和召回;候选里有、上下文里没有,查重排与上下文组装;上下文已包含完整证据,再查生成和引用。更完整的指标见 RAG 评估。

本篇由应用按固定流程执行检索。下一篇把 RAG 做成 Agent 的工具介绍如何把检索封装成工具,由 Agent 根据任务决定何时调用。

持续学习,持续实践。