RAG 优化方案
RAG 优化不是“叠加更多算法”,而是先建立可重复的评测集,将失败定位到数据、召回、排序、上下文或生成中的一层,再用最小实验修复该层。
修订说明(核实于 2026-08-13,东八区) 本篇整体严谨度高,多数结论原文已带限定(RRF 的定位、FAQ 混排、Delete-then-Insert 的事务要求、overlap 的 10~20% 实验起点),复核后予以保留。修订集中在三处:
- 🔴 增量 Diff 只比一个哈希集合,会漏掉四类情况:重复块被集合吞掉、文本没变但位置变了、文本没变但权限变了(越权风险)、文本没变但模型换了。已拆成四个正交指纹。
- 🔴 评测集用
relevant_chunk_ids当 Ground Truth,与"实验不同切块策略"直接冲突——切块一改标注就全废。已改为锚定doc_id+ 原文证据span+ 原子事实,再按语料/切块版本自动生成 qrels。- CRAG 的原论文与企业变体混用:Web 搜索正是原论文 Incorrect / Ambiguous 分支的既定动作。换成内部源是合理的工程改写,但应如实称为"企业变体"。
- RSE 补充了"不同实现差异很大"的限定。
阅读提示 本笔记面向已经理解 RAG 基本流程、Embedding、向量检索和 BM25 的学习者,目标不是罗列算法,而是回答:当 RAG 效果不好时,如何定位问题、选择优化手段并验证收益?
目录
- 一、先判断问题发生在哪一层
- 二、先建立评测基线
- 三、文档与切块优化
- 四、查询与召回优化
- 五、重排与上下文构造
- 六、自适应与纠错型 RAG
- 七、GraphRAG 与反馈闭环
- 八、文档更新与增量索引
- 九、FAQ 与文档的分源检索
- 十、推荐的落地顺序
- 十一、优化手段速查表
- 十二、复习重点
核心主线 RAG 优化不是“叠加更多算法”,而是先建立可重复的评测集,将失败定位到数据、召回、排序、上下文或生成中的一层,再用最小实验修复该层。通常应优先解决数据质量、权限过滤、混合检索和重排,最后才考虑 Self-RAG、CRAG、GraphRAG 等复杂编排。
读图顺序是从左到右:先用失败样本定位故障层,再选择对应策略,最后通过离线指标和端到端答案指标判断是否保留改动。没有评测闭环的“优化”,只是增加复杂度。
一、先判断问题发生在哪一层
一个错误答案不一定是大模型“不会回答”。RAG 的错误会沿流水线传播:上游没有找对证据,下游再强的模型也无法可靠生成答案。
| 故障层 | 典型现象 | 优先检查 |
|---|---|---|
| 数据层 | 正确内容根本搜不到,来源过期或重复 | 清洗、版本、解析、元数据、权限 |
| 切块层 | 命中半句话,缺少标题、表头或上下文 | 块边界、块大小、父子块、表格处理 |
| 召回层 | 正确块存在,但没有进入候选集 | Query 改写、BM25、向量模型、过滤条件、Top K |
| 排序层 | 正确块已召回,却排在噪声后面 | Reranker、融合方式、去重 |
| 上下文层 | 证据很多,但相互重复、矛盾或超预算 | 压缩、片段合并、证据排序、token 预算 |
| 生成层 | 证据正确,答案仍编造或漏引来源 | Prompt、引用约束、拒答策略、模型能力 |
⚠️ 只看最终答案,无法定位检索问题 错误操作: 只比较生成答案“像不像正确答案”,不保存召回列表与分数。
实际结果: 无法区分是没召回、排序错误,还是模型没有利用证据;调参只能靠猜。
原因: 最终答案把多个阶段的误差压缩成了一个结果。
正确做法: 为每次请求记录 Query、过滤条件、各路候选、融合分数、重排分数、最终上下文、引用与耗时。
二、先建立评测基线
RAG 评测不能只看最终答案,而要分别回答三个问题:正确证据是否被召回、提交给模型的上下文是否可用、模型是否基于证据正确作答。只有分层评测,才能判断应该调整切块、检索、重排、上下文构造还是生成策略。
用户问题
↓
Retriever → Top K 候选证据 ① 检索质量
↓
上下文构造 ② 上下文质量
↓
LLM → 最终答案与引用 ③ 生成质量
↓
用户任务是否完成 ④ 端到端与系统指标2.1 先构建可追溯的评测集
评测集是基线的基础。每条样本至少包含问题、参考答案或关键事实,以及相关文档或块的稳定 ID:
{
"question": "Milvus 默认端口是多少?",
"reference_answer": "19530",
"relevant_document_ids": ["milvus-install-doc"],
"relevant_chunk_ids": ["chunk-142"],
"tags": ["事实查询", "单跳"]
}样本应覆盖简单事实、多文档与多跳问题、同义表达、错别字、长短 Query、跨块信息、时效冲突、无答案问题和干扰信息。若系统存在权限分级,还必须加入越权检索样本。
⚠️ 用
relevant_chunk_ids当 Ground Truth,会在你调切块参数的那一刻失效 这是个自相矛盾:本篇第三节要求你实验不同的切块策略,而chunk_id是切块的产物。切块参数一改,所有 chunk 重新生成,旧的relevant_chunk_ids全部指向不存在的块——Recall 直接掉到 0,而系统其实一点没变坏。于是你面临两个坏选择:要么不敢动切块参数(评测集锁死了优化空间),要么每换一次参数就重新标注一遍(成本高到没人会做)。
解法:把标注锚定在「不随切块变化的东西」上。 标注时记录:
{ "question": "Milvus 默认端口是多少?", "reference_answer": "19530", "evidence": [{ "doc_id": "milvus-install-doc", // 文档身份,不随切块变 "locator": {"section": "2.1 安装", "page": 7}, // 结构位置 "span": "默认端口为 19530", // ← 原文证据片段,最关键 "atomic_fact": "Milvus 的默认端口是 19530" // 原子事实 }], "tags": ["事实查询", "单跳"] }
span(原文证据片段)是关键:它让你可以为任意一个切块版本自动重新生成 qrels——只要判断"哪些 chunk 的文本包含(或覆盖)这个 span",就得到了这一版的relevant_chunk_ids。标注一次,所有切块实验复用。落地方式:
人工标注(doc_id + span + atomic_fact) ← 只做一次,跨版本复用 ↓ 对每个「语料版本 × 切块版本」自动生成 qrels_v{corpus}_{chunking}.json ← 机器产物,可随时重建然后给每次评测记录三个版本号:语料快照版本、切块配置版本、qrels 版本。缺了这套编号,你无法区分"系统退化"和"评测集换了"——而这两件事在报表上长得一模一样。
可以先从线上失败日志和领域专家常问问题中整理 30~100 条高价值样本,再逐步扩充;“100~500 条”只能作为某些项目早期规模的参考,不能替代对业务覆盖率的判断。评测集、语料快照和标注规则都应有版本号,否则无法区分系统退化与数据集变化。
2.2 检索评测:证据是否被找回并排在前面
假设某个问题有四个相关块 A、B、C、D,系统的 Top 5 为 A、B、X、C、Y,则 Recall@5 = 3/4 = 75%。常用指标如下:
| 指标 | 回答的问题 | 适用情况 |
|---|---|---|
| Hit Rate@K | 前 K 个结果中是否至少命中一个相关项 | 每题只关心“是否命中” |
| Recall@K | 全部相关项中有多少进入前 K | 多证据、多跳问题 |
| Precision@K | 前 K 个结果中有多少真正相关 | 衡量候选噪声 |
| MRR | 第一个相关结果排得多靠前 | 通常只需一个关键答案 |
| MAP | 多个相关项的平均排序精度 | 二元相关、多相关项 |
| nDCG@K | 不同相关等级的结果排序是否合理 | 存在分级相关性标注 |
若相关块排名第 2,则 Hit@1 = 0、Hit@3 = 1。Hit Rate 只判断是否至少命中一个相关项;Recall 则要求知道所有相关项,二者不能混用。
RAG 的第一阶段通常优先保证 Recall,因为正确证据没有进入候选集时,后续模型无法补救。但 Recall 不能脱离 Precision、排序质量、延迟和 token 预算单独优化:单纯增大 K 虽可能提高 Recall,也会引入噪声并抬高重排与生成成本。
2.3 上下文评测:召回结果是否足以作答
检索命中不等于最终上下文可用。正确块可能在扩展、去重、压缩或 token 截断时丢失,也可能被大量相似噪声淹没。上下文层重点检查:
- Context Precision: 提供给模型的上下文中,有多少内容与问题和答案真正相关;
- Context Recall: 参考答案所需的关键信息是否被完整覆盖;
- Context Relevance: 上下文与问题的整体相关程度;
- Context Sufficiency: 仅依靠当前上下文,是否足以推出参考答案。
例如答案需要 A + B + C 三项证据,而上下文只有 A + B,即使召回内容都相关,仍存在上下文召回或充分性不足。上下文不充分时应回查检索和上下文构造;上下文充分但答案错误时,问题更可能位于生成层。
2.4 生成评测:答案是否正确且受到证据支持
假设上下文明确写着“Milvus 默认监听端口为 19530”,模型却回答 19531,说明检索正确但生成错误。生成层应拆开评估:
| 指标 | 判断内容 |
|---|---|
| Answer Correctness | 答案在事实和结论上是否正确 |
| Answer Relevance | 是否直接回答用户的问题 |
| Faithfulness / Groundedness | 答案中的断言是否受到上下文支持 |
| Completeness | 多条件或汇总问题是否遗漏要点 |
| Citation Accuracy / Coverage | 引用是否支持对应断言,关键结论是否都有引用 |
| No-answer Detection | 证据不足时能否正确拒答 |
例如上下文只包含“Redis 默认端口为 6379”,模型又补充了上下文中不存在的开发者信息:即使端口回答正确,额外断言仍会降低忠实度。所谓“幻觉率”也必须先定义断言切分、支持关系和聚合方式,不能把它当成无需说明即可横向比较的统一指标。
2.5 系统指标与基线对比
质量提升必须同时满足工程约束。系统层至少记录:
- P50、P95,必要时记录 P99 延迟;
- 检索、重排和生成各阶段耗时;
- Embedding 与 LLM 的 token 或调用成本;
- 超时、失败和降级率;
- 同一配置重复执行时的稳定性。
建立基线时,应固定评测集版本、语料快照、切块策略、Embedding 模型、检索方式、Top K、Reranker、Prompt、生成模型和随机参数。每次实验只修改一个主要变量,并保存完整配置差异。
| 实验版本 | Recall@5 | MRR | Faithfulness | Correctness | P95 |
|---|---|---|---|---|---|
| Baseline | 【实测值】 | 【实测值】 | 【实测值】 | 【实测值】 | 【实测值】 |
| + Hybrid Search | 【实测值】 | 【实测值】 | 【实测值】 | 【实测值】 | 【实测值】 |
| + Reranker | 【实测值】 | 【实测值】 | 【实测值】 | 【实测值】 | 【实测值】 |
表中的数值必须来自同一评测集和语料快照。若判官模型或判官 Prompt 发生变化,应重新计算所有对照实验,不能把不同“尺子”产生的分数直接比较。
2.6 工具选择与推荐实践顺序
Ragas、DeepEval、LangSmith、TruLens 和 LlamaIndex Evaluation 都能帮助组织部分评测流程,但框架不能替代业务标注。生产项目通常组合使用:
人工标注的 Ground Truth
+
传统信息检索指标
+
版本化的 LLM-as-a-Judge
+
线上业务与系统指标推荐的实践顺序是:
- 构建并版本化评测集与语料快照;
- 先计算
Recall@K、MRR 等检索指标; - 再评估上下文充分性、答案正确性和忠实度;
- 保存 Baseline 及完整系统配置;
- 每次只修改一个主要变量,例如
chunk_size、Embedding、Top K 或 Reranker; - 在相同数据与度量版本上重新执行;
- 同时比较质量、延迟、成本与稳定性,决定是否保留改动。
评测基线的核心结论 RAG 评测必须拆开检索、上下文和生成阶段。若
Recall@K已经很低,应先修复数据、切块、Embedding、检索或重排,而不是持续修改 Prompt。
三、文档与切块优化
3.1 先修数据,再调检索
入库前应去掉页眉页脚、导航、重复版本和解析乱码,同时保留标题层级、表头、页码、来源、更新时间、访问范围等元数据。扫描 PDF、复杂表格和多栏排版应单独抽检,不能只确认“成功生成了文本”。
元数据至少应支持:
source_id / document_id / chunk_id
title / section / page
updated_at / version
tenant_id / access_scope权限过滤必须在检索阶段执行,不能先召回越权内容,再依赖 Prompt 要求模型忽略。
3.2 切块策略与粒度选择
切块的目标不是追求统一长度,而是让每个块既能独立表达一个相对完整的语义单元,又不会因内容过多而稀释检索信号。常见策略如下:
- 递归字符切分: 按段落、句子、词等分隔符逐级回退,是最通用的基线。处理中文时,应显式加入
。!?;、换行和空行等分隔符。 - 按文档结构切分: Markdown 按标题层级,代码按函数或类,HTML 按 DOM 节点,PDF 尽量按章节和版面结构。作者已经划定的语义边界通常比固定字符长度更可靠。
- 语义切分(Semantic Chunking): 计算相邻句子的 Embedding 相似度,在语义变化明显的位置断开。它可能获得更自然的边界,但计算和调参成本较高,更适合高价值语料或结构不明显的长文本。
- 重叠窗口(Chunk Overlap): 在相邻块之间保留少量重复内容,用于缓解关键信息被边界截断。可将块长的 10%~20% 作为实验起点,而不是固定标准;重叠过大会造成索引膨胀、重复召回和 token 浪费。
块大小和重叠比例应通过评测集决定。定义、FAQ 等短事实适合较小块;制度、流程和跨段论证通常需要更完整的上下文。若一种粒度无法兼顾召回精度与回答完整性,应优先考虑父子块,而不是不断增大单块长度。
3.3 父子块:小块负责找,大块负责答
父子块(Parent-Child Retrieval)将同一份内容保存为两种粒度:
子块:较短、语义集中,用于 Embedding 与召回
父块:包含完整段落或章节,用于提交给 LLM它解决了一个根本矛盾:小块更容易精确匹配,但大块更容易保留完整语境。实现时,子块必须记录 parent_id,命中后回查父块,并对相同父块去重。
3.4 连续块扩展与 RSE
连续块扩展是在命中块后补充相邻块,例如取 i-1、i、i+1。它适合跨段定义、步骤说明和表格上下文,但不应无条件扩展,否则容易把无关内容塞进上下文。
RSE(Relevant Segment Extraction,可理解为“相关连续片段提取”)比固定邻居更灵活:先给命中块打分,再根据文档顺序合并相邻高价值块,并用长度惩罚控制片段大小。核心目标是返回一个连续且证据密集的段落,而不是零散 Top K。
⚠️ RSE 不是一个有统一规范的标准算法——不同实现的打分函数、合并条件和长度惩罚差别很大(这里描述的思路接近 dsRAG 的实现)。所以引用 RSE 时要说明"用的是哪一家的哪个版本",评测结果也只在该实现下成立,不能跨实现比较。
⚠️ overlap 不能替代连续片段恢复 错误操作: 不论文档结构,持续增大
chunk_overlap。实际结果: 索引体积、重复召回和 token 消耗上升,但跨块问题仍可能缺少完整上下文。
原因: overlap 是入库阶段的固定重复;相邻块扩展和 RSE 是查询阶段按相关性恢复上下文,两者解决问题的时机不同。
正确做法: 先按标题和语义边界切分,只保留适量 overlap;确有跨块证据时,再对命中结果做动态扩展。
3.5 标题、摘要与假设问题增强
可以为每个块生成标题、摘要或若干“这个块能回答的问题”,再将增强文本用于检索。它能缩小用户问法与原文写法之间的语义差距。
实践中最好分别保存:
retrieval_text = 标题 + 摘要/假设问题 + 原文
answer_text = 原始正文检索时使用增强文本,生成时优先提供原始正文,避免让 LLM 生成的摘要或问题被误当成正式证据。增强内容还应记录生成模型和版本,以便重建索引。
3.6 层次索引
层次索引先检索文档或章节摘要,再在命中的范围内检索细粒度块:
Query → 章节/文档摘要 Top K → 限定候选范围 → 细粒度 Chunk Top K它适合长报告、多章节手册和主题边界清楚的语料。若摘要质量差或第一层漏召回,第二层将永远看不到正确块,因此第一层应追求较高召回,并允许保留多个候选分支。
四、查询与召回优化
4.1 查询重写、回溯提示与子查询分解
- 查询重写(Query Rewriting):补全多轮对话中的省略和指代,使问题可独立检索;
- 回溯提示(Step-back Prompting):把过窄问题提升为更一般的概念问题,用于补充背景知识;
- 子查询分解(Query Decomposition):把多跳或多条件问题拆成多个可检索问题,分别取证后合并。
例如:
原问题:对比 A 和 B 的退款条件,并说明逾期后怎么办
子查询 1:A 的退款条件是什么?
子查询 2:B 的退款条件是什么?
子查询 3:超过退款期限后的处理规则是什么?重写后的 Query 必须保留用户限定条件,如产品、时间、地区和角色。可同时保留原 Query 与改写 Query 检索,再融合结果,降低改写偏航的风险。
4.2 HyDE:用假设文档连接问题与语料
HyDE = Hypothetical Document Embeddings(假设文档嵌入)。它先让 LLM 生成一段“可能的答案文档”,再对该文档做 Embedding,并用它寻找真实文档。
用户问题 → LLM 生成假设文档 → Embedding → 检索真实文档 → 基于真实证据回答HyDE 的假设文档只是检索桥梁,不能作为答案证据。它在零样本、抽象问题或 Query 与文档语言风格差异大时可能有效,但会增加一次模型调用,并可能因假设内容偏航而降低召回,因此必须在本地评测集上验证。HyDE 论文
4.3 混合检索:语义召回与精确匹配互补
向量检索擅长同义表达和概念相似,BM25 擅长错误码、型号、人名、缩写和精确术语。生产系统常并行执行两路检索,再通过 RRF 融合。
RRF = Reciprocal Rank Fusion(倒数排名融合)。它主要使用名次而不是直接比较不同检索器的原始分数:
[ RRF(d)=\sum_{r \in R}\frac{1}{c+rank_r(d)} ]
其中 rank_r(d) 是文档 d 在检索器 r 中的排名,c 是减弱头部名次差异的常数。
def rrf(rankings: list[list[str]], c: int = 60) -> dict[str, float]:
scores: dict[str, float] = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1 / (c + rank)
return scores
scores = rrf([
["doc-a", "doc-b", "doc-c"], # 向量检索
["doc-b", "doc-d", "doc-a"], # BM25
])
print(max(scores, key=scores.get)) # doc-b为什么不直接相加相似度 BM25 分数、余弦相似度和其他检索器分数通常不在同一量纲,直接加权相加需要校准。RRF 使用排名,简单且稳健;如果已有标注数据,再考虑学习融合权重。
五、重排与上下文构造
5.1 两阶段检索:先广召回,再精排序
常见流程是:
BM25 / 向量检索召回 20~100 个候选
→ 融合与去重
→ Cross-Encoder 或 LLM Rerank
→ 保留 3~10 个证据块Cross-Encoder 将 Query 与候选块成对输入模型,通常比单独编码后的向量相似度更精确,但计算量随候选数增加。LLM Rerank 能处理复杂标准,但成本、延迟和输出稳定性通常更差,适合小候选集或高价值场景。
候选数量的选择 第一阶段的 K 过小会漏召回,过大则会拖慢重排并增加噪声。应同时观察
Recall@K、重排后的nDCG@K、延迟与成本,而不是只追求更大的 K。
5.2 上下文压缩
上下文压缩从候选块中提取与 Query 直接相关的句子,减少无关 token。它必须保留来源、否定词、适用条件、时间和主语,不能把“不得退款”压缩成“退款”。
可靠做法是同时保存:
- 压缩片段;
- 原始块 ID 与字符区间;
- 原始块全文;
- 压缩模型和版本。
高风险业务中,引用展示应能回到原文,而不是只展示 LLM 改写后的摘要。
5.3 上下文不是越多越好
上下文构造还要处理去重、冲突、时效和顺序:优先放置高相关、权威、较新且互补的证据;对同一事实的重复块去重;对冲突版本明确标记时间或版本,而不是让模型自行猜测。
六、自适应与纠错型 RAG
6.1 CRAG:检索失败后采取纠错动作
CRAG = Corrective Retrieval-Augmented Generation(纠错式检索增强生成)。其核心不是简单“给结果打分”,而是通过检索评估器判断证据质量,再触发不同动作:可信时直接使用,不确定时补充或修正,明显不可信时放弃该结果并改用其他知识源。原论文还使用 Web 检索扩展静态语料,并对文档做分解、过滤和重组。CRAG 论文
检索结果 → 相关性评估
├─ 高置信:直接构造上下文
├─ 不确定:改写 Query / 再检索 / 补充外部来源
└─ 低置信:拒绝使用并回答“证据不足”说清楚哪部分是原论文,哪部分是企业变体 上面这张分支图是企业环境下的改写版,不是 CRAG 原论文的流程,两者别混着引用。
原论文的做法: 检索评估器给出三个标签。判为 Correct 时对文档做分解—过滤—重组、只保留关键片段;判为 Incorrect 时丢弃全部检索结果,改用 Web 搜索获取新知识;判为 Ambiguous 时两者结合。也就是说,Web 搜索在原设计里正是 Incorrect / Ambiguous 两个分支的既定动作——它是论文用来"扩展静态语料局限"的核心手段,不是可选装饰。
企业变体的做法: 内网系统通常不能、也不该在证据不足时去公网抓内容(数据边界、合规、来源不可控、时效与引用无法保证)。所以把 Web 那一格替换成:其他受信任的内部源(工单系统、CRM、另一个知识库、结构化数据库),或者干脆拒答并转人工。
这个替换是合理的,但要如实命名: 应当称为「借鉴 CRAG 思路的企业变体」,而不是说「Web 搜索不是 CRAG 的固定动作」——后者是对原论文的误述。写方案、做汇报、对外分享时,这个区分很重要:你改了论文的关键环节,就该说明改了什么、为什么改,否则别人按论文的结论预期你的效果,会对不上。
无论换成哪个源,对引入的外部内容都必须做安全、时效和引用校验。
6.2 Self-RAG 不等于“多写一个反思 Prompt”
Self-RAG = Self-Reflective Retrieval-Augmented Generation(自反思式 RAG)。原始方法训练模型生成特殊的反思 token,从而在推理时判断是否需要检索、证据是否相关、回答是否受到证据支持;它不是普通 RAG 后追加一句“请反思”的同义词。Self-RAG 论文
工程上可以借鉴其控制逻辑,构建“是否检索—证据评价—是否再检索—答案核验”的状态机,但应称为自适应 RAG 或反思式工作流,避免把 Prompt 编排直接等同于论文中的训练方法。
⚠️ 用同一个 LLM 同时回答和自我裁判 错误操作: 让同一模型生成答案后,仅凭一句反思 Prompt 宣布答案可靠。
实际结果: 模型可能重复原有偏见,给错误答案一个看似合理的解释。
原因: 自评不是独立证据,且同一上下文和模型容易产生相关性错误。
正确做法: 优先使用可计算的检索指标、引用对齐、规则校验或独立评估模型;重要结论仍需回到原始证据。
七、GraphRAG 与反馈闭环
7.1 GraphRAG 适合关系型、多跳型问题
GraphRAG 将实体、事件和关系组织为图,并结合图遍历、社区摘要或向量检索回答跨文档问题。它适合:
- “A 通过哪些公司与 B 有关联?”这类多跳关系问题;
- 实体别名多、关系链比单段语义相似更重要的语料;
- 需要全局主题总结的大型文档集合。
它不天然优于普通 RAG。构图需要实体消歧、关系抽取、更新和溯源;若问题主要是政策条款或 FAQ 精确命中,混合检索加重排通常更简单。
关系数据库保存正式业务事实时,AI 抽取的知识图谱应视为可重建派生数据,不能反向覆盖事实源。
7.2 Feedback Loop:从日志学习,但不直接在线改权重
反馈闭环可以收集点赞/点踩、人工纠错、点击引用、改写问题、失败类型和最终解决状态,将其转化为:
- 新评测样本;
- Hard Negative(难负样本);
- Query—Document 相关性标注;
- 切块和过滤规则修订;
- 检索器、Reranker 或融合权重的离线训练数据。
用户反馈并不等于客观真值。上线前应经过清洗、权限隔离、离线评测和灰度验证,不应让单次反馈直接修改全局检索权重。
八、文档更新与增量索引
文档更新的前提是让索引中的内容可寻址、可比较、可替换。如果入库时没有稳定标识,一处小修改也可能迫使系统重建整个知识库。每个块至少应保留以下元数据:
{
"doc_id": "manual",
"chunk_id": "manual#section-2#0007",
"content_hash": "a3f5...",
"version": 3,
"updated_at": "2026-08-06"
}doc_id:稳定的文档级标识,不应随文件名或版本号变化;chunk_id:块级标识,最好根据稳定的章节路径和块序号确定性生成;content_hash:用于判断实际参与检索的内容是否发生变化;version与updated_at:用于版本追踪、冲突处理和时效排序。
8.1 全量替换:简单可靠的默认方案
全量替换(Delete-then-Insert)是按 doc_id 删除旧块,再对最新版文档重新解析、切分和入库。它实现简单、状态清晰,文档规模较小或更新不频繁时通常应作为默认方案。
它的代价是:即使只修改一个错别字,也会重新计算整篇文档的 Embedding。删除与写入最好放在事务、临时索引或版本切换流程中,避免更新过程中出现短暂的无数据状态。
8.2 增量 Diff:只重算发生变化的块
增量更新会重新切分新版文档,计算各块的 content_hash,再与索引中的旧集合比较:
新 hash 不在旧集合 → 新增并计算 Embedding
旧 hash 不在新集合 → 删除对应旧块
新旧集合均存在 → 保留,不重复计算 Embedding这样,一处局部修改通常只需重算受影响的少量块。不过,content_hash 必须基于最终用于检索的文本计算。如果索引文本包含标题、上下文前缀、摘要或 Contextual Retrieval 增强内容,那么这些内容也必须参与哈希;否则章节标题已经变化,系统却可能误判下游块无需更新。
⚠️ 只比较「一个哈希集合」会漏掉四类情况,其中一类是安全事故 上面那三行伪代码把
content_hash当成了唯一判据,并且用集合来比较。两个假设都不够。① 集合会把重复块吞掉。 同一段文本在文档里出现多次(页眉、免责声明、重复引用的条款),
set只保留一个,数量和位置信息丢失。「新旧集合均存在」于是无法区分「出现 1 次」和「出现 3 次」,删除逻辑会误删。② 文本没变,但位置变了。 一段话从第 2 章移到第 7 章,
content_hash完全相同,于是被判为"无需更新"。可它的页码、父标题、章节路径、相邻块关系全变了——这些元数据既用于溯源展示,也用于父子块扩展和过滤,不更新就是错的。③ 文本没变,但权限 / 状态变了。(这一类最危险)ACL 标签、租户归属、有效期、审核状态、密级——都不在正文里。一份文档被收紧权限后,增量任务因为"内容没变"跳过了它,无权限的用户继续能检索到。 这是越权,不是数据陈旧。
④ 文本没变,但向量的生成方式变了。 换 Embedding 模型、模型升了小版本、改归一化、加/去掉
query:前缀、调整预处理——旧向量不能复用,尽管文本一字未改。解法:把单一哈希拆成四个正交指纹,各自决定各自的动作。
指纹 覆盖 变化时的动作 retrieval_text_hash最终参与检索的文本(含增强前缀) 重新切块 + 重新向量化 metadata_hash页码、章节路径、相邻关系、时效 只更新标量字段,向量可复用 acl_hash权限标签、租户、密级、审核状态 只更新权限字段,必须立即生效,不可延后 embedding_fingerprint模型名 + revision + 维度 + 归一化 + 前缀 + 预处理版本 全量重新向量化 核心是把两个决策分开:「这条记录要不要更新」和「旧向量能不能复用」。它们经常给出不同的答案——大量场景下向量可以原样保留,但记录必须更新。把两者绑在一个哈希上,就必然在某一侧犯错。
另外,用有序列表 + 位置信息代替集合来做 diff(如
(offset, retrieval_text_hash)的列表),才能正确处理重复块。ID 设计的完整讨论见 RAG架构设计 第五节——那里还有一条更基础的提醒:ID 变了是新增不是覆盖,差集删除不能省。
⚠️ 固定长度切分会放大增量更新范围 错误操作: 使用固定长度切分,并把块序号或整块文本作为唯一的变更依据。
实际结果: 文档中部插入少量内容后,后续块的边界整体平移,大量哈希随之失效,增量 Diff 退化成近似全量重建。
原因: 固定长度边界与文档语义结构无关,前面的字符变化会级联影响后续所有块。
正确做法: 优先按标题、段落等稳定结构切分,让修改影响局限在局部章节;同时保留
doc_id、章节路径和版本信息,便于安全删除与回滚。
无论采用哪种更新方式,都应先构建新版本并通过抽样检索或回归评测,再切换线上版本;旧版本保留到验证完成后,才能避免解析失败或索引异常直接污染生产知识库。
九、FAQ 与文档的分源检索
FAQ 问答对与原始文档承担的职责不同:FAQ 提供经过审核、可直接复用的标准答案,文档块负责覆盖更广的知识和完整依据。推荐采用分源存储、统一编排的思路:物理上可以使用两个 Collection,也可以在同一 Collection 中通过 source_type 等字段严格隔离;逻辑上分别召回,再根据置信度决定直接返回或合并重排。
9.1 为什么不应直接混排
将 FAQ 与文档块放进同一路候选并直接比较向量分数,容易产生三类问题:
- 文本形态不同: FAQ 问题通常短而聚焦,文档块更长、包含更多上下文。长度和信息密度差异会影响相似度分布,但不能简单断言短文本一定更相关。
- 匹配任务不同: Query 与 FAQ 问题属于“问—问”匹配,Query 与文档块更接近“问—答案或证据”匹配;两路检索的分数分布未必可直接比较。
- 业务可信度不同: 人工审核的标准答案与自动切分的原文片段具有不同的权威等级。优先级应由业务规则、版本和审核状态决定,不能只交给 Embedding 相似度。
因此,关键不是一定使用两个数据库,而是确保两类数据可以独立配置索引、召回数量、阈值、权限、版本和评测指标。
9.2 两路召回与分层决策
Query
├─→ FAQ 检索
│ ├─ 高置信且满足短路条件 → 直接返回已审核答案
│ └─ 置信度不足 → 作为候选证据
└─→ 文档检索 → 文档候选块
↓
跨源合并、去重与 Rerank
↓
LLM 基于证据生成FAQ 短路能减少 token、延迟和生成不确定性,但不能仅凭一个未经校准的余弦相似度阈值执行。短路条件至少应同时检查:FAQ 是否处于已审核和有效版本、用户权限是否满足、Top 1 与后续结果是否有足够分差,以及该阈值在独立评测集上的误匹配率。具体阈值应按 Embedding 模型和业务数据校准。
对于政策解释、时间敏感内容、多条件问题或需要引用原文的场景,即使 FAQ 命中,也应补充检索文档并展示来源,避免标准答案过期或遗漏适用条件。
9.3 FAQ 的索引方式
FAQ 通常使用“问题”或用户可能采用的问法参与检索,答案作为返回内容和审核对象保存。若同一答案对应多种常见问法,可建立“一答多问”结构:
faq_answer_id: refund-policy-001
├─ question: 如何申请退款?
├─ question: 退款入口在哪里?
└─ question: 我想退费怎么办?每种问法可以拥有独立向量,但都指向同一个已审核答案 ID。这样既提高问法覆盖率,也避免复制多份答案后出现版本不一致。FAQ 记录还应保存 status、version、updated_at、适用范围和审核人等元数据。
跨源合并时,不应直接相加两路原始相似度。可以先在各自召回通道内确定名次,再使用 RRF、经过标注数据校准的权重,或 Cross-Encoder 统一重排。若 FAQ 具有明确的业务优先级,还应在 Prompt 或编排规则中标注“标准答案优先、文档证据用于补充和校验”。
十、推荐的落地顺序
- 建立基线: 固定评测集,记录检索结果、答案、引用、延迟和成本。
- 修复数据: 解析、去重、版本、稳定标识、元数据、权限和可追溯性。
- 调切块: 先结构化切块,再按失败样本尝试父子块或相邻块扩展。
- 增强召回: 元数据过滤 + BM25/向量混合检索 + RRF。
- 提高精度: 增加 Reranker、去重和上下文预算控制。
- 处理复杂 Query: 再加入对话补全、查询重写和子查询分解。
- 按场景试验高级方法: HyDE、层次索引、RSE、CRAG、自适应工作流、GraphRAG。
- 完善更新机制: 根据规模选择全量替换或增量 Diff,并支持版本验证与回滚。
- 拆分异构知识源: FAQ 与文档分别评测和召回,校准短路条件与跨源融合策略。
- 建立反馈闭环: 将线上失败转为新评测样本,持续做回归测试。
每一步都应满足:离线指标有改善、端到端答案不退化、延迟和成本在预算内、权限边界不改变。
十一、优化手段速查表
| 失败现象 | 优先方案 | 主要代价/风险 |
|---|---|---|
| 命中块太碎、缺上下文 | 父子块、相邻块扩展、RSE | token 增加、重复内容 |
| 用户问法和原文差异大 | 查询重写、标题/摘要/假设问题增强 | LLM 偏航、索引重建 |
| 型号、错误码、专名漏召回 | BM25 + 向量混合检索 | 两路索引与融合复杂度 |
| 正确块进入候选但排名低 | Cross-Encoder Rerank | 延迟与算力 |
| 多条件、多跳问题漏信息 | 子查询分解 | 多次检索、结果合并错误 |
| 长文档跨章节定位困难 | 层次索引 | 上层漏召回会截断下层 |
| 抽象 Query 难以匹配 | HyDE | 额外调用、假设偏航 |
| 上下文噪声过多 | 压缩、去重、token 预算 | 关键限定条件被删 |
| 检索质量波动大 | CRAG / 自适应再检索 | 状态机、评估器与成本增加 |
| 跨文档关系问题 | GraphRAG | 构图、消歧、更新成本高 |
| 文档更新导致频繁全量重建 | 稳定 ID + 内容哈希 + 增量 Diff | 标识设计、版本切换与一致性维护 |
| FAQ 与文档块互相挤占排名 | 分源召回 + 短路规则 + 跨源 Rerank | 阈值误判、答案过期、编排复杂度 |
| 不知道改动是否有效 | 分层评测 + 反馈闭环 | 标注与维护成本 |
十二、复习重点
可以直接复述的结论
- RAG 优化的第一步是建立评测基线和可观测链路,不是添加新算法。
- 先区分数据、切块、召回、排序、上下文和生成故障,再对症修改。
- 小块利于精确召回,大块利于完整回答;父子块用两种粒度化解矛盾。
- BM25 与向量检索互补,RRF 用排名融合异构分数,Reranker 再对候选精排。
- HyDE 的假设答案只用于检索,不能当证据;Self-RAG 也不等于普通的反思 Prompt。
- GraphRAG 适合多跳关系和全局主题,不是所有知识库的默认升级路线。
- 文档更新依赖稳定的
doc_id、chunk_id和内容哈希;结构化切分还能限制增量更新的影响范围。- FAQ 与文档应分源召回、统一编排;FAQ 短路阈值必须基于业务评测校准,不能照搬经验值。
- 任何优化都要同时检查质量、引用、权限、延迟和成本,并通过回归评测防止局部提升造成整体退化。