RAG:从检索增强生成到可维护的知识系统

RAG 不只是把文档切片后塞进提示词。本文从问题定义、索引、检索、重排、生成、引用和评估一路拆开,给出一套可以落地的知识库架构。

目录

很多人第一次接触 RAG(Retrieval-Augmented Generation,检索增强生成)时,会把它理解成一句话:

先从知识库里搜几段文字,再把这些文字放进 Prompt,让大模型回答问题。

这句话没有错,但它把真正困难的部分都藏起来了。

一个能在演示里工作的 RAG,和一个能让用户信任、能持续更新、能定位错误、能控制成本的知识系统,中间还隔着一整套工程。文档该怎样切分?检索到的内容是否真的回答了问题?多个片段之间发生冲突怎么办?模型引用了哪一段证据?回答错了,究竟是没有召回、排序不对,还是生成阶段胡说?

本文把 RAG 当作一条完整的数据链路来解释,并给出一个适合从小项目开始逐步演进的架构。

1. RAG 解决的是什么问题

大语言模型的知识主要存放在参数里。预训练和微调会让模型学到大量语言规律、事实和任务模式,但参数不是一个方便查询的数据库:

  • 新资料不会自动进入模型参数;
  • 私有资料通常不在训练数据里;
  • 参数中的知识难以追溯来源;
  • 模型可能把相似事实混在一起;
  • 每次为了新知识重新训练模型,成本高、周期长。

RAG 的思路是把知识分成两种记忆:

  1. 参数记忆:模型自身学到的通用语言能力、推理能力和世界知识。
  2. 非参数记忆:放在文档库、数据库或向量索引中的外部知识。

回答问题时,系统先从非参数记忆中找相关证据,再让模型结合证据生成回答。最简化的形式可以写成:

用户问题
  -> 问题预处理
  -> 检索相关片段
  -> 重排与过滤
  -> 组织上下文
  -> 大模型生成
  -> 引用、校验与返回

原始 RAG 论文把它描述为参数化记忆与非参数化记忆的组合,并以一个神经检索器访问维基百科向量索引。论文还比较了两类生成方式:整段生成共享同一批检索片段,以及不同生成 token 可以使用不同片段。这个思想后来被大量产品化,但工程上的 RAG 往往需要更多的过滤、权限和观测层。

2. 先别急着上向量数据库

在实现之前,最好先回答三个问题。

2.1 知识是否会变化

如果知识几乎不变,而且数量很小,直接把资料整理成 Prompt 或配置文件,可能比 RAG 更简单。

如果知识会频繁更新,例如产品文档、内部流程、政策、库存或课程资料,那么把知识放在外部索引里更合适。更新时只需要重新处理发生变化的文档,不必重新训练模型。

2.2 用户需要的是事实还是写作风格

RAG 擅长补充“模型需要查到的事实”。它不一定能解决“模型说话方式不对”或“输出格式不稳定”。

  • 让模型知道公司的报销规则,优先考虑 RAG。
  • 让模型固定使用某种语气和 JSON 格式,优先考虑 Prompt、约束解码或微调。
  • 让模型学会一套稳定的分类动作,可能需要 RAG 与微调结合。

2.3 错误的代价有多高

聊天机器人偶尔答错,和医疗、财务、权限审批系统答错,风险完全不同。高风险场景不能把“检索到了相关内容”当成“回答一定正确”,必须加入来源显示、拒答策略、人工审核或规则校验。

3. 一条完整的 RAG 数据链路

3.1 文档接入

文档接入不只是读取文件。系统需要先统一来源、权限、版本和元数据。

建议至少记录:

{
  "document_id": "handbook-2026-09",
  "source": "https://example.com/handbook",
  "title": "员工手册",
  "version": "2026.09",
  "updated_at": "2026-09-01T09:00:00+08:00",
  "tenant_id": "company-a",
  "access_level": "internal"
}

如果原始文件是 PDF、网页、扫描图片或表格,还要考虑解析质量。解析阶段丢失标题、表格关系和脚注,后面再好的向量模型也无法恢复。

3.2 清洗与结构化

常见清洗动作包括:

  • 去掉导航、页眉页脚和重复版权声明;
  • 保留标题层级;
  • 把表格转换成具有字段语义的文本;
  • 保留列表顺序和代码块边界;
  • 合并被分页打断的句子;
  • 记录原文位置,便于回链和引用。

清洗不是越彻底越好。过度清洗可能把“第 3 条适用于第 2 条例外情况”这类关系删掉。好的清洗目标是让文本更容易检索,同时不破坏它原有的逻辑结构。

3.3 切片:不要只按字符数截断

向量检索通常以 chunk(文本块)为单位。最简单的方法是每 500 个字符切一段并保留 50 个字符重叠,但这只是一个起点。

