过去 18 个月我做过 4 个 RAG 项目(2 个内部知识库、1 个客服、1 个法律检索), 加上看过的客户系统超过 10 个。这篇是把所有让我们把整套系统推倒重做的坑总结成一份避雷指南。
TL;DR
- RAG demo 跑通到上生产之间有一道评估鸿沟,95% 的项目挂在这里。
- Embedding 模型、chunk 策略、reranker 时机是三个最贵的错——一旦选错,重建成本几乎都要 2-4 周。
- 没有 eval 集就没有 RAG。先做 100 条标注样本再写代码。
坑 1 · embedding 模型选错
最常见错误:随手选了 all-MiniLM-L6-v2 因为「它免费」。然后 6 周后召回率上不去,才发现:
- 它是 384 维老模型,2024 年后基本不该作为生产首选
- 对中文性能尤其差(训练语料 EN 占绝大多数)
- 不支持长 chunk(> 512 token 直接截断,悄悄丢内容)
我现在的默认选型决策:
| 语言 | 预算紧 | 预算宽 | 本地部署 |
|---|---|---|---|
| 纯英文 | text-embedding-3-small | text-embedding-3-large | nomic-embed-text-v1.5 |
| 中文 / 中英混合 | text-embedding-3-large | BGE-M3 / Qwen3-Embedding | BGE-M3 |
| 代码 | voyage-code-3 | voyage-code-3 | nomic-embed-code |
坑 2 · chunk 切法想当然
「512 token 固定切」是教程标配,是生产毒药。
典型场景:
- FAQ:每条问答应该是一个 chunk,固定切会把答案截断在中间
- API 文档:方法签名、参数表、示例必须一起留,否则检索回来用户看不懂
- 聊天记录:按消息块或按时间窗,固定 token 切会把上下文打散
我现在用一个分层策略:
def chunk(doc):
# 1. structural chunking:按 markdown / HTML 标签拆
sections = split_by_headings(doc, max_levels=2)
# 2. 长度控制
out = []
for s in sections:
if token_count(s) > 800:
# 长 section 二次切,但用 semantic split(句号 / 段落优先)
out.extend(semantic_split(s, target=400))
else:
out.append(s)
# 3. 头尾拼回上下文(让 chunk 不孤立)
return [
f"# {doc.title}\n[Section: {s.heading}]\n{s.body}"
for s in out
]坑 3 · 上线前没有 eval 集
最常见死法:「先把 RAG 跑通,eval 上线再加」。结果上线后无法调优,因为没有可重复的指标。
最小可跑的 eval 集长这样:
# evals/v1.jsonl
{"q": "退款流程是什么?", "must_contain_doc_ids": ["doc-203", "doc-877"]}
{"q": "API 返回 429 怎么办?", "must_contain_doc_ids": ["doc-12"]}
{"q": "保修期多久", "must_contain_doc_ids": ["doc-55", "doc-9"]}100 条手工标注花你 1.5 天。看着多,但它能让你做的每一次改动都可量化, 否则你只能凭感觉调,最后通常调到一个比初版更糟的状态。
坑 4 · 召回评估只看 top-K
团队最爱报的指标:「我们 top-5 召回 88%!」 然后上线后发现答案经常错——因为相关文档在 top-5 里,但不在前 2。 生成模型只读 top-2 拼 prompt,剩下的全浪费。
真正该看的:
- MRR(平均倒数排名)——金标 doc 排在第 N 位的 1/N 的均值
- top-1 hit——金标在第一位的比例(最严格)
- nDCG@5——同时考虑命中和位置权重
坑 5 · 重新索引代价没算
某次客户问:「换 embedding 模型要多久?」 我说:「改一行代码的事。」 实际上:50 万 chunk × 0.05 美元/千 token × 平均 200 token = \$500 + 6 小时。 而且要做新旧模型并存的双写期,确保零宕机。
所以:
- 选 embedding 模型时直接选生产意图,不要先用便宜的,万一上线用户多了你要重建
- vector DB 选支持 namespace / collection 的(pgvector / Qdrant / Pinecone),方便 A/B
- 预留「全量重建」预算 = chunk 数 × token 单价
坑 6 · 多语言混合
中文用户问「API 怎么集成?」,文档是英文的。BGE-M3 这种多语言模型才能跨语言召回; OpenAI text-embedding-3 表现也行;MiniLM、bge-base-en 这种只懂英文的会直接挂。
除了模型选对,还要做查询改写:检索前先用 LLM 把中文 query 翻译成英文, 或同时用中文 query + 英文翻译两版做检索后合并。
坑 7 · 元数据过滤太晚
权限敏感场景(用户 A 不能看用户 B 的数据),过滤必须发生在向量检索阶段而不是检索后。 否则 top-K 里全是别人的数据,过滤完空了,模型一脸懵。
# 错的写法:先查再过滤
candidates = db.search(query_vec, top_k=5)
visible = [c for c in candidates if c.user_id == current_user] # 经常空
# 对的写法:检索时就带 filter
candidates = db.search(
query_vec,
top_k=5,
filter={"user_id": current_user}
)坑 8 · Reranker 时机错
Reranker(如 cohere-rerank、bge-reranker)应该用在检索之后、生成之前。
- 检索阶段拿 top-50(粗排,重召回)
- Reranker 把它压到 top-5(精排,重精度)
- 生成模型只看 top-5
常见错误:上来就用 reranker 替代 embedding 检索。reranker 是 N×N 比对,5 万条文档你跑不动。
坑 9 · 监控盲点
RAG 上线后必须监控的 3 个数:
- 「我不知道」率——模型说「无法回答」的比例。突然飙升说明检索质量塌了
- 引用 chunk 一致性——回答里引的 chunk 是不是检索 top 内的(防幻觉)
- 用户继续追问率——一次答不完,用户再问一次就是答得不好的信号
上线 checklist
- [ ] 选了正确的 embedding 模型(看坑 1 表)
- [ ] chunk 策略不是固定 token,是结构 + 语义混合
- [ ] 有 ≥ 100 条标注 eval 集,能 CI 跑
- [ ] 评估指标是 MRR + top-1 + nDCG@5,不只是 top-K hit
- [ ] 算清楚全量重建成本
- [ ] 多语言场景上 BGE-M3 或 text-embedding-3-large + query rewriting
- [ ] 元数据过滤在检索阶段做
- [ ] 引入 reranker,但放在检索后
- [ ] 监控「不知道」率、chunk 一致性、追问率
这篇 playbook 我手写后用 LLM 协助润色 / 校对,每一段技术结论都基于真实测试。如果你发现描述与你的环境有出入,欢迎提交 issue 或邮件 hello@xaikey.com。争议条目我会标注更新日期。
加入每周 AI 工程师 Brief
新 playbook 上线第一时间通知,附作者每周观察。永久免费。