"prompt 改到第七版还是不准确",是 RAG 项目里最典型的岔路。此时继续调提示词,等于在最后一层修前面四层的错。RAG 的误差沿着切分 → 向量化 → 检索 → 拼装 → 生成逐层放大,而排错的顺序必须反过来:先证明文档没被切坏,再证明召回里有正确片段,最后才谈生成。跳过前两步换检索方案,通常只是把问题挪了个位置。
1. 五层诊断:先定位,再换方案
L1 切分 同一段答案是否完整落在单个 chunk 内? 验证:抽 20 个问题,肉眼对齐原文
L2 向量化 该 chunk 是否与其他 chunk 明显可分? 验证:算问题与目标 chunk 的相似度排名
L3 检索 目标 chunk 是否进了 Top-K? 验证:只看召回,不看生成
L4 拼装 进入 prompt 的片段是否被截断/去重/排序错? 验证:打印最终 prompt 原文
L5 生成 有正确片段时模型是否用得对? 验证:把答案片段直接贴进 prompt 重跑
| 层 | 失效信号 | 修法 | 换检索方案能解决吗 |
|---|---|---|---|
| 切分 | 答案跨 chunk 断裂、表格被切散 | 改切分粒度/按结构切 | 不能 |
| 向量化 | 目标片段排名在 50 名开外 | 换 embedding、加关键词路 | 部分能 |
| 检索 | Top-K 内没有、过滤条件写错 | 混合检索、rerank、改过滤 | 能 |
| 拼装 | 片段被 token 上限截掉 | 重排、压缩、去重 | 不能 |
| 生成 | 片段正确但答案跑偏 | 改指令、约束引用 | 不能 |
硬规矩:打印出真实 prompt 之前,不改任何检索参数。多数"检索不准"是拼装阶段把最相关片段截掉了。
2. 两种检索方式各自的失效面
| 查询类型 | 纯向量 | 纯关键词 |
|---|---|---|
| 精确标识(订单号、错误码、SKU) | 差,语义近但字面不对 | 强 |
| 长尾实体(生僻人名、内部代号) | 差,训练分布里没有 | 中,需分词正确 |
| 同义表达("退款" vs "退回款项") | 强 | 差 |
| 跨语言 | 强(多语模型) | 差 |
| 否定语义("不含酒精的") | 弱,常召回相反含义 | 中,取决于分词与停用词 |
| 短查询(2–3 字) | 弱,向量信息不足 | 中 |
收益取决于查询分布里精确项的比例:全是自然语言长问句时,加 BM25 只增加复杂度;编号、型号、代码密集的库,不加就是系统性漏召回。
3. 融合方式与它的成本
# RRF:只用排名,不用分数,避免两路分数量纲不可比
def rrf(rank_lists, k=60):
score = {}
for ranks in rank_lists:
for pos, doc_id in enumerate(ranks, start=1):
score[doc_id] = score.get(doc_id, 0.0) + 1.0 / (k + pos)
return sorted(score.items(), key=lambda x: -x[1])
| 融合方式 | 优点 | 代价 |
|---|---|---|
| 分数归一后加权 | 可调权重、能表达偏好 | 两路分数量纲不同,min-max 归一受异常值影响 |
| RRF(排名倒数) | 无需标定、稳健 | 丢掉分数信息,头部差异被抹平 |
| 学习式融合 | 上限最高 | 需要标注集与持续训练 |
RRF 的 k=60 是起点不是最优值,它决定头部排名有多重要;抄走常量从不验证,是最常见的假配置。
4. rerank:只在"召回有、排序错"时有用
| 现状 | rerank 的作用 | 判断方法 |
|---|---|---|
| 目标片段在 Top-50 内,但不在 Top-5 | 收益最大 | 算 Top-50 命中率 |
| 目标片段根本不在 Top-50 | 无用,先修召回 | 同样算命中率 |
| Top-5 已命中 | 无用,纯增延迟 | 抽样看现状 |
| 长文档、语义细微差别 | 收益明显 | 对比 rerank 前后 NDCG |
rerank 是排序器,不是召回器。上线前先量 Top-50 命中率:它低于目标时,rerank 只是把错误排得更整齐,代价是多一次模型前向的延迟。
5. 分块策略由内容类型决定
| 内容类型 | 推荐切法 | 常见错误 |
|---|---|---|
| FAQ / 问答对 | 一问一块,问题作元数据 | 按固定字数切,把问答拆开 |
| 长文档/规范 | 按标题层级切,保留父标题路径 | 用固定窗口,章节语义断裂 |
| 表格 | 整表一块 + 行级摘要 | 按字符切,表头丢失 |
| 代码 | 按函数/类切,带文件路径 | 按行数切,函数体被劈开 |
| 会议记录 | 按议题切,带时间与发言人 | 全文一块,检索粒度太粗 |
chunk = {
"text": text,
"title_path": ["退款政策", "跨境订单"], # 父级路径:拼装时可补上下文
"type": "faq",
"source_id": "doc-1042",
"updated_at": "2026-03-01",
}
块大小是一对反向成本:太小语义不完整,太大向量被稀释。判据是答案的平均长度——块应略大于一个典型答案,而不是等于段落机械长度。
6. 多路召回与查询改写
| 手段 | 解决的问题 | 引入的风险 |
|---|---|---|
| 多路召回(向量 + BM25 + 结构化) | 不同查询类型互补 | 融合权重难调、成本上升 |
| 查询改写(扩展同义词) | 用词不一致 | 改写跑偏,召回无关内容 |
| HyDE(先生成假答案再检索) | 短查询语义不足 | 假答案带偏方向,延迟翻倍 |
| 子问题拆解 | 复合问题 | 拆错则全错,合并易漏 |
改写与 HyDE 都属于"用一个模型输出喂给检索",它们的失效模式和生成阶段一样:看不出自己错。必须留一条"不改写"基线,否则分不清收益与评估噪声。
7. 图结构检索的代价
| 维度 | 向量检索 | 图结构检索 |
|---|---|---|
| 构建 | 一次性 embedding | 抽取实体关系 + 建图,需人工校验 |
| 更新 | 增量 upsert | 实体消歧、边失效、社区重算 |
| 查询 | 一次近邻搜索 | 多跳遍历,复杂度随跳数上升 |
| 擅长 | 语义相似、模糊匹配 | 多跳关系、全局归纳类问题 |
图的判据很窄:问题需跨文档做关系推理,且实体边界清晰可抽。"文档很多"不构成理由——硬抽无结构文本得到噪声图,查询还更贵。
8. 权限过滤必须在检索之前
# 正确:过滤条件下推到检索,候选集本身就不含越权内容
results = index.search(query_vec, top_k=20, filter={"tenant_id": tid, "acl": {"$in": roles}})
# 错误:先检索再过滤,Top-K 被无关内容占满
results = [d for d in index.search(query_vec, top_k=20) if can_read(user, d)]
| 位置 | 后果 |
|---|---|
| 检索前置(元数据过滤) | 正确,且不浪费召回名额 |
| 生成后过滤 | 越权内容已进 prompt,等于已泄露 |
| 检索后过滤 | 名义安全,实际 Top-K 被挤占,召回质量下降 |
这是硬约束:内容一旦进过上下文,就无法主张没被看到。多租户索引必须让租户维度成为过滤字段,而不是事后筛。
9. 成本结构
| 项 | 触发时机 | 量级感受 |
|---|---|---|
| 索引构建 | 首次全量、模型升级、切分变更 | 一次性,但与文档量成正比 |
| 向量存储 | 常驻 | 与向量维度、副本数成正比 |
| 每次查询 | 每请求 | 嵌入 + 检索 + (可选)rerank + 生成 |
| 重排与改写 | 每请求,可关 | 直接加延迟与调用次数 |
成本杠杆不在向量库选型,而在每次查询调用几次模型:rerank 与改写都开,单次查询可能从一次变三次,高频场景里这就是预算表第一行。
10. 评估:检索与答案分开评
检索侧 Recall@K / MRR / NDCG 需要"哪个 chunk 是正确的"标注
答案侧 正确性 / 引用一致性 / 拒答 需要参考答案或无答案样本
| 样本 | 占比 | 作用 |
|---|---|---|
| 主路径问题 | 多数 | 守住基本盘 |
| 长尾实体 | 一部分 | 暴露关键词路缺失 |
| 无答案问题 | 必须有 | 检验是否会编 |
| 跨文档多跳 | 少量 | 检验图/多路召回是否值得 |
没有无答案样本,系统就永远在"必须给个答案"。拒答率异常低通常不是能力强,是评测集缺了这类。
11. 增量更新与双索引切换
index:
alias_read: rag-live # 查询永远读别名,不读具体索引
build: rag-build-20260301 # 重建写入新索引
swap: # 校验通过后原子切换
- step: verify_count # 文档数与预期一致
- step: verify_sample # 抽样比对旧索引命中
- step: alias_swap # 原子切换读别名
- step: keep_old_7d # 保留旧索引以便回滚
| 变更类型 | 重建范围 |
|---|---|
| 新增/修改文档 | 只重算受影响 chunk |
| 切分策略变更 | 全量重建 |
| embedding 模型更换 | 全量重建,且必须双索引并行 |
| 元数据/权限变更 | 只更新 payload,不重算向量 |
最常见工程债是"改切分直接原地覆盖":索引不可回滚,切分实验就得赌线上效果。别名 + 双索引成本很小,换来的是回滚能力。
12. 排错表
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 答案缺关键数字 | 表格被切散、表头丢失 | 整表一块,行级摘要 |
| 明明有文档却答"未提及" | 目标 chunk 未进 Top-K | 算 Recall@50,补 BM25 路 |
| 引用了不存在的段落 | 生成层编造 | 强制引用 ID,拼装时校验 |
| 换了 embedding 后效果下降 | 旧索引未重建,向量空间混用 | 全量重建并双索引切换 |
| 权限不同的用户看到同一答案 | 过滤在生成后 | 过滤前置到检索 |
| rerank 上线后延迟翻倍、准确率不变 | Top-5 本已命中 | 抽样验证后再决定是否保留 |
13. 边界
| 情形 | 判断 | 理由 |
|---|---|---|
| 知识量很小(几十条) | 全量塞进上下文 | 检索的复杂度换不来收益 |
| 查询模式单一(固定几类) | 规则路由 + 少量检索 | 用不上通用检索栈 |
| 强实时(秒级数据变化) | 检索 + 实时接口混用 | 索引永远追不上写入 |
| 内容高频变动 | 缩短链路、接受短窗口滞后 | 重建成本会吃掉收益 |
| 答案需精确计算 | 走工具/查询,不走检索 | 检索给的是文本,不是结果 |
RAG 擅长"找到说法",不擅长"算出数值":聚合计算类问答塞进检索链路,换任何方案都不会准。
14. 自测
- 你能打印出线上真实进入 prompt 的完整片段吗?截断发生在哪一步?
- 目标 chunk 在 Top-50 里的命中率是多少?这个数决定 rerank 是否值得。
- 你的评估集里有多少"知识库没有答案"的样本?
- 权限过滤写在检索的哪个参数上,还是写在结果列表推导式里?
- 换 embedding 模型时,旧索引怎么处理,回滚路径是什么?
- 你的查询里精确标识(编号、错误码)占多大比例,关键词路缺席的代价是多少?