切片策略可以分成三类:

  1. 固定长度切片:实现简单,适合快速验证。
  2. 递归切片:优先在标题、段落、句子边界切分,超长时再继续拆。
  3. 结构感知切片:按照 Markdown 标题、HTML DOM、函数、类、表格或 FAQ 问答对切分。

切片时需要同时考虑两件事:

  • chunk 要足够小,检索结果才不会被大量无关内容淹没;
  • chunk 要足够完整,读者拿到它时仍然看得懂主语、条件和结论。

例如下面三段文字不能机械地分开:

退款规则
标准商品支持签收后 7 天内无理由退货。
定制商品不适用该规则,除非商品存在质量问题。

如果只召回最后一句,模型很可能把“定制商品不适用”误套到所有商品上。实际工程中可以使用父子块:用较小的子块负责检索,用包含标题和上下文的父块负责送入模型。

3.4 向量化与索引

Embedding 模型把文本映射成向量。语义相近的文本通常在向量空间里更接近。常见相似度包括余弦相似度、点积和欧氏距离。

余弦相似度为:

cos(a, b) = (a · b) / (||a|| ||b||)

向量库只是存储和查询工具,不会自动让检索变聪明。真正影响效果的通常是:

  • Embedding 模型是否适合中文、代码或领域术语;
  • 文档与查询是否使用一致的预处理;
  • 是否保存关键词和元数据;
  • 是否做权限过滤;
  • 向量索引参数是否与数据规模匹配。

实际系统经常使用混合检索:

语义检索:找到表达方式不同但含义相近的内容
关键词检索:找到产品名、错误码、版本号、专有名词
混合结果:合并、去重,再交给重排模型

只使用向量检索时,ERR_CONNECTION_RESET、v1.4.2、人名和编号等精确字符串可能表现不稳定;只使用关键词检索时,又很难处理同义改写。

3.5 查询改写

用户的提问不一定适合直接检索。例如:

用户:这个东西怎么退?

检索器不知道“这个东西”指什么。查询改写可以结合对话历史,将它改成:

如何退还在本店购买的标准商品?

但查询改写也有风险:模型可能擅自补充用户没有说过的条件。因此应保留原问题,并在改写失败时回退;对于权限、订单和医疗等场景,改写后的实体必须经过结构化校验。

多查询(multi-query)也是一种方法:让模型生成几个不同措辞的检索问题,再合并结果。它可以提高召回率,但会增加延迟和调用成本,不应默认对每个请求都开启。

4. 检索到不等于用得好

4.1 Top-k 不是越大越好

k=10 并不天然比 k=3 好。更多片段会增加召回机会,也会增加上下文噪声、重复和互相冲突。

可以把检索过程拆成两阶段:

第一阶段:便宜、快速地召回较多候选,例如 20 个
第二阶段:用 reranker 对候选重排,只保留 3 到 8 个

重排模型可以同时看查询和候选文本,判断“这段内容是否真的回答了这个问题”,通常比单纯比较两个 embedding 更细致。

4.2 元数据过滤要先于语义相似度

租户、用户权限、语言、产品版本和时间范围,不应该交给模型自行判断。它们应在检索层成为硬过滤条件:

WHERE tenant_id = 当前租户
  AND access_level <= 当前用户权限
  AND valid_from <= 当前时间
  AND (valid_to IS NULL OR valid_to > 当前时间)

先做权限过滤,再做相似度排序,避免“相似但不该看到”的文档进入上下文。

4.3 去重和多样性

多个 chunk 可能来自同一段文档。简单取 top-k 会让上下文被重复内容占满。可以按照文档、章节或来源做去重,并使用 MMR(最大边际相关性)在相关性和多样性之间取平衡。

直觉上,MMR 不是只问“这段像不像问题”,还会问“它和已经选中的片段是不是完全重复”。

5. 让模型只在证据范围内回答

RAG 不是防止幻觉的魔法。模型仍然可能:

  • 把两个片段拼成原文没有表达的结论;
  • 把旧版本和新版本混在一起;
  • 看到关键词相似就强行回答;
  • 在证据不足时用常识补全。

因此 Prompt 应明确规定边界:

你是一个严谨的知识库助手。
只能依据“参考资料”回答问题。
如果参考资料不足以支持结论,请明确说“当前资料不足”,不要用常识补全。
回答中的每个关键事实都要附上对应资料编号。
如果资料之间冲突,分别列出版本和来源,不要自行拍板。

更进一步,可以让模型输出结构化结果:

{
  "answer": "……",
  "citations": [
    {"chunk_id": "handbook-2026-09#refund-03", "claim": "……"}
  ],
  "confidence": "medium",
  "needs_human_review": false
}

注意:模型生成的 citation 不等于真实引用。系统应在服务端检查 citation 中的 chunk_id 是否来自本次检索结果,并把显示内容与实际 chunk 绑定,不能让模型自由编造链接。

