← 返回博客
AI

别凭感觉优化 RAG:我如何建立第一个可测量的 Retrieval Baseline

·24 分钟阅读

说明: 本文中的评测案例均经过脱敏处理。部分业务名称、字段和示例数据已替换为具有相同检索特征的 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。

第一次真实运行得到:

MetricBaseline V1
Recall@50.9167
MRR0.6261
nDCG@50.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?

查看全部文章 →

别凭感觉优化 RAG:我如何建立第一个可测量的 Retrieval Baseline|Michikatsu