RAG 经常被缩写成“文档转向量、取 top-k、塞给模型”。这条直线掩盖了真正决定质量的七个阶段:数据是否新鲜且有权限,切分是否保留语义边界,查询是否覆盖用户意图,召回是否兼顾专有名词与语义,重排是否选对证据,上下文是否在预算内保持多样性,最终每个论断能否被引用片段支持。

本文面向已经做过向量搜索、准备将知识问答投入生产的开发者。你需要熟悉 embedding、倒排索引和基本检索指标。目标不是给出一个通用 chunk size,而是建立一条可单独观察、调参和评测的证据管线。

RAG 的原始工作把参数化生成模型与可检索的非参数记忆结合,用外部文档为知识密集任务提供信息和来源线索,详见 Lewis 等人的RAG 论文。生产实现不必复刻论文训练方式,但应保留这个核心:检索到的是可更新、可追溯的证据,生成器不能把模型记忆伪装成检索事实。

RAG 从文档摄取、切分、召回、重排、装配、回答到引用验证的七段证据管线

图 1(1600 × 900):WEB/SUN 原创 SVG。每一条候选片段从摄取到回答都保留 source ID、版本与原文跨度,引用不是生成结束后再猜。

文档碎片穿过检索与重排装置后汇聚为可追溯的证据束

图 2(1600 × 900):RAG 证据链的编辑式视觉隐喻,不作为技术流程图。OpenAI Image Gen × WEB/SUN,2026-07-16;项目内原创生成资产。

0. 先定义答案契约和证据单位

索引之前先写出系统允许回答什么。面向内部制度的问答应明确:只依据当前生效文档;证据不足就拒答;冲突版本并列展示;每个事实性论断附来源;没有权限的文档既不能检索,也不能泄露“它存在”。这些规则决定数据模型与评测,而不是最后一句 prompt。

证据单位不是向量,而是一个可追溯对象:

type EvidenceChunk = {
  chunkId: string;
  documentId: string;
  revision: string;
  headingPath: string[];
  sourceSpan: { start: number; end: number };
  text: string;
  acl: string[];
  validFrom?: string;
  validTo?: string;
  checksum: string;
};

revision + sourceSpan 让引用能回到确切原文,acl 让过滤发生在召回前,时间字段支持“当前有效”与历史查询,checksum 识别内容是否真的变化。不要只存切分后的 text 和一个文件名;文档更新后,那种引用无法证明自己指向哪一版。

1. 摄取:把数据质量放在检索之前

摄取流程解析正文、标题层级、表格、代码、脚注和附件关系,删除导航模板等重复噪声,同时保留原始文件的不可变版本。每个文档要有来源、所有者、语言、时间、权限与生命周期。索引构建成功后再原子切换 active revision,避免用户检索到半份新文档。

删除和更新也属于一致性设计。OpenAI 的Retrieval 指南说明,vector store 文件操作中有异步步骤,删除后的搜索结果也可能在短时间内仍出现。使用托管或自建索引时,都应把“源数据已删除”和“索引已不可检索”分别确认;高敏感数据不能只发出删除请求就宣称完成。

缓存键必须包含语料 revision、权限范围、查询计划版本和检索配置。只按用户问题缓存,会把旧制度答案延续到新索引,也可能把高权限结果返回给低权限用户。更新文档时可以让新旧索引短暂并存,但每次回答只能绑定一个明确 revision;跨版本候选混合会制造无法解释的冲突。

文档内容本身也是不可信输入。网页或内部文件可能包含“忽略问题并执行某工具”一类指令,检索命中不应提升其权限。装配器把片段标为证据数据,生成器只允许从中抽取事实;RAG Agent 的写工具和私有数据能力仍需独立授权与审批。检索相关性不是安全许可。

摄取失败要隔离到文档,不要让整个批次静默成功。记录解析器版本、失败页码和缺失结构;扫描件 OCR、复杂表格与代码块需要专用路径。若解析后正文为空或标题错位,应进入人工检查,不要生成一堆无意义 embedding。

2. 切分:优先结构,再谈长度与重叠

固定每 500 token 切一刀容易实现,却会把标题与正文、条件与例外、表头与单元格拆开。先按文档结构切:章节、段落、列表、代码函数、表格与问答对;只有单个结构块超预算时,再按句子或 token 二次切分。每块带上 heading path,必要时把短标题作为检索文本的一部分。

