RAG 完整链路
RAG 解决什么问题
RAG(Retrieval-Augmented Generation,检索增强生成)在生成答案前,先从外部知识库检索相关资料,再把资料和问题一起交给模型。它适合处理模型训练中未覆盖的私有文档、更新后的制度,以及需要核对出处的细节。
闭卷问答:
用户问题 -> 模型凭训练记忆回答
RAG 问答:
用户问题 -> 查资料 -> 把资料交给模型 -> 模型基于资料回答RAG 能缓解这些问题:
- 知识截止:模型训练后发生的新信息,它本来不知道。
- 私有知识:公司手册、产品文档、项目记录、个人笔记,模型没训练过。
- 细节错误:制度、金额、版本、日期等信息可以从原文核对,减少依赖模型记忆。
- 可溯源:回答后能告诉用户“根据哪份资料、哪一页、哪一段”。
RAG 的效果仍受各环节影响:
- 检索不到正确资料,模型仍然答不准。
- 检索到了错误或过期资料,答案也会错。
- 文档切分太差,模型只看到半截证据。
- 即使 prompt 要求依据资料,模型也可能遗漏限制或补出没有依据的结论。
向量检索是 RAG 常用的检索方式,也可以使用全文搜索、数据库查询等方式取得资料。本文接着 Embedding 基础,以向量知识库为主介绍完整流程。
两个阶段、六个步骤
一条完整 RAG 链路分成两个阶段。
离线阶段:建库
离线阶段通常不是每次用户提问都跑,而是在文档新增、更新、删除时执行。
原始文档 -> 加载 -> 清洗 -> 分块 -> 向量化 -> 写入向量库在线阶段:问答
在线阶段每次用户提问都会跑。
用户问题 -> 检索 -> 拼上下文 -> 模型生成答案
↑
可加入查询改写、业务过滤和重排按实现顺序,可以分成六步:
| 步骤 | 名称 | 做什么 |
|---|---|---|
| 1 | 加载 load | 读取 PDF、Markdown、网页、数据库记录等原始资料 |
| 2 | 清洗 clean | 去除无关噪声,修复抽取后的文本结构 |
| 3 | 分块 chunk | 把长文档切成适合检索的小片段 |
| 4 | 向量化并入库 embed & store | 每个 chunk 生成 embedding,连同原文和元数据写入向量库 |
| 5 | 检索 retrieve | 用户问题也转成向量,从库里找相关 chunk |
| 6 | 生成 generate | 将选中的证据放入上下文,让模型基于资料回答 |
第一步:加载文档
加载时需要处理不同格式的文本结构,并保留后续引用所需的位置信息。
| 来源 | 常见问题 | 处理建议 |
|---|---|---|
| Markdown / txt | 标题、列表、代码块结构容易被打散 | 保留标题层级和段落边界 |
| 页眉页脚、页码、分栏、表格抽取混乱 | 抽取后人工抽查几页,保留页码 | |
| 网页 | 导航栏、广告、版权声明、相关推荐混进正文 | 只保留正文区域,去掉样板噪声 |
| Word / 飞书 / Notion | 标题、表格、评论、附件关系复杂 | 尽量转成结构化 Markdown 再处理 |
| 数据库记录 | 字段短、上下文少 | 拼上字段名和业务上下文 |
| 表格 | 单元格脱离表头后意义丢失 | 每行拼入表头,必要时按行或按主题建 chunk |
加载时要尽量保留来源信息:
{
"source": "employee-handbook.pdf",
"page": 12,
"title": "员工手册",
"section": "假期政策",
"updated_at": "2026-08-09"
}这些 metadata 后面会用于过滤、引用和排查问题。
第二步:清洗文档
清洗要去掉干扰检索的噪声,同时保留能回答问题的事实。
常见清洗动作:
- 去掉重复页眉页脚、目录、版权声明、导航栏。
- 合并 PDF 抽取后被错误断开的句子。
- 保留标题层级,让 chunk 知道自己属于哪一节。
- 规范空格、换行和异常字符。
- 删除明显无意义的样板文本,如“点击这里查看更多”。
- 给表格补表头,让每行独立可读。
一个例子:
原始抽取:
第 12 页
员工手册 2026
年假
入职满 1 年
5 天
入职满 3 年
7 天
员工手册 2026
清洗后:
标题:员工手册 / 假期政策 / 年假
内容:员工入职满 1 年享有 5 天年假;入职满 3 年享有 7 天年假。
来源:employee-handbook.pdf,第 12 页清洗不要过度。比如制度里的“除外”“不得”“仅限”“生效日期”很关键,不能为了简洁删掉。
第三步:分块
RAG 检索的最小单位通常不是整篇文档,而是 chunk。
chunk 太大:
- 检索命中后夹带大量无关内容。
- prompt 里塞不了几条证据。
- 模型看见一堆文字,反而抓不住关键句。
chunk 太小:
- 一句话被切断,证据不完整。
- 条件和结论分在两个 chunk,模型只看到半截。
- 标题、表头、限定条件丢失。
同一段文档:
原文:
退货政策:商品签收后 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 还需要保存标识与来源。下面展示文本和元数据,省略向量字段:
{
"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 年国内出差的报销期限”时,这个片段缺少年份、适用范围和报销主题,可能难以匹配。可以在建立索引前,把文档标题和章节路径拼到原文前面:
文档:2026 年境内差旅办法
章节:报销提交期限
原文:员工应在行程结束后 30 个自然日内提交。用拼接后的文本生成 embedding;如果同时使用 BM25,也让这些背景词参与索引。只把标题存进 metadata,通常不会自动影响文本向量或全文匹配,需要显式选择参与索引的字段。
标题和章节路径无法解释的跨段指代,还可以由模型读取文档后,为每个片段生成简短背景。Anthropic 的 Contextual Retrieval 就采用这种方式,再将背景与原文一起用于向量和 BM25 检索。模型生成会增加建库成本,也可能误解主体或版本,因此可以先用标题拼接,确有信息缺失时再引入生成步骤。
原文与补充背景应分别保存。回答时提供原文及可核对的标题、适用范围,引用仍定位到原文;生成的背景不能作为新增事实的依据。背景应来自权限兼容的资料,来源更新后也需检查是否重新生成。这里补充背景是为了改善检索匹配;命中后需要读取更完整的条款时,可以使用父子块或句子窗口。
第四步:向量化并入库
每个 chunk 都要经过 embedding 模型变成向量,再写入向量数据库。
chunk text -> embedding model -> vector -> vector store入库时要注意:
- 查询侧和文档侧必须使用兼容的 embedding 配置,包括模型版本、维度和预处理。模型若要求不同的 query/document 前缀,要分别遵守。
- 换 embedding 模型或维度,通常要重建整个向量库。
- 入库要可重复执行,同一文档更新后能删除旧 chunk 或覆盖旧 chunk。
- 大量文档应批量 embedding,减少请求开销,并按服务限流设置批量大小。
- 文档删除后,向量库里的旧 chunk 也要删除,否则会答出过期内容。
建库伪代码:
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,不会自动删除新版本中已不存在的块。文档更新时应记录新旧块的归属,清理失效记录;批量入库还需要失败重试、进度记录和版本管理,避免查询到只写入一半的版本。
第五步:在线查询
在线查询以检索为起点,根据需要加入查询改写、过滤和重排:
用户问题
-> 可选:问题改写 / 补全上下文
-> query embedding
-> metadata 过滤
-> 向量检索 / 关键词检索 / 混合检索
-> 可选:reranker 重排
-> 取证据片段
-> 拼 prompt
-> 生成答案查询改写
用户问题经常很短,直接检索效果不好。
用户:这个能报吗?如果当前对话上文提到“出差打车发票”,更适合检索的 query 是:
差旅 打车 发票 报销 条件查询改写通常包括:
- 把代词换成实体。
- 补上当前对话里的上下文。
- 加上可能出现在文档里的同义词。
但也别改写过头。用户问“Python 3.12 是否支持某功能”,不要改成泛泛的“Python 功能支持情况”,精确版本会丢。
metadata 过滤
metadata 过滤用于限定检索范围,例如部门、文档类型和租户。过滤条件可由后端生成,再交给检索引擎执行。
例子:
{
"doc_type": "policy",
"department": "hr",
"tenant_id": "company-a",
"doc_version": "2026-08"
}例如,年假问题可以限制在 HR 制度中查询,租户条件则用于隔离不同公司的资料。业务范围和访问权限需要分别处理,不能只凭一个部门标签判断用户是否有权访问。
权限范围必须由工具或后端根据登录身份确定。无权限资料不能进入模型上下文。查询制度时还要区分文档更新时间和制度生效时间,不能简单地把最新上传的文档当作适用版本。
向量检索、关键词检索和混合检索
向量检索擅长语义相似。关键词检索擅长精确词。
| 查询类型 | 更适合 |
|---|---|
| “自动猫粮机怎么选”找“宠物喂食器选购” | 向量检索 |
订单号 A1002、错误码 ERR_CONN_RESET | 关键词检索 |
| “生鲜商品能不能 7 天无理由退货” | 混合检索 |
| “Python 3.12 这个版本的限制” | 混合检索 |
两路检索可以按下面的方式组合,候选数量再通过评测调整:
dense 向量检索 top 20
BM25 / 全文检索 top 20
合并去重
reranker 重排
取 top 3-8 给模型重排序
top-k 中可能混入主题相似、却无法回答当前问题的片段。例如用户问退款期限,检索结果却排在前面的是退货申请条件。
重排序(reranking)对已召回的候选重新打分。常见的 cross-encoder 会同时读取问题和片段,比单独编码后比较向量更适合判断两者的具体关联。
向量检索 top 20 -> reranker 打分 -> 取 top 5什么时候尤其该加 rerank:
- 文档很多,top-k 经常混入相似但无用的片段。
- 问题需要精确条件,如金额、日期、例外规则。
- 用户问题很短,向量检索容易发散。
- 资料里有大量重复模板,如很多政策文档都写“适用范围”。
rerank 会增加延迟和计算成本,通常先召回几十条候选再重排。它只能调整候选顺序,无法补回未被召回的证据。混合检索的融合方式见混合检索与 RRF,重排模型的选择见重排序。
第六步:组装上下文并生成答案
选择和组织证据
检索返回的是候选片段。放入 prompt 前,还需要去重、筛选和组织,避免无关内容占用上下文。
一个基本模板:
你是知识库问答助手。请只依据【资料】回答【问题】。
如果资料中没有答案,请说“资料中未提及”,不要猜测。
涉及时间、金额、条件、例外情况时,必须保留原文限制。
回答后列出使用的来源。
【资料】
[1] 来源:员工手册,第 12 页
员工入职满 1 年享有 5 天年假,满 3 年享有 7 天年假。
[2] 来源:员工手册,第 13 页
年假需提前 3 个工作日在系统中提交申请。
【问题】
我入职两年了,今年能休几天年假?拼上下文时要做几件事:
- 去掉重复 chunk。
- 选择与问题相关、在适用时间内有效、且用户有权访问的资料。
- 每条资料带来源编号,方便回答引用。
- 为系统指令、问题、对话历史和输出预留空间,剩余 token 预算用于证据。
- 保留否定、例外、适用范围和生效日期。
如果多个 chunk 来自同一版本的同一小节,可以合并重叠内容,减少重复文本占用的 token。
生成答案与核对引用
上面的 prompt 已经限定回答依据。资料之间存在冲突时,还应要求模型分别列出来源,不要把不同版本的条款合并成一条规则。
一个好的回答:
你入职两年,属于“满 1 年但未满 3 年”,今年可休 5 天年假。
如果要申请,需提前 3 个工作日在系统中提交。
来源:[1] 员工手册第 12 页;[2] 员工手册第 13 页。一个差的回答:
一般来说,入职两年通常可以休 5-10 天左右,具体看公司规定。后一个回答没有引用证据,还把明确的制度说成了模糊常识。
生成后需要核对来源编号是否存在,以及对应原文能否支持答案。编号检查可以用代码完成,事实支持关系仍需评测或抽查;仅在 prompt 中写“不要猜测”不能保证答案正确。
资料不足时的处理
用户问:
我们公司宠物假能休几天?如果本次检索没有找到相关规定,可以回答:
本次检索未找到“宠物假”的相关规定,无法确认可休天数。也可以请用户补充对应制度文件,再针对新资料查询。
检索未命中不等于知识库中一定没有这项制度。检索服务超时也应报告查询失败,不能当作正常的无结果处理。
一次端到端问答长什么样
用户问:
签收几天内能退货?生鲜也可以吗?
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 个容易被相似文档干扰的问题。
每条评测记录至少包含:
{
"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 根据任务决定何时调用。