一、背景:为什么要单独拆检索层?
在做 RAG 系统测试时,一开始很容易陷入一个误区:
👉 只看最终回答对不对
做下来发现:
很多问题根本不是生成错,是"没找到对的内容"
就是说:
问题出在检索层
二、检索层到底在做什么?
一句话:
从一堆 chunk 中,找出和问题最相关的几个
整体流程:
用户问题
↓
拆词(query_tokens)
↓
每个chunk拆词(chunk_tokens)
↓
计算交集数量(score)
↓
排序
↓
取 top_k(默认3)
👉 本质:
词面匹配 + 计数排序
三、手算一遍检索过程(核心理解)
例子说明:
输入:
query:如何查看日志
Step1:拆分 query_token
query_token = {
如何, 何查, 查看, 看日, 日志,
如何查看日志
}
Step2:chunk 拆词
chunk1:
日志查看方法
chunk1_token = {
日志, 志查, 查看, 看方, 方法,
日志查看方法
}
chunk2:
系统参数配置说明
chunk2_token = {
系统, 统参, 参数, 数配, 配置, 置说, 说明
}
Step3:计算交集(score)
query vs chunk1
交集 = {查看, 日志}
score = 2
query vs chunk2
交集 = {}
score = 0
Step4:排序 + 选择
chunk1 = 2
chunk2 = 0
👉 chunk1 被召回
四、关键理解(重要)
通过这个手算过程,可以得出结论:
👉检索层不理解"意思",只看"词是否一样"
这意味着:
- "设置" ≠ "配置"
- "查看" ≠ "查询"
👉 即使意思一样,也可能完全匹配不到
五、检索层核心的三类问题
(测试重点)
1️⃣ 找不到:漏召回
典型的就是同义词问题。
👉 示例:
- 用户问:"如何配置参数"
- 文档写:"参数设置方法"
系统处理:
- 配置 ≠ 设置 ❌
- 只有"参数"命中
👉 score 很低甚至被拒绝
结论:
👉 意思对,但词不一样 → 找不到
2️⃣ 找错了:误召回
问题中包含常见词,导致错误匹配。
👉 示例:
- 用户问:报销车票
- 召回:购买车票 / 酒店预订
原因:
"车票"这个词匹配上了
但语义完全错误
结论:
👉 词对了,但内容错了 → 找错
3️⃣ 找不全:召回不完整
答案被拆在多个 chunk 中。
但:
top_k = 3
👉 只取前三个
问题:
- 关键 chunk 排第4 ❌
- 或只召回部分信息
结论:
👉 答案存在,但没全部进上下文 → 找不全
六、参数对检索结果的影响
1️⃣ top_k
默认:3
影响:
- 太小 → 漏召回
- 太大 → 噪声污染
2️⃣ min_score
默认:2
影响:
- 太高 → 正确结果被拒绝
- 太低 → 错误内容进入
👉 本质:
召回范围 vs 噪声控制 的平衡
七、一个关键结论
👉检索层决定了RAG的"上限"
因为:
- 没召回 → 一定答错
- 召回错误 → 一定答偏
- 召回不全 → 一定答不完整
模型再强,也无法弥补:
👉 "没喂对数据"
八、小结
这一层真正要理解的不是代码,是:
- 怎么拆词
- 怎么算分
- 怎么排序
- 怎么被截断
所以:
检索层就是:把问题和文本拆成词,按重合度排序,取最相关的几个。
简洁说明版:
在这个项目里,我重点分析了RAG的检索层实现。
这套实现不是向量检索,而是基于关键词匹配:
先把用户问题和每个chunk都做分词,然后通过计算词项重合数量进行打分排序,最后取top_k个结果进入后续流程。
基于这个机制,我主要关注三类核心问题:
第一是漏召回 ,比如同义词问题,"配置"和"设置"词面不同,可能完全匹配不到;
第二是误召回 ,问题里包含一些通用词,导致召回了看似相关但实际错误的内容;
第三是召回不完整,比如答案分布在多个chunk中,但由于top_k限制或排序问题,关键内容没有被全部召回。
所以在测试时,会重点验证召回是否准确、是否完整,以及在不同表达方式下的稳定性,确保后续生成阶段有正确的输入。