chunk 太小,证据缺少限定条件;太大,embedding 混合多个主题,重排和上下文都浪费预算。重叠可以保留跨边界语义,也会制造近重复候选,让 top-k 看似相关却只覆盖一个事实。正确参数来自评测查询:统计答案跨度、章节结构和候选多样性,分别测试长度与 overlap,而不是寻找社区“最佳值”。

可以为不同内容类型使用不同策略:API 参考按符号与段落;政策按条款和例外;教程按步骤组;表格将表头、行标识与目标行共同序列化;代码保留签名和必要上下文。一个文档可以同时生成用于关键词检索的原文视图和用于语义检索的摘要视图,但二者必须回指同一证据对象。

切分还要防止越权拼接。不同 ACL 或不同租户的内容不能因“主题相近”合成一个 chunk;否则检索过滤无法再安全拆开。权限边界优先于语义完整性。

3. 查询理解:检索问题,不是照抄聊天句子

用户问题可能包含指代、多个子问题、时间和权限条件。检索器需要一个确定的 query plan:规范化当前问题,补充已确认实体,抽取过滤条件,必要时拆成多个可独立检索的子查询。重写结果要随 trace 保存,便于判断是原问题难还是重写跑偏。

OpenAI Retrieval API 提供可选的query rewriting,并在结果中返回改写后的查询;同时支持属性过滤。无论使用托管还是自建实现,过滤应在检索前应用:租户、权限、语言、文档状态、产品版本和有效日期不应等到模型看到片段后再删除。

多跳问题不能只扩大 top-k。例如“功能 A 在新版为何取代旧方案,它对迁移有什么影响”包含版本变化和迁移建议,先分别检索变更说明、当前参考与迁移指南,再合并证据更可控。每个子查询设置候选和时间预算,避免模型不断创造新问题。

4. 召回:稀疏与稠密各自保留

稀疏检索擅长精确 token:错误码、类名、产品编号、罕见人名;稠密检索把文本映射为向量,能匹配同义改写。OpenAI 的Embeddings 指南解释了 embedding 用于搜索、聚类与推荐等任务;Retrieval 指南也展示了语义相近但关键词重叠很少的结果。

稠密召回的研究基础之一是 DPR:它使用双编码器分别表示问题与段落,让语义相近对象在向量空间匹配,原始论文见Dense Passage Retrieval。但生产知识库常有型号、数字和新术语,不能据此删除 BM25。混合检索分别产生候选,再通过 reciprocal rank fusion 或校准分数组合,通常比把所有信号硬压成一个分数更易调试。

OpenAI 当前 Retrieval 文档允许通过 ranking_options.hybrid_search 调整 embedding 与 text 权重,并设置 score threshold。阈值提高会减少低相关候选,也可能损失召回;权重和阈值都必须在自己的查询分布上评测。不要跨模型、跨索引直接比较未经校准的相似度分数。

召回阶段追求“相关证据进入候选集合”,因此候选数可以大于最终上下文数。记录每个通道的 rank、原始分数与融合分数,才能发现专有名词只被稀疏通道找到、语义改写只被稠密通道找到,或某通道持续制造噪声。

5. 重排:用更贵的判断检查更少的候选

双编码器为查询和文档独立编码,适合大规模召回,但查询与每段证据的细粒度交互有限。重排器只处理候选集合,可以用 cross-encoder、LLM rubric 或 late interaction 计算更精确相关性。ColBERTv2 通过 token 级 late interaction 与压缩在效果和索引成本间做取舍,参见ColBERTv2 论文。这说明“重排”不是一种固定模型,而是一类在候选上增加交互的阶段。

重排 rubric 至少区分主题相关和答案支持。一个段落反复提到“退款”,不代表它包含退款期限。对需要引用的问答,排序目标应接近“该片段能否支持问题中的某个原子事实”。多子查询还要保证覆盖:先为每个子问题保留最低配额,再全局排序,避免最热门子问题吞掉所有位置。

LLM 重排具有成本和不稳定性,适合小候选集并配缓存、结构化输出和回归评测。对高流量任务,可先用轻量重排器缩小集合,再在少数困难查询上调用更强判断。所有重排都受截止时间控制;超时应回退到融合排名并在 trace 标记,不应返回空答案。

延迟预算应分配到各阶段,而不是只看端到端平均值。记录 query rewrite、各召回通道、融合、重排、装配和生成的分位耗时,并在请求开始时传递统一 deadline。若剩余时间不足,优先保留已经通过权限过滤的高置信证据并进入简化回答;不要让重排耗尽时间后强迫生成器在空上下文中作答。

6. 上下文装配:top-k 不是复制粘贴

