Featured image of post RAG生产实践:常见坑、性能优化与迭代路线图

RAG生产实践:常见坑、性能优化与迭代路线图

从 Demo 到生产环境,RAG 最大的挑战通常不是搭起来,而是持续稳定地跑下去。

RAG 的 Demo 很容易做,生产环境却很容易失控。

一开始你可能只要:

  • 一个 embedding 模型
  • 一个向量库
  • 一个聊天模型

但真到线上,问题会迅速变成:

  • 更新怎么做
  • 成本怎么控
  • 延迟怎么降
  • 数据怎么隔离
  • 错误怎么排查
  • 版本怎么回归

所以生产级 RAG 的重点是"工程稳定性",不只是"回答效果"。

这篇文章会讲:

  • 生产环境最常见的坑
  • 性能优化的思路
  • 一个可执行的迭代路线

一、最常见的 10 个坑

坑 1:把所有文档放进一个大集合

现象

公司有产品文档、技术规范、内部 wiki、销售话术、HR 制度……全部丢进一个向量库。

问题

用户问"API 超时怎么配置",检索结果混入了销售话术(因为提到了"时间")、HR 制度(因为提到了"加班时间")。正确答案被噪声淹没。

原理

向量检索基于语义相似度。“时间"这个概念在很多文档里都出现,如果检索范围太大,语义相似的噪声也会被召回。

解决

按业务边界拆分 source,检索时指定来源。

知识库结构:
├── product_docs/    # 产品文档
├── api_specs/       # API 规范
├── internal_wiki/   # 内部 wiki
└── hr_policies/     # HR 制度

检索时:
filter = {"source": "api_specs"}

效果

配置Recall@5噪声比例
全量检索58%62%
按 source 过滤84%18%

坑 2:chunk 没有业务上下文

现象

按段落切分后,某个 chunk 只剩一句话:“将旋钮调至 3 档,等待指示灯变绿。”

问题

用户问"ZK-200 怎么校准”,这个 chunk 不会召回。因为产品名、设备类型、操作名称都在标题里——被切掉了。

原理

Embedding 只能看到 chunk 内部的文字。如果 chunk 里没有"ZK-200"和"校准"这些关键词,向量就不会和这些问题相似。

解决

把父级标题拼到 chunk 前面:

原始 chunk:
"将旋钮调至 3 档,等待指示灯变绿。"

补充上下文:
"【ZK-200 操作手册 > 2. 使用方法 > 2.3 校准流程】
将旋钮调至 3 档,等待指示灯变绿。"

效果

配置Recall@5
原始 chunk41%
补充上下文后78%

这是 RAG 里性价比最高的优化之一:成本几乎为零,效果翻倍。

现象

用户问"错误码 E0028 是什么意思",向量检索召回的是"常见错误码及解决方案"这种泛泛而谈的文档,而不是精确包含"E0028"的那段。

原理

向量检索擅长语义相似,不擅长精确匹配。“E0028"作为一个编号,在向量空间里没有特殊意义,它只是一个 token。

解决

引入 BM25 或 sparse 检索,做混合检索。

混合检索:
向量检索 → 召回语义相似的文档
BM25 检索 → 召回包含精确关键词的文档
RRF 融合 → 合并两路结果

效果

检索方式精确匹配查询 Recall@5语义查询 Recall@5
向量检索47%82%
BM2576%58%
混合检索84%85%

坑 4:假 rerank

现象

代码里写着"rerank”,但只是把向量分数重新排了一次序。

def rerank(candidates):
    return sorted(candidates, key=lambda x: x['score'], reverse=True)

问题

这不是真正的 rerank,只是把已有的分数重新排序,效果和没有 rerank 一样。

原理

真正的 Rerank 需要用 Cross-Encoder:

  • 把 Query 和每个候选 Document 拼在一起
  • 丢进 Transformer,让模型学习两者的交互
  • 输出新的相关性分数

这比双塔结构的向量检索更精确,因为 Query 和 Document 能充分交互。

解决

使用真正的 Cross-Encoder 模型:

候选文档 → Cross-Encoder → 新分数 → 重新排序