6. RAG 的评估:把“感觉不错”拆成指标

要改进 RAG,必须建立一组小而真实的测试集。每条测试至少包括:

问题、期望答案、相关文档、不可使用的文档、是否允许拒答

可以把质量拆成四个层次:

6.1 召回质量

  • 相关文档是否在候选集合中?
  • Recall@k 是多少?
  • 文档版本和权限是否正确?

如果正确答案所在的 chunk 根本没有被召回,优化 Prompt 没有意义,应先查切片、Embedding、混合检索和查询改写。

6.2 排序质量

  • 最相关的片段是否排在前面?
  • 前几个结果是否重复?
  • 是否存在相似但不适用的章节?

这里可以观察 MRR、nDCG 或人工标注的相关性等级。

6.3 生成质量

  • 回答是否覆盖问题?
  • 是否忠实于证据?
  • 是否在证据不足时拒答?
  • 引用是否真的支持结论?

6.4 系统质量

  • 首 token 延迟和总延迟;
  • 每次请求的 token 与检索成本;
  • 索引更新延迟;
  • 不同租户之间是否隔离;
  • 失败时是否有可理解的错误信息。

最有价值的调试日志通常不是最终答案,而是这一整条链路:原问题、改写问题、过滤条件、候选 chunk、重排分数、最终上下文、模型答案和用户反馈。

7. 一个适合起步的架构

小项目不需要一开始就拆成很多微服务。可以先保持模块边界清楚:

ingest/
  loaders.py       # PDF、Markdown、网页、数据库
  normalize.py     # 清洗与结构化
  chunk.py         # 切片与父子块
  index.py         # embedding、向量库、关键词索引

query/
  rewrite.py       # 查询改写
  retrieve.py      # 混合检索与权限过滤
  rerank.py        # 重排
  context.py       # 去重、压缩、拼接上下文

answer/
  prompt.py        # 回答约束
  generate.py      # 模型调用
  cite.py          # 引用绑定与校验
  evaluate.py      # 离线评估与回归测试

第一版可以这样工作:

  1. 文档更新时,计算内容哈希。
  2. 哈希未变化的文档跳过处理。
  3. 新文档按标题和段落切片,保存来源与位置。
  4. 同时写入向量索引和关键词索引。
  5. 请求进来后先做权限和版本过滤。
  6. 召回 20 个候选,重排后保留 5 个。
  7. 把来源编号和文本一起交给模型。
  8. 服务端校验引用,再把答案和来源返回给用户。

这套架构已经足够支撑很多 FAQ、内部文档和个人知识库。只有当数据规模、并发或治理要求真正上来,再考虑独立的索引服务、异步队列和多路召回编排。

8. 常见失败原因

失败一:切片太大

每个 chunk 塞入一整页甚至一整篇文章,向量代表的是混合主题,检索结果看起来相关但不够精确。

失败二:切片太小

主语、条件和结论被拆散,召回到的句子脱离上下文,模型不得不猜。

失败三:只做向量检索

版本号、错误码、专有名词和精确名称经常更适合关键词检索。

失败四:把所有历史版本一起塞给模型

模型不一定知道哪一个版本更新。应该在索引层标出有效时间、版本和优先级。

失败五:没有拒答路径

当知识库没有答案时,系统仍然要求模型“回答得有帮助”,就容易出现一本正经的编造。

失败六:没有离线测试集

每次调整 chunk 或 top-k 后只能靠感觉判断,效果很难稳定复现。

9. RAG、微调和搜索怎样分工

可以用一个简单的判断表:

需求更适合的方案
查找经常更新的事实RAG / 搜索
给回答附来源RAG
改变输出语气和格式Prompt / 微调
学习固定的分类或判断动作微调,必要时结合 RAG
需要精确数字、编号和过滤结构化数据库 / 关键词检索
需要多步计算工具调用 / 工作流
资料量小且变化少直接放入上下文

真实应用往往不是在这些方案里三选一,而是让它们协作:数据库负责事实,搜索负责定位,RAG 负责组织语言,微调负责稳定行为,规则负责安全边界。

10. 最后的判断

RAG 的关键不是“把向量数据库接上大模型”,而是让一条证据链变得可解释:

这个问题被怎样理解?
哪些资料被找到了?
为什么选择这些资料?
答案中的每个结论来自哪里?
如果资料不足,系统会不会诚实地停下来?

当这些问题都能回答时,RAG 才从一个 Demo 变成一个知识系统。

对于个人项目,我会建议从最小闭环开始:先做 30 到 100 条真实问题,手动检查召回结果,加入来源显示和拒答,再考虑更复杂的重排、多查询和自动评估。先把证据链做实,模型换得再大也不会替你修复糟糕的知识组织。

参考资料


Ambient 默认关闭