别凭感觉优化 RAG:我如何建立第一个可测量的 Retrieval Baseline
说明: 本文中的评测案例均经过脱敏处理。部分业务名称、字段和示例数据已替换为具有相同检索特征的 synthetic examples。它们用于还原真实的 Retrieval Failure Pattern,不包含真实业务数据。
最近在重新系统学习 RAG 时,我开始思考一个看起来很基础、但以前没有认真回答过的问题:
一个 RAG 系统跑起来以后,我怎么知道它到底好不好?
大多数 RAG Demo 都遵循类似的流程:
Documents
↓
Chunking
↓
Embedding
↓
Vector Database
↓
Retrieval
↓
LLM
↓
Answer
把文档切成 chunks,生成 embeddings,存进 vector database,再根据用户 query 做 similarity search,最后把检索到的 context 交给 LLM。
把这条链路跑起来其实不算特别困难。
真正的问题是:
然后呢?
如果回答效果不好,我应该优化什么?
是 Chunk Size?
Embedding Model?
还是应该加入 BM25 / Hybrid Retrieval?
Query Rewrite?
Reranker?
如果我同时修改几个地方,最终回答看起来“好像更好了”,我又怎么知道究竟是哪一个改动产生了效果?
我逐渐意识到:
在优化 RAG 之前,我首先需要的不是另一个 RAG technique,而是一个可以测量的 baseline。
所以这一次,我没有急着加入任何复杂的 RAG 优化策略。
我先搭了一个尽可能简单的 RAG,然后给它建立了一套 Retrieval Evaluation。
结果比我预想的有意思得多。
1. 我故意从一个很简单的 RAG 开始
我的 Baseline V1 没有什么高级技术:
Chunk Strategy: Fixed Chunk
Chunk Size: 1000 characters
Chunk Overlap: 0
Embedding: text-embedding-v4
Vector Store: Qdrant
Distance: Cosine
Retrieval Strategy: Dense Retrieval
Top-K: 5
Query Rewrite: None
Reranker: None
Hybrid Retrieval: None
Chunking 甚至只是最简单的字符切分:
1000 characters
→ cut
→ next 1000 characters
→ cut
...
它不理解 Markdown heading。
不理解 paragraph boundary。
也不理解 semantic boundary。
甚至没有 overlap。
这么做是故意的。
因为我现在需要的并不是一个“尽可能强”的 RAG,而是一个:
足够简单、行为明确、可以重复运行的 control group。
如果一开始就同时加入 Semantic Chunking、Hybrid Retrieval、Query Rewrite 和 Reranking,即使最终效果很好,我也很难知道每一个组件究竟贡献了什么。
2. “能回答问题”并不是一个 Eval
以前测试一个 RAG,我很容易这么做:
问系统几个问题。
看看答案。
然后得出一个非常模糊的结论:
嗯,好像还不错。
但这个方法有一个明显的问题:
它无法稳定量化。
假设今天我把 chunk size 从 1000 改成 500。
昨天的回答:
看起来不错。
今天的回答:
好像也不错。
那到底有没有提升?
不知道。
而且,一个 RAG 的最终回答实际上至少受到两个环节影响:
Retrieval Quality
+
Generation Quality
如果最终回答错了,我首先需要区分:
Retriever 根本没有找到正确 evidence?
还是:
Retriever 找到了正确 evidence,但是 LLM 没有正确利用?
所以我的第一版 Evaluation 没有急着评最终 LLM Answer。
我先把问题缩小到:
Retrieval。
3. 我的 Retrieval Eval 到底在测什么?
我先建立了一个很小的 evaluation dataset。
对于每一个问题,我都会标记它对应的 expected evidence。
脱敏后,一个 Eval Case 大概长这样:
{
"case_id": "case_001",
"query": "为什么需要把订单信息同步到下游履约系统?",
"expected_evidence": [
{
"document_id": "doc_001",
"unit_id": "doc_001:chunk:0",
"relevance_grade": 1
}
]
}
Retriever 真正执行:
Query
↓
Query Embedding
↓
Vector Search
↓
Top-K Retrieval Results
然后 Evaluation Runner 比较:
Expected Evidence
VS
Actual Top-K Results
第一版我使用了三个常见的 Retrieval Metrics。
Recall@K:正确资料有没有找到?
Recall@K 回答的是:
应该找到的 evidence,有多少进入了 Top-K?
比如一个问题只有一个 expected evidence。
正确 chunk 进入 Top-5:
Recall@5 = 1
没有进入:
Recall@5 = 0
如果一个问题需要两个 evidence,而 Top-5 只找到了一个:
Recall@5 = 0.5
所以我现在会把 Recall 理解成:
有没有把该找的东西找回来?
MRR:正确资料排得够不够靠前?
MRR(Mean Reciprocal Rank)关注的是另一个问题:
第一个正确 evidence 出现得有多早?
例如:
Correct Evidence Rank #1 → 1.000
Correct Evidence Rank #2 → 0.500
Correct Evidence Rank #3 → 0.333
Correct Evidence Rank #4 → 0.250
Correct Evidence Rank #5 → 0.200
Not Found → 0
这意味着:
“Top-5 找到了”
和
“Rank #1 就找到了”
其实是完全不同的 retrieval quality。
nDCG@K:整个排序质量怎么样?
nDCG@K 则进一步考虑:
相关 evidence 是否出现在一个合理的 ranking position?
所以我现在会这样理解这三个指标:
Recall@5
→ 找没找到?
MRR
→ 第一个正确结果排得够不够靠前?
nDCG@5
→ 整个 Top-5 的 relevance ranking 怎么样?
当这三个指标放在一起后,Retrieval Quality 第一次从“感觉”变成了可以观察的东西。
4. 第一次真实 Baseline
我的第一版 benchmark 非常小:
Documents: 1
Retrieval Units: 14
Eval Cases: 30
其中第一版 evaluation questions / ground truth 还有 AI 辅助生成的部分。
所以我不会把它描述成 production benchmark。
它目前更接近:
一个用于验证 Evaluation Pipeline、发现 Failure Pattern 和进行后续 Regression Testing 的小型 benchmark。
第一次真实运行得到:
| Metric | Baseline V1 |
|---|---|
| Recall@5 | 0.9167 |
| MRR | 0.6261 |
| nDCG@5 | 0.6961 |
看到 Recall@5 = 0.9167 的时候,我第一反应其实是:
91.7%?那不是已经挺好了吗?
但是再看:
MRR = 0.6261
问题就出现了。
Retriever 很多时候确实“找到了”,但没有“排好”。
5. High Recall ≠ Good Retrieval
我开始逐个检查 per-case retrieval results,而不是只看 aggregate metrics。
很快就发现了一种典型情况:
Query
↓
Top-5 Retrieval Results
#1 Related Chunk
#2 Related Chunk
#3 Related Chunk
#4 Correct Evidence ←
#5 Another Chunk
如果只看 Recall:
Recall@5 = 1
看起来完全正确。
因为 expected evidence 确实进入了 Top-5。
但是:
Reciprocal Rank = 1 / 4 = 0.25
这显然和 Rank #1 不是同一种 retrieval quality。
这让我第一次非常直观地意识到:
High Recall does not necessarily mean good retrieval.
Dense Retrieval 很可能已经找到了正确的 semantic neighborhood,却没有把真正 answer-bearing 的 evidence 放到最前面。
这也是为什么仅仅说:
“我的 RAG 能召回正确内容。”
其实是不够的。
我们还需要问:
它把正确内容排在了哪里?
6. 第一个 Failure Pattern:Topic-Relevant ≠ Answer-Bearing
其中有一些 case 很有意思。
假设用户问:
订单同步失败后,系统是否允许重试?
真正包含答案的 chunk 可能明确写着:
If synchronization fails:
- mark the task as FAILED
- preserve the failure reason
- allow subsequent retry
但是 Dense Retriever 返回:
#1 关于订单同步流程的介绍
#2 关于同步状态的说明
#3 关于下游系统的说明
#4 关于异常处理的介绍
#5 真正包含 retry 规则的 chunk ←
从语义上看,前四个结果都“相关”。
它们都在谈:
sync
failure
status
downstream system
但真正能够直接回答用户问题的 evidence 却排在最后。
这让我意识到:
Topic relevance 和 Answer-bearing relevance 不是一回事。
Embedding-based Dense Retrieval 很擅长找到“语义上属于这个主题”的内容。
但用户真正需要的往往是:
哪个 chunk 最直接地包含答案?
这也让我开始理解为什么 Reranking 会成为 RAG 系统中的一个独立问题。
不过在 Baseline V1,我没有加 Reranker。
因为现在我首先需要记录这个 failure。
7. 第二个 Failure Pattern:Fixed Chunking 会直接破坏语义
另外一个很明显的问题来自 Chunking。
我的 Baseline 使用:
Fixed Chunk
1000 characters
0 overlap
程序只知道:
text[0:1000]
text[1000:2000]
text[2000:3000]
它完全不知道什么叫一个完整的知识单元。
于是,一个完整句子可能变成:
Chunk N:
Each package is associated with one shipment
---------------- chunk boundary ----------------
Chunk N+1:
and one package reference ID.
从程序角度看:
每个 chunk 都满足 1000 characters。
完全正确。
但从语义角度看:
一个完整事实已经被拆成了两个 Retrieval Units。
在我的 Eval 里,确实出现了一个问题需要两个相关 evidence,但 Retriever 最终只召回了其中一个。
结果:
Recall@5 = 0.5
这让我开始意识到:
Chunk Size 并不只是“500、800 还是 1000”的参数问题。
更本质的问题是:
每一个 Retrieval Unit 是否仍然保留了足够完整的语义?
8. Structured Data 把问题暴露得更明显
这个问题在 JSON / structured data 上尤其明显。
假设知识库中有一个 synthetic request example:
{
"shippingMethod": "Express",
"currency": "EUR",
"dimensionUnit": "CM",
"orderStatus": "Canceled",
"orderType": "Drop Shipping"
}
Fixed-size chunking 并不知道这是 JSON。
所以它完全可能这样切:
Chunk N:
{
"shippingMethod": "Express",
"curr
---------------- chunk boundary ----------------
Chunk N+1:
ency": "EUR",
"dimensionUnit": "CM",
"orderStatus": "Canceled",
"orderType": "Drop Shipping"
}
字段甚至可能从中间被切开。
但还有一个更隐蔽的问题。
假设:
Chunk N:
## Request Example
{
...
而真正的字段和值全部落到了:
Chunk N+1:
"currency": "EUR",
"dimensionUnit": "CM",
"orderStatus": "Canceled",
"orderType": "Drop Shipping"
那么 Chunk N+1 虽然保留了答案,却可能失去一个重要上下文:
这些字段来自一个 Request Example。
最终我确实观察到了类似的 Retrieval Miss。
比如用户问:
示例订单中的 orderType 是什么?
正确 evidence 中明明存在:
"orderType": "Drop Shipping"
但这个 chunk 没有进入 Top-5。
也就是说:
Answer exists in the knowledge base
↓
Chunk boundary removes useful context
↓
Chunk representation becomes weaker
↓
Dense Retrieval misses the evidence
以前看到:
Chunking strategy is important for RAG.
我知道这句话。
但真正看到这种 failure case 后,我才开始理解它为什么重要。
9. 第三个 Failure Pattern:Evaluation Dataset 自己也可能有问题
另一个让我意外的发现并不来自 Retriever。
而来自 Ground Truth。
假设某个 case 标注:
Expected Evidence:
chunk_2
Retriever 返回:
#1 chunk_1
#2 chunk_2
那么按照当前 ground truth:
Reciprocal Rank = 0.5
第一反应很容易是:
Retriever 排错了。
但是当我真正打开 chunk_1 阅读后发现:
chunk_1 其实也能够回答这个问题。
这意味着可能不是 Retriever 错。
而是:
Ground Truth 标注得不完整。
于是我发现 RAG Evaluation 有一个有点“递归”的问题:
Before trusting your eval results, you also need to evaluate your evaluation dataset.
特别是当 evaluation questions 或 expected evidence 有 AI 辅助生成时,这个问题更值得警惕。
AI 可以帮忙提高 dataset creation 的效率。
但至少对于一个严肃的 benchmark:
Question
Expected Evidence
Relevance Grade
仍然需要 human review。
否则你最终可能是在:
用一个不准确的 benchmark,非常精确地测量一个系统。
10. 91.67% Recall 并不意味着我的 RAG 有 91.67% 的“准确率”
这里还有一个非常重要的限制。
我的 Baseline V1 只有:
1 document
14 retrieval units
30 eval cases
而每次 retrieval:
Top-K = 5
也就是说,每次 Retriever 都会返回:
5 / 14 ≈ 35.7%
的候选 chunks。
这和一个拥有:
10,000
100,000
甚至 millions of chunks
的 production knowledge base 完全不是一个难度。
所以:
Recall@5 = 0.9167
绝对不能解释成:
“这个 RAG 在生产环境拥有 91.67% 的准确率。”
这不是当前实验能证明的事情。
Baseline V1 更准确的定位应该是:
Evaluation Pipeline Validation + Small Retrieval Regression Benchmark。
未来还需要:
- 更多 documents
- 更多 domain coverage
- 更多 hard negatives
- 更严格的 human-reviewed ground truth
- 不同 query types
- 更接近真实用户分布的问题
才能逐渐变成一个更可信的 benchmark。
11. 那这个很小的 Baseline 到底有什么价值?
我觉得价值反而很明确。
因为从现在开始,我终于有了一条可以重复执行的实验循环:
Baseline
↓
Run Evals
↓
Inspect Failure Cases
↓
Form a Hypothesis
↓
Change One Main Variable
↓
Run the Same Benchmark
↓
Compare Metrics
↓
Inspect Regressions
例如现在已经出现了一个明确的假设:
Fixed-size chunking 造成的 context loss,可能正在影响部分 retrieval cases。
那下一步就可以设计一个实验:
Baseline V1
Fixed 1000-char Chunking
VS
Structure-aware / Context-preserving Chunking
保持:
Same Dataset
Same Embedding Model
Same Vector Store
Same Retriever
Same Top-K
Same Metrics
尽量只改变:
Chunking Strategy
然后再看:
Recall@5
MRR
nDCG@5
以及更重要的:
原来的 failure cases 到底有没有被修复?
12. 我不准备一次把所有 RAG 技术都加进去
看到这些 failure cases 以后,当然可以立刻开始加入:
BM25
Hybrid Retrieval
Query Rewrite
Reranker
Semantic Chunking
Contextual Retrieval
...
但如果一次全部加进去,就又会回到最开始的问题:
到底是谁让系统变好了?
所以接下来我更想把每一个优化当成一个独立 experiment。
例如:
Baseline V1
Fixed Chunk + Dense Retrieval
Recall@5 0.9167
MRR 0.6261
nDCG@5 0.6961
↓
Experiment 1
Context-preserving Chunking
↓
Same Benchmark
↓
Compare
之后再分别研究:
Dense Retrieval
vs
BM25
vs
Hybrid Retrieval
以及:
Can Reranking improve MRR without hurting Recall?
还有:
When does Query Rewrite actually help?
每次实验我希望只回答三个问题:
What improved?
Which failure cases were fixed?
What regressed?
13. 从“功能驱动”变成“Eval 驱动”
这次最大的收获,其实不是实现了 Recall、MRR 或 nDCG。
也不是把 Embedding、Qdrant 和 Retrieval Runner 接了起来。
而是我开始改变自己优化 RAG 的方式。
以前更容易是:
RAG 效果不好
↓
听说 Hybrid Retrieval 很好
↓
加 Hybrid
↓
听说 Reranker 很重要
↓
加 Reranker
↓
感觉好像更强了
现在我更希望变成:
Baseline
↓
Failure
↓
Evidence
↓
Hypothesis
↓
Controlled Experiment
↓
Measurement
↓
Regression Analysis
也就是:
Building RAG by Evals, Not Vibes.
Baseline V1 当然还非常小。
Dataset 需要扩大,Ground Truth 需要人工 review,Evaluation methodology 也还有很多东西需要继续学习。
但至少从这一版开始,后面每增加一个所谓的“RAG optimization”,我都可以要求它回答一个非常具体的问题:
Compared with the baseline, what did you actually improve?
下一步
下一步我准备先研究当前最明显的一类 failure:
Chunking / Context Preservation。
因为 Baseline V1 已经出现了完整语义被固定字符边界切断、structured data 丢失上下文,以及 expected evidence 无法进入 Top-K 的案例。
下一次实验不会先追求“更复杂”。
而是验证一个非常具体的假设:
Can better chunking actually fix the retrieval failures observed in Baseline V1?
如果答案是 yes,我希望能够通过同一套 benchmark 证明它。
如果答案是 no,那同样是一个有价值的实验结果。
Next: Part 2 — Can Better Chunking Fix These Retrieval Failures?