效果

配置Recall@5正确答案平均排名
无 Rerank72%4.2
假 Rerank72%4.1
真 Rerank88%1.8

坑 5:top_k 和阈值写死

现象

所有场景都用固定的 top_k=5 和 score_threshold=0.3。

问题

  • 简单问题:只需要 3 个候选就够了,多余的候选增加噪声
  • 复杂问题:需要 10 个候选才能覆盖,5 个不够

原理

检索参数取决于问题的复杂度、文档的分布、embedding 模型的特性。不同场景的最优参数不同。

解决

参数可配置,甚至动态调整:

简单事实问题 → top_k=3, threshold=0.5(少而精)
复杂问题 → top_k=15, threshold=0(多召回)
对比类问题 → top_k=20(需要更多信息)

坑 6:没有 context budget 设计

现象

检索到 15 个 chunk,全部塞进 prompt,总共 12000 tokens。

问题

  • 成本高:token 消耗大
  • 延迟高:模型处理慢
  • 效果差:内容太长,模型注意力分散(Lost in the Middle 问题)

原理

LLM 的注意力机制在长文本上会分散。研究表明,当相关信息在 prompt 中间位置时,模型更容易忽略它。

解决

设计 context budget:

预算:4000 tokens

组装策略:
1. 优先高分 chunk
2. 控制 token 总量
3. 必要时截断过长的 chunk
4. 保持来源多样性(不同 source 的 chunk 都有)

经验值

场景推荐 token 预算
简单问答2000-3000
复杂问答3000-5000
文档总结5000-8000

坑 7:文档更新了,索引没同步

现象

周一用户问"报销流程",答案正确。周三财务更新了报销规范,周五用户再问,答案还是旧的。

问题

文档更新后,向量库没有同步,还在用旧版本。

解决

建立版本管理机制:

方案 1:定时全量同步
- 每天凌晨重建索引
- 简单但成本高

方案 2:增量更新
- 监控文档变更
- 只更新变化的文档
- 复杂但成本低

方案 3:版本标记
- 每个文档有版本号
- 检索时检查版本是否最新
- 过期则重新索引

关键:保留文档的 hash 或时间戳,便于判断是否需要更新。

坑 8:没有证据引用

现象

模型回答:“报销需要提交发票和审批单。”

用户:“这个答案依据是什么?”

系统无法回答。

问题

答案和来源脱节,用户无法验证答案的可信度。

解决

Prompt 中强制要求引用:

要求:
1. 答案必须基于参考资料
2. 回答时标注引用来源,格式:[来源: 文档名 > 章节]
3. 如果不确定,说明不确定

答案示例:

报销需要提交发票和审批单 [来源: 财务规范 > 3.2 报销流程]。

好处

  • 用户可以验证答案
  • 系统可以追溯错误
  • 增加答案可信度

坑 9:没有离线 benchmark

现象

开发者说"效果提升了",产品说"感觉变差了",谁也说服不了谁。

问题

没有客观的评价标准,优化靠主观感受。

解决

建立固定 benchmark:

评测集:
- 50-100 个问题
- 每个问题有期望召回的 chunk
- 每个问题有参考答案

定期评测:
- 每次改动后跑 benchmark
- 对比历史版本
- 设置阈值(如 Recall@5 > 80%)

原则

  • 评测集要隔离,不参与训练
  • 定期从线上采样真实问题,更新评测集
  • 多指标综合评估

坑 10:没有 inspect 能力

现象

答案错了,不知道是检索错、排序错、还是模型错。

问题

无法定位问题,优化靠猜。

解决

保留每次请求的 Trace:

Trace 包含:
- 用户问题
- 检索候选(top-10)
- Rerank 结果
- 最终 prompt
- 模型答案

有 Inspect 页面,可以查看每一步的中间结果。

好处

  • 快速定位问题
  • 积累 bad cases
  • 指导优化方向

二、性能优化的思路

延迟分析

一个典型 RAG 请求的延迟分布:

总延迟:400-600ms

