RAG 项目里的检索与排序:为什么不能只靠向量检索?
这次做 RAG 检索与排序后,我最大的收获是:RAG 不是“把文档交给大模型回答”,而是一条检索、排序、证据判断、生成回答的工程链路。关键词检索:适合技术术语、接口名、文件名等精确匹配。向量检索:适合语义相似、表达方式不同的问题。混合检索:把两者结合起来,提高召回稳定性。证据门控:避免检索结果只是主题相关,但没有真实答案。如果只看最终回答,很容易忽略中间问题。为什么搜到这些 chunk?为什么这个
一开始做知识库问答时,我以为 RAG 的核心就是:
上传文档 -> 用户提问 -> 调用大模型 -> 返回答案
但真正把 RAG 接进项目后,我发现影响回答质量的关键不只是大模型,而是前面的检索与排序。
如果检索没有找到正确资料,后面的大模型再强也只能乱猜。
如果检索找到了很多资料,但排序不好,真正有用的 chunk 可能排不到前面。
如果向量检索返回了“看起来相关”的内容,但其实没有答案,模型还可能一本正经地胡说。
所以这篇文章主要复盘我在项目中围绕“检索与排序”遇到的问题、解决方案和后续优化方向。
1. 项目背景:为什么需要检索?
我的项目是一个聊天系统,后来接入了一个独立的 Python FastAPI AI 服务 RAG Agent。
具体链路是:
用户在群知识库上传文档
-> Java 后端校验群权限
-> Java 将文件转发给 Python AI 服务
-> Python 保存文件、计算 MD5,并写入文档元数据
-> 用户触发解析
-> Python 按文件类型读取文本并切分 chunks
-> chunks 写入 SQLite
-> 用户触发索引
-> Python 将 chunks 向量化并写入 Chroma
-> 用户在群聊中提问
-> 系统只检索全局知识库 + 当前群知识库
-> 关键词检索 + 向量检索召回相关 chunk
-> 混合排序和证据判断
-> 生成回答并返回来源
这里最关键的一步就是:
用户问题 -> 检索知识库 -> 找到真正有用的 chunk
如果这一步做不好,就会出现两个典型问题:
1. 知识库里明明有资料,但系统没搜到。
2. 系统搜到了一些相关内容,但这些内容其实不能支撑回答。
这两个问题都和检索与排序有关。
2. 第一个问题:知识库里有 Netty 文档,但问 Netty 没命中
项目里我遇到过一个很典型的问题:
知识库中 netty 是什么?
知识库里明明已经上传了 Netty 相关文档,但系统一开始没有稳定命中。
这个问题让我意识到:只靠一种检索方式是不够的。
2.1 关键词检索的优点
关键词检索很好理解,就是看用户问题里的词有没有出现在 chunk 里。
比如问题是:
知识库中 netty 是什么?
如果某个 chunk 里出现了:
Netty 是一个异步事件驱动的网络应用框架
那么这个 chunk 就应该被搜出来。
关键词检索特别适合这些内容:
Netty
WebSocket
API
接口路径
类名
文件名
字段名
这些词不是靠“语义理解”就一定能稳定命中的,它们本身就是精确术语。
2.2 关键词检索的问题
但关键词检索也有缺点。
如果用户换一种说法,而文档里没有相同词,就可能搜不到。
比如文档里写的是:
RAG 评测指标包括 source_hit_rate 和 keyword_hit_rate
用户问:
怎么判断知识库问答效果好不好?
这时用户没有直接说 source_hit_rate,关键词检索可能就不稳定。
所以关键词检索适合精确词,但不适合所有语义问题。
3. 第二个问题:向量检索能召回语义,但不一定有答案
后来我接入了 Chroma 向量检索。
向量检索的思路是:
问题 -> Embedding 向量
chunk -> Embedding 向量
比较两个向量的语义距离
返回最相似的 chunks
它的优点是能处理“换一种说法”的问题。
比如用户问:
怎么证明 RAG 效果变好了?
文档里可能写的是:
通过 case_pass_rate、source_hit_rate、keyword_hit_rate 评估效果
虽然用户问题和文档原文不完全一样,但向量检索有机会命中。
但是,向量检索也带来了一个新问题:
最相似,不等于有答案。
比如用户问:
生产环境 IP 是什么?
知识库里其实没有生产环境 IP。
但向量检索可能会返回一些“部署”“服务”“环境”相关的 chunk。它们和问题主题相似,但没有真正回答 IP 是多少。
如果这时候直接把这些 chunk 交给大模型,模型可能会开始编造。
所以我后来加了证据判断:
检索结果相关
不代表
证据足够
对于“生产环境 IP”“公司内部薪资”“云服务器地址”这类精确事实问题,必须真的命中关键字段,否则宁可拒答。
4. 解决方案:关键词 + 向量混合检索
最后我把项目里的检索改成了混合检索。
核心思路是:
关键词检索
+ 向量检索
-> 合并
-> 去重
-> 加权排序
-> 得到最终 chunk
项目里的主流程可以简化成这样:
def run_hybrid_rag_search(question, top_k):
keyword_items = keyword_search(question, top_k)
vector_items = vector_search(question, top_k)
if vector_items_has_enough_evidence(vector_items):
final_items = merge_keyword_and_vector_items(
keyword_items,
vector_items,
top_k
)
return final_items
if keyword_items_has_enough_evidence(keyword_items):
return keyword_items
return []
这套逻辑背后的判断是:
1. 向量检索负责语义召回。
2. 关键词检索负责精确术语召回。
3. 两路都命中的 chunk 更可信。
4. 如果证据不足,不进入回答阶段。
5. 关键词是怎么提取的?
项目中有一个细节很重要:用户可能不会用空格分词。
比如:
知识库中netty是什么?
如果只按空格切分,这句话会被当成一个整体:
知识库中netty是什么?
这样很难命中文档中的 Netty。
所以我做了一个简单的英文技术词提取:
def extract_ascii_search_terms(query):
return re.findall(r"[a-zA-Z][a-zA-Z0-9_.+-]*", query)
这样可以从中文句子里提取出:
netty
然后关键词检索时会同时使用:
完整问题
分词结果
英文技术词
这样对中英文混合技术问题更友好。
6. 关键词怎么打分?
学习版关键词打分比较简单:
def calculate_keyword_score(query, terms, content):
score = 0
if query in content:
score += 2
for term in terms:
if term in content:
score += 1
return score
也就是说:
完整问题命中,加 2 分
单个关键词命中,加 1 分
分数越高,排序越靠前
这个版本不复杂,但对初学阶段很有帮助,因为它足够直观,也容易调试。
比如问:
知识库中 netty 是什么?
如果 chunk 里包含 netty,它就会被排到前面。
7. 向量分数到底是什么?
向量检索这里很容易误解。
Chroma 原始返回的不是 score,而是 distance。
distance 越小,表示问题和 chunk 越相似。
但在项目里,为了方便后续和关键词检索一起排序,我把 distance 转成了一个“越大越相关”的整数分数:
score = int(10000 / (1 + max(distance, 0.0)))
也就是说:
distance 越小
-> score 越大
-> 相关性越高
举几个例子:
distance = 0 -> score = 10000
distance = 0.3 -> score ≈ 7692
distance = 0.9 -> score ≈ 5263
distance = 1.0 -> score = 5000
这里的 score 不是准确率,也不是百分制,它只是项目内部为了排序设计的相关性分数。
后面做混合排序时,还会再把这个分数压缩一下:
def normalize_vector_score(raw_vector_score):
return max(1, min(raw_vector_score // 10, 1000))
比如:
raw score = 7692 -> normalized score = 769
raw score = 5263 -> normalized score = 526
raw score = 10000 -> normalized score = 1000
这样做是为了避免向量分数太大,直接压过关键词分数。
8. 向量结果怎么和关键词结果合并?
混合检索里一个关键问题是:
如果同一个 chunk 同时被关键词和向量命中,应该怎么办?
我的做法是按 chunk_id 去重。
原因是同一个 chunk 不应该重复塞进 prompt。重复内容会浪费上下文,也可能干扰模型。
合并逻辑可以简化成:
merged = {}
for item in vector_items:
merged[item.chunk_id] = normalize_vector_score(item.score)
for item in keyword_items:
keyword_score = item.score * HYBRID_KEYWORD_SCORE_WEIGHT
if item.chunk_id in merged:
merged[item.chunk_id] += keyword_score
else:
merged[item.chunk_id] = keyword_score
return sort_by_score(merged)
这样做的好处是:
1. 同一个 chunk 不会重复出现。
2. 同时被关键词和向量命中的 chunk 会加分。
3. 最终排序更稳定。
这也符合直觉:
如果一个 chunk 既语义相似,又包含关键术语,
它大概率比只命中一路的 chunk 更可靠。
例如:
关键词分数 = 2
关键词权重 = 1200
向量原始分数 = 9000
向量归一化分数 = 900
最终合并后:
关键词部分:2 * 1200 = 2400
向量部分:900
最终分数:3300
这样可以让精确关键词命中的内容有更高权重,同时保留向量检索带来的语义召回能力。
9. 第三个问题:检索结果有了,但答案还是不好
做完混合检索后,我又遇到一个问题:
检索结果有了,但最终回答仍然不稳定。
原因可能有很多:
1. top_k 太小,真正有用的 chunk 没进来。
2. top_k 太大,引入太多噪声。
3. chunk 切分太碎,答案被切断。
4. 关键词结果和向量结果分数权重不合理。
5. 向量结果主题相关,但证据不足。
6. 证据门控过严,把本来能回答的问题拒掉了。
所以我后来做了一个 RAG 调试面板。
调试面板会展示:
问题类型
关键词
证据词
关键词检索结果
向量检索结果
最终片段
是否进入回答阶段
拒答原因
这一步非常重要。
因为 RAG 系统不能只看最终答案,要能解释:
为什么命中了这些 chunk?
为什么这个 chunk 排第一?
为什么最后拒答?
为什么没有进入大模型回答?
这才是工程化 RAG 和普通 demo 的区别。
10. 检索效果不好时,我怎么排查?
现在如果遇到“知识库里明明有内容,但 AI 没回答”,我会按下面顺序排查:
1. 问题是否真的在知识库范围内?
2. 文档是否已经上传成功?
3. 文档是否已经解析成 chunk?
4. chunk 是否已经写入 SQLite?
5. chunk 是否已经构建向量索引?
6. 当前群聊 scope 是否正确?
7. 关键词是否提取到了核心术语?
8. 向量检索 top_k 是否太小?
9. 混合排序后,相关 chunk 是否被压低?
10. 证据门控是否过严?
这里的 scope 也很重要。
在群聊场景中,A 群不能查到 B 群的知识库,所以检索范围必须限制在:
全局知识库 + 当前群知识库
如果 scope 错了,也会出现“明明上传了文档但搜不到”的问题。
11. 这次优化带来的收获
这次做完混合检索后,我对 RAG 的理解发生了变化。
以前我更关注:
模型能不能回答?
现在我更关注:
模型回答前拿到了什么资料?
这些资料是不是正确来源?
这些资料是否真的足够支撑回答?
最终排序为什么是这样?
这几个问题比“模型输出看起来像不像”更重要。
因为 RAG 的可靠性不是只来自模型,而是来自完整链路:
文档解析
-> chunk 切分
-> 关键词检索
-> 向量检索
-> 混合排序
-> 证据判断
-> Prompt 构造
-> 来源引用
其中任何一环出问题,最后回答都可能不稳定。
12. 后续优化方向
目前这套混合检索还是学习版,后续可以继续从下面几个方向优化。
12.1 引入 rerank
现在项目里的排序主要靠关键词分数和向量分数加权。这个方式简单、可解释,但它本质上还是规则排序。
后续可以加入 rerank。
rerank 可以理解为“二次排序”。
第一阶段先用关键词检索和向量检索召回一批候选 chunk,比如取前 20 个。
第二阶段再用一个更精细的模型或规则,对这 20 个 chunk 重新判断:
哪个 chunk 最能回答用户问题?
哪个 chunk 只是主题相关,但没有直接答案?
哪个 chunk 应该排在最前面?
简单来说:
第一阶段:召回
目标是尽量别漏掉可能相关的 chunk。
第二阶段:重排
目标是从候选 chunk 里挑出最有用、最能支撑回答的 chunk。
比如用户问:
Netty 和 WebSocket 有什么关系?
第一阶段混合检索可能召回:
1. Netty 是什么
2. WebSocket 是什么
3. Netty 如何支持 WebSocket
4. Java 网络编程基础
5. 服务器部署说明
这些内容都可能有点相关,但真正最有用的是:
Netty 如何支持 WebSocket
rerank 的作用就是把这种更能直接回答问题的 chunk 排到前面。
所以,rerank 不是替代关键词检索和向量检索,而是在它们召回候选结果之后,再做一次更精细的排序。
当然,rerank 也不是没有成本。它通常会增加额外的模型调用或计算开销,所以更适合在知识库变大、候选 chunk 较多、排序质量要求更高时引入。
12.2 基于评测集调整权重
目前关键词权重和向量权重主要是规则设置的。
后续可以通过评测集来调参:
不同权重组合
-> 跑同一批 RAG case
-> 对比 source_hit_rate、keyword_hit_rate、no_answer_accuracy
-> 选择整体效果更好的权重
这样比凭感觉调参数更可靠。
12.3 扩大检索评测集
现在的评测 case 数量还比较少,更像是验证核心链路。
后续应该扩展更多真实问题:
概念解释题
精确事实题
总结归纳题
无依据拒答题
中英文混合技术题
跨 chunk 问题
只有评测集变丰富,才能真正判断检索排序有没有变好。
12.4 完善检索调试能力
项目里已经有 RAG 调试面板,可以看到关键词检索结果、向量检索结果、最终片段和证据判断原因。
后续可以继续展示更细的检索排序信息,比如:
每个 chunk 的关键词分数
每个 chunk 的向量分数
合并后的最终分数
排序前后的变化
证据门控判断原因
这样当某个问题没有命中时,可以更快判断是关键词提取问题、向量召回问题、排序权重问题,还是证据门控问题。
12.5 补充 chunk 切分测试
项目里已经做过第一轮 chunk 切分优化,包括:
Markdown 结构感知切分
长表格按行分组
每个表格 chunk 保留表头
相邻 chunk 补全
后续重点不是从零开始优化 chunk,而是补充测试,保证后续继续改检索和切分策略时,不会破坏已有能力。
比如测试:
Markdown 标题不会被错误合并到上一个 chunk
代码块尽量保持完整
短表格不会被拆开
长表格会按行分组
每个长表格 chunk 都保留表头
13. 总结
这次做 RAG 检索与排序后,我最大的收获是:
RAG 不是“把文档交给大模型回答”,而是一条检索、排序、证据判断、生成回答的工程链路。
关键词检索、向量检索、混合排序各有作用:
关键词检索:
适合技术术语、接口名、文件名等精确匹配。
向量检索:
适合语义相似、表达方式不同的问题。
混合检索:
把两者结合起来,提高召回稳定性。
证据门控:
避免检索结果只是主题相关,但没有真实答案。
如果只看最终回答,很容易忽略中间问题。
真正可靠的 RAG 系统,应该能回答这些问题:
为什么搜到这些 chunk?
为什么这个 chunk 排在前面?
为什么这个问题可以回答?
为什么那个问题必须拒答?
优化后指标有没有变好?
所以,检索与排序不是 RAG 项目里的小细节,而是决定回答质量的核心环节。
对我来说,这次优化也让我从“会调大模型接口”,进一步理解到 AI 应用开发真正要做的是:
把模型能力放进一个可控、可解释、可评测的工程系统里。
14. 项目地址
如果你想看完整项目代码,可以参考:
- X-RAG Agent:https://github.com/Iterate101/X-RAG-Agent
本文主要涉及的代码文件:
X-RAG Agent/rag_service.pyX-RAG Agent/knowledge_service.pyX-RAG Agent/api.py
项目还在持续完善中,当前版本更偏学习和实习作品,重点是把 RAG 的文档入库、检索排序、证据判断和调试链路跑通。
更多推荐


所有评论(0)