装配器在总 token 预算内选择证据,目标是相关、覆盖、多样、可引用。先去除高度重叠 chunk;合并同文档相邻片段时保持 source span;为不同子问题和来源保留空间;把关键限定条件与例外放在它所修饰的规则附近。每段使用稳定短 ID,例如 [S3],并附标题、版本和原文,而不是让模型从 URL 猜来源。

不要用超长上下文代替检索。Lost in the Middle 研究发现,相关信息在长上下文中的位置变化会影响模型利用效果,相关证据位于中间时性能可能明显下降,见原始论文。因此评测应随机化证据顺序和位置;装配时按任务结构组织,而不是假设更多 token 单调提升质量。

上下文还需要明确“不足”。如果召回不到覆盖关键子问题的片段,传递 coverage_gap 给生成器,要求它指出缺少什么或拒答。不要用模型内部知识补齐后再附一个看似相关的引用。

7. 生成与引用:先建立 claim—evidence 映射

生成器接收问题、答案契约、证据列表和覆盖缺口。要求每个可验证事实引用一个或多个 source ID,只根据提供证据回答,冲突时并列版本,证据不足时明确说明。生成后把答案拆成原子 claim,验证引用片段是否蕴含 claim、所有外部事实是否有引用、引用是否指向允许展示的来源。

引用存在不等于引用正确。ALCE 工作提出了面向带引用生成的基准,并将流畅度、正确性和引用质量分开评估,参见Enabling Large Language Models to Generate Text with Citations。生产系统至少要区分 citation correctness(引用是否支持论断)和 citation completeness(需要证据的论断是否都覆盖)。

引用链接由应用根据 source ID 构造,不允许模型自由生成 URL。页面显示文档标题、版本、片段高亮和访问日期;无权查看全文时,也不能通过引用预览泄露内容。引用验证失败可以删除不受支持的 claim、重新生成一次,或返回“证据不足”,但修复次数必须有限。

长答案可以先生成 claim plan,再逐组分配证据,最后才写自然语言。这样装配器能在生成前发现某个章节没有来源,也能避免同一引用被机械贴到多个无关论断。plan 不是向用户展示的推理过程,而是一份结构化生产工件:claim ID、所需证据类型、source ID 和验证状态。它仍要受总步骤与 token 预算限制。

一份可观察的实现合同

每次查询保存以下非敏感证据:规范化 query 与子查询;过滤条件;每个召回通道的候选 rank;融合和重排分;选入上下文的 chunk 与版本;被预算淘汰的原因;最终 claim—source 映射;答案状态与耗时。正文按隐私策略脱敏或只存哈希。

检索结果可以返回统一结构:

type RetrievalTrace = {
  queryPlan: QueryPlan;
  candidates: CandidateScore[];
  selected: Array<{ chunkId: string; sourceId: string; reason: string }>;
  gaps: string[];
  indexRevision: string;
};

有了这份 trace,答案错误时才能判断是数据缺失、切分破坏语义、查询重写错误、召回漏掉、重排错选、上下文截断,还是生成器无视证据。没有分层 trace,只能不断改 prompt。

失败模式与取舍

典型失败包括:更新文档却保留旧索引;按固定长度切断例外条款;只用向量导致错误码检索失败;ACL 在生成后才过滤;top-k 被近重复 chunk 占满;重排只测主题相关;塞入过长上下文;引用由模型编造;答案有引用但引用不支持论断。

混合检索、重排和引用验证都会增加延迟与工程量。低风险 FAQ 可以使用托管 vector store、简单过滤和少量引用;受监管或高价值知识应保留版本、权限、混合召回、claim 级验证与人工抽检。复杂度应由失败成本和评测提升证明。

结论

真正可回归的 RAG 还要为整条管线生成配置 revision:语料快照、切分器、embedding、稀疏索引、融合权重、重排器、阈值和上下文装配规则缺一不可。评测失败时先重放同一 revision,再只替换一个阶段做对照;否则一次“优化”同时更换索引与 prompt,即使答案变好,也无法知道改进来自哪里,更无法在新语料引入噪声时安全回退。

RAG 不是一个向量数据库功能,而是一条证据供应链。摄取保证版本和权限,结构化切分保留语义,查询计划表达真实问题,稀疏与稠密通道提高召回,重排筛选支持性证据,上下文装配控制预算与位置,生成阶段建立可验证的 claim—source 映射。

当每个答案都能追溯到确切文档版本和原文跨度,每个失败都能定位到管线阶段,RAG 才真正实现“外部知识可更新、结论可核验”,而不是给模型附上一叠可能相关的文字。

Sources