├── Query Embedding:30-50ms(5-10%)
├── 向量检索:30-50ms(5-10%)
├── BM25 检索:10-20ms(2-5%)
├── RRF 融合:<5ms(<1%)
├── Rerank:150-200ms(30-50%)← 主要瓶颈
├── Prompt 组装:<10ms(<1%)
└── LLM 生成:150-300ms(30-50%)← 主要瓶颈

两个主要瓶颈:RerankLLM 生成

Rerank 优化

思路 1:减少候选数量

Rerank 的计算量与候选数量成正比。

100 个候选 × Cross-Encoder ≈ 200ms
30 个候选 × Cross-Encoder ≈ 70ms

但不能减太少,否则会漏掉正确答案。建议:

  • 召回 50-100 个
  • Rerank 前 20-30 个
  • 输出 top 5-10 个

思路 2:用更快的模型

模型延迟(100 候选)Recall@5
bge-reranker-large~180ms88%
bge-reranker-base~95ms85%
ms-marco-MiniLM~45ms80%

速度和效果的权衡。

思路 3:跳过简单问题

对于简单问题,可能不需要 Rerank:

简单问题(候选集中度高)→ 跳过 Rerank
复杂问题(候选分散)→ 需要 Rerank

判断标准:看 top-5 候选的分数分布。如果第一名分数远高于其他,说明检索已经足够准确。

LLM 优化

思路 1:用更快的模型

GPT-4o-mini 比 GPT-4o 快 3-5 倍,成本是 1/10。

对于大部分 RAG 场景,mini 版足够用。

思路 2:流式输出

不要等完整答案生成,边生成边返回:

传统模式:
用户等待 → 完整答案 → 一次返回(延迟高)

流式模式:
用户等待 → 第一个字 → 第二个字 → ...(感知延迟低)

流式不会减少实际延迟,但能改善用户体验。

思路 3:缓存热门 query

相同问题直接返回缓存:

热门问题 TOP 100:
"报销流程" → 缓存答案
"API 认证" → 缓存答案
...

命中率:10-30%
节省:10-30% 的 LLM 调用

检索优化

思路 1:缓存 query embedding

同一个问题的 embedding 可以缓存:

Query → Embedding(40ms)
Query(重复)→ 缓存(<1ms)

思路 2:预计算

某些场景可以预计算:

FAQ 场景:
- 预计算每个 FAQ 的 embedding
- 预计算每个 FAQ 的最佳答案
- 用户问题直接匹配 FAQ

适合问题集合固定的场景。

思路 3:减少召回数量

如果检索质量已经很高,可以减少召回数量:

top-50 → top-30
节省:向量检索和 Rerank 的时间
风险:可能漏掉正确答案

三、成本优化

Token 消耗分析

单次请求的 token 消耗:

输入 token:
- Query:20-50
- Chunks(5个):2000-5000
- System prompt:100-200
总输入:2000-6000 tokens

输出 token:
- Answer:100-500

按 GPT-4o-mini 定价:
输入:$0.15/1M tokens
输出:$0.60/1M tokens

单次请求成本:$0.0003-0.001
日均 10000 次 → 月成本:$100-300

成本优化思路

1. 控制上下文长度

上下文太长不仅成本高,效果还差。

经验值:3000-4000 tokens

超过预算时:
- 截断过长的 chunk
- 减少进入 prompt 的 chunk 数量
- 优先保留高分的 chunk

2. 用更便宜的模型

GPT-4o-mini 的成本是 GPT-4o 的 1/10,效果差距不大。

3. 缓存热门问题

10-30% 的请求是重复问题,直接返回缓存。

4. 优化 prompt

去掉冗余的 prompt 内容,精简指令。

差的 prompt(冗长):
"你是一个非常专业的技术文档助手,你的任务是根据用户提供的参考资料
来回答用户的问题。请注意,你的回答必须严格基于参考资料..."

好的 prompt(精简):
"根据参考资料回答问题。答案必须基于资料,标注来源[数字]。"

四、迭代路线图

V1:能跑通(1-2 周)

目标:回答对 60% 的核心问题

配置:
- 基础 chunking(按段落)
- 纯向量检索(top_k=5)
- 基础 prompt + 引用要求

