上线一个 RAG,难点从来不是“把文档塞进向量库”。
真正麻烦的是:命中率飘、延迟变大、答案开始胡说、出了问题还复盘不了。
这篇文章我把上线前最值得做的事情整理成三张图:
- 架构图:你到底在上线什么(数据管道/索引/检索/rerank/生成/校验/观测)
- 对比卡片:混合检索 vs 纯向量 vs 纯 BM25,怎么选不纠结
- Checklist 卡片:上线前逐项勾掉,避免“上线后边跑边修”
1) 你上线的不是模型,是一条链路(架构图)
RAG 的“能力上限”往往由最弱的一环决定:数据质量、切分、检索、拼接、校验、观测。

这张图里有三个节点特别容易被忽略:
- 上下文构建(去重/截断/引用):很多胡说来自“证据被截断/重复污染”
- 后置校验:引用是否存在?关键数值是否一致?敏感内容是否外泄?
- 可观测性:出了 badcase,必须能回放到“当时检到了什么、拼了什么 prompt”
2) 选型别纠结:先混合,再 rerank(对比卡片)
不少团队一上来就想“把 embedding 调到完美”。
但工程上更稳的默认是:混合检索做底,rerank 提质。

2.1 一个简单结论
- 你对“型号/ID/精确术语”敏感:BM25 不能丢
- 你对“同义词/长尾表达”敏感:向量检索必须有
- 你想上线后能排障:混合检索 + 可观测是性价比最高的组合
3) 上线前 Checklist(可收藏卡片)
如果你只想把这篇文章的核心复制到团队 wiki:就复制这张图。

4) 代码块:一次请求要记录哪些东西(最小可用)
上线后排障最怕一句话:
“它刚才明明可以的。”
你要能回放,就得把关键中间产物打出来:规范化后的 query、top chunks、最终 prompt 长度、是否截断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| from dataclasses import dataclass
from typing import List, Dict, Any
import time
@dataclass
class Chunk:
doc_id: str
score: float
text: str
def rag_debug_once(question: str) -> Dict[str, Any]:
t0 = time.time()
# 1) query
query = " ".join(question.strip().split())
# 2) retrieve (mock)
chunks: List[Chunk] = [
Chunk(doc_id="doc:pricing", score=0.78, text="..."),
Chunk(doc_id="doc:limits", score=0.74, text="..."),
]
# 3) build prompt
context = "\n\n".join(
f"[source:{c.doc_id} score={c.score:.2f}]\n{c.text}" for c in chunks
)
prompt = (
"只允许基于 sources 回答,并在结尾列出引用。\n\n"
f"Question:\n{query}\n\n"
f"Sources:\n{context}\n\n"
"Answer:\n"
)
# 4) llm call (mock)
answer = "(mock) ..."
return {
"latency_ms": int((time.time() - t0) * 1000),
"query": query,
"top_docs": [{"doc_id": c.doc_id, "score": c.score} for c in chunks],
"prompt_chars": len(prompt),
"answer": answer,
}
|
5) 收尾:把“可回放”当成上线前置条件
RAG 的迭代不是玄学。
你只要能把一次 badcase 的链路完整记录下来(检索→拼接→生成→校验),后面每一次优化都会更快、更确定。