验收:
- 能回答常见问题
- 有引用来源
- 能 demo 演示

不要做

  • 不要全量导入所有文档
  • 不要追求复杂架构
  • 不要过度优化

V2:能诊断(2-3 周)

目标:知道为什么对/错

配置:
- Inspect 页面(看检索候选)
- Metadata 过滤
- 参数可配置
- 离线评测集(50 条)

验收:
- 能看到检索的 top-10
- 能定位问题在哪一层
- 有 Recall@5 指标

V3:能优化(3-4 周)

目标:召回率 80%+

配置:
- Hybrid Retrieval(向量 + BM25)
- 真 Rerank
- Chunking 优化(补充上下文)
- 评测集扩展(100 条)

验收:
- Recall@5 > 80%
- 能回答复杂问题
- Bad cases 持续跟踪

V4:能运营(持续)

目标:持续监控、持续迭代

配置:
- Trace 存储
- 监控大盘
- 告警规则
- 用户反馈收集
- CI/CD 集成

验收:
- 有质量监控
- 能快速定位线上问题
- 每次改动有回归测试

收益递减规律

60% → 80%:基础优化(chunking、混合检索)
80% → 90%:系统优化(Rerank、prompt 调优)
90% → 95%:高级优化(query rewrite、模型微调)
95% → 99%:难度极大,投入产出比低

建议:先做到 85%,再决定是否值得继续投入。

五、团队协作

角色分工

产品/运营:
- 定义知识边界
- 收集 bad cases
- 标注评测集
- 用户反馈归类

算法/NLP:
- Embedding 模型选择
- Rerank 模型调优
- Chunking 方案
- 高级策略

后端工程师:
- Ingestion pipeline
- 向量库维护
- Trace/Inspect 系统
- 性能优化
- 监控告警

协作流程

日常迭代:

1. 产品收集 bad cases
2. 算法分析问题(定位到哪一层)
3. 算法 + 后端共同优化
4. 跑 benchmark 验证
5. 上线 + 监控

问题归属

问题归属方向
召不回正确内容算法Chunking、检索策略
候选噪声多算法Rerank、过滤
答案格式不对算法Prompt
延迟高后端缓存、异步
系统不稳定后端稳定性
模型幻觉算法Prompt、模型选择
知识库没更新产品/运营文档维护流程

六、判断系统成熟度

如果满足以下条件,说明系统开始成熟:

□ 知道每次回答用了哪些证据
  (有 Inspect,能追溯)

□ 能复现错误回答的完整链路
  (有 Trace 存储)

□ 有固定 benchmark
  (定期跑,有阈值)

□ 能对比不同版本的质量
  (有版本管理,能 A/B)

□ 知道下一步优化哪一层
  (有监控,不是盲调)

如果这 5 条还做不到,最应该投入的不是"换更强模型",而是把检索、Inspect 和评测体系补完整。

七、系列总结

这四篇文章回答了 RAG 的核心问题:

入门

  • RAG 的本质:把知识从参数里剥离出来
  • Embedding 的原理:对比学习、向量空间
  • 向量检索的局限:对精确匹配不稳定

进阶

  • Chunking:按语义切分 + 补充上下文
  • 混合检索:向量 + BM25 + RRF 融合
  • Rerank:Cross-Encoder 做真正的精排

评测

  • 分层评测:检索、重排、组装、生成
  • LLM-as-Judge:让大模型评判答案质量
  • Inspect:保留完整 Trace,定位单次问题

生产

  • 常见坑:上下文丢失、假 rerank、无引用
  • 性能优化:缓存、模型选择、候选数量控制
  • 迭代路线:从能跑到能运营

RAG 不是银弹,但它是目前让 LLM 接地气的最实用方案。关键在于:不要追求一步到位,而是建立可迭代的能力


后续可以深入的方向

  1. Query Rewrite 和 Query Decomposition
  2. Contextual Retrieval(Anthropic 的方法)
  3. 多租户与权限设计
  4. RAG 与 Agent 的结合
  5. 多模态 RAG(图片、表格)
使用 Hugo 构建
主题 StackJimmy 设计