我把 RAG 召回从 57.6% 做到 78.8%,然后发现 75% 的真实问题它答不了

一个专升本考生做的政策问答系统。从"指标很好看"到"用户说能用的地方不多",我花了三周才搞明白: 我一直在优化一个自己幻想出来的需求。


一、为什么做这个

我是 2027 届专升本考生,河南。备考时有个痛点:政策文件太散,报名时间、考试科目、免试条件、投档规则散在教育厅通知和几十所院校的招生章程里,用搜索引擎找不准,问同学也不权威。

所以我想做一个政策问答机器人:输入问题,返回答案 + 引用出处。

技术上选 RAG(检索增强生成):

  • 语料:河南省教育考试院政策文件 + 院校招生章程,142 个文本块 / 8.4 万字
  • 向量库:Qdrant + BGE-M3(1024 维)
  • 生成:DeepSeek-V3
  • 后端:FastAPI
  • 部署:腾讯云香港,2 核 2G

二、三个坑,每个都让我推翻之前的结论

坑 1:三个 Qdrant 集合,导致我一周半的结论是错的

现象:第三周我做了个实验,结论是"rrf混合检索没用(query语义用向量更好),BM25 + 向量加起来不如只用向量"。数据摆在那:混合 47.1%,向量 47.1%,一样。我信了一周半。

转折:第四周做别的实验时顺手查了向量库:

bash 复制代码
curl http://localhost:6333/collections

发现同时存在三个集合:

  • chunks:139 个点
  • ls_rag_docs_clean:2403 个点
  • ls_rag_docs_clean_200:200 个点

而我三个评估脚本,各指一个集合。

根因:我过去三周所有的"对比实验",比的其实是三个不同的知识库。

修复 :统一到 chunks(139 点),point_id 用 uuid5(NAMESPACE_URL, chunk_id),payload 存 chunk_id。

结论:同源验证不做,所有对比实验都是假的。


坑 2:id 空间不一致,RRF 一直在空转

现象:混合检索"看起来在跑",但效果始终等于单路。

排查 :打了日志,发现 BM25 返回的 id 是 17、16、89 这种整数,向量返回的是 e7061bb895c2 这种哈希。

根因:

python 复制代码
# 错误代码
chunks[idx].get("chunk_id", idx)

我的字段名叫 id,这里写的却是 chunk_id,取不到就静默回落成数组下标。

所以 RRF 一直在融合两个完全不相交的 id 集合:同一个块,在 BM25 里叫 17,在向量里叫 e7061bb895c2,被当成两个不同实体。

修复 :强制只认 id/chunk_id/chunkId,缺失就抛异常,不回落下标。

结论 :静默兜底比崩溃更危险。


坑 3:30.5% 的评估要点,语料里根本没有

现象 :要点覆盖率一直上不去(38.3%),归因全是 miss_recall(检索没捞到)。

排查:我逐个要点去全语料里搜 4-gram,结果:

erlang 复制代码
118 个非拒答要点中,36 个(30.5%)语料根本没有

也就是说,我一直在为不存在的答案扣分。

修复:分母从 128 改成 80(语料可支撑要点),4 道不可答题从分母排除。

结论 :"检索失败"和"知识库缺失"必须分开量化,否则会优化错方向。


三、修完坑,指标终于能看了

数据处理

阶段 块数 重复 噪声
原始 200 9 组重复 id 15 条导航/新闻列表
清洗后 139 0 4.3%
补语料后 142 0 4.3%

清洗做了什么:

  • 按 id 去重
  • 噪声块过滤(导航、页脚、日期列表)
  • 表格整表合并(11 个碎片 → 10 张整表)
  • 分数线表碎片合并(40 个碎片 → 4 张整表,剥掉重复模板头)

检索效果(块级 recall@5)

配置 recall@5 说明
向量单路(原始基线) 19/33 = 57.6% 起点
+ 元数据过滤 24/33 = 72.7% 主力,+15.1pp
+ 多路 RRF 24/33 = 72.7% 无增量
+ rerank 26/33 = 78.8% +6.1pp,但是延迟 5.5 倍
BM25 单路 13/33 = 39.4% ---
query 改写 25/33 = 75.8% 与 rerank 能力重叠,无用

元数据过滤为什么有效 :142 块语料里 125 块是院校招生章程,模板句("录取分数线""录取规则")在纯语义检索下会挤占真正的政策块和分数线块。按 doc_type 路由后,policy 只剩 10 块、score 只剩 7 块,精度直接上去。


四、然后我收集了 20 条真实用户提问

这是整个项目最重要的转折点。

我在 QQ 群、贴吧、知乎、小红书、百度知道上,原话抄了 20 条真实考生的提问,然后逐条判断:我的系统能回答吗?

结果

判定 条数 占比
能答 1 5%
部分可答 4 20%
不能答 15 75%

只有 1 条能答。

答不了的分为两类:

75% 是超出范围:

  • "英语60出头冲100现实吗"(备考策略)
  • "考点附近酒店什么时候订"(事务办理)
  • "郑州哪里有自习室"(生活信息)
  • "为了公办再战一年值不值"(个人决策)

剩下是语料缺失:

  • "跨专业要交什么材料"(缺材料清单)
  • "平行志愿会滑档吗"(缺退档细则)
  • "护理学能填报哪些学校"(缺招生计划)

这才是最重要的发现

我的 33 道评估题,全是我按语料编的。

我当初的做法是:翻语料,看到有什么内容,就出一道对应的题。所以 题目天然都在语料覆盖范围内。

这个评估集测的是"我设计的题",不是"用户真正问的题"。

72.7% 这个数字,是系统性高估的。

还有一个细节很有意思:那唯一能答的 1 条,是"免试是不是不用考试"。

  • BM25 答不上(0/1)------ 用户说"不用考试",原文写"综合考查",词面完全不重合
  • 向量答得上(1/1)------ 语义能理解

而我的 20 条真实提问里,70% 是口语化的。这说明如果线上用纯 BM25,或者 BM25 权重太高,对真实用户基本失效。


五、所以我做了三件事

1. 建"拒答"指标

那 15 条答不了的问题,系统应该正确说"未找到相关信息",而不是编造。

建了 19 条拒答测试集(16 条真实提问 + 3 条原评估集),实测:

分组 结果
超范围(14 条) 14/14 = 100%
语料缺失(5 条) 4/5 = 80%
合计 18/19 = 94.7%

"承认不知道"和"答对"同等重要。

2. 收窄产品定位

用户想知道的 占比 系统能做什么
政策/资格(报名、考试、投档、免试) ~40% ✅ 部分能答
备考策略 ~25% ❌ 不属于本产品
事务办理 ~20% ❌ 不属于本产品
院校体验 ~15% ❌ 不属于本产品

结论:这是政策问答机器人,不是备考助手。 超范围的问题就该拒答,并在页面上写清楚。

3. 把三组数字一起报

指标 数值 说明
core(我编的题) 24/33 = 72.7% 基线
user(真实提问) 1/20 = 5% 可答率 真实场景
拒答准确率 18/19 = 94.7% 边界能力

这三组放在一起,比单看 72.7% 诚实得多。


六、工程部分:3 个 API + 流式 + 部署

API 设计

bash 复制代码
POST /chat          { question, mode: "fast"|"high" } → { answer, sources[], ... }
POST /search        { query, top_k, filters } → { hits[] }
GET  /health        → { status, chunks_count, same_source, ... }
POST /chat/stream   → SSE 流式

关键设计:

  • 启动时强制同源校验:Qdrant 点数 must == 文件块数,不一致返回 503(不是返回空结果)
  • 每条结果带 in_corpus 标记:便于排查 id 对不上的问题
  • 两种模式:fast(过滤+向量,72.7%,¥9.86/千次)/ high(+rerank,78.8%,¥19.82/千次)

流式输出

用户反馈"等 6.7 秒以为卡死了",所以做了 SSE。

指标 非流式 流式 改善
首字延迟 6675ms 1577ms 4.2×
来源展示 6675ms 736ms 9.1×

事件设计:

csharp 复制代码
event: meta   → 来源 + 检索耗时(用户马上看到出处)
event: token  → 逐字
event: done   → 首字延迟 / 总耗时 / 字数

关键认知 :延迟的 90% 来自 LLM 生成,不是检索。所以优化方向是流式,不是优化检索。

部署

腾讯云香港轻量(免备案),Docker + Qdrant + systemd + Nginx。

踩的坑:

  • 阿里云 1GiB 内存装 Docker 时 SSH 被 OOM 杀掉 2 次 → 换 2GiB
  • 腾讯云默认 ubuntu 用户,密钥配错了目录 → 用户和密钥必须对应

七、用真实用户测,又发现了新问题

找了 2 个专升本考生实测(一个计算机网络技术、一个护理)。

他们说

测试者 一句话评价
T1 "初窥门径"
T2 "能用的地方不多"

发现的问题(我完全没想到的)

🔴 材料N 编号悬空:

我的 API 返回了 sources[] 数组,字段齐全。但 LLM 在答案里写"见材料1",而来源卡片没有编号。

两个测试者独立提出:"不知道哪来的材料1"

教训:数据完整 ≠ 用户理解。 给结构化数据不加编号对应,等于给了半套方案。

🔴 "我能报什么专业"被答成分数线汇总:

T2 问"我能报考什么专业",系统答了"所有专业及其分数线"。

她说:"啊?不记得我是什么专业了么?"

根因 :没有加入记忆,用户要的是"专科专业 → 本科专业对照关系",另一方面我的语料里根本没有这张对照表。

🟠 其他问题:

  • 拒答行为不一致(同类问题有时长篇解释有时直接拒答)
  • 输入框发送后不清空
  • 来源片段被截断(只显示 200 字)

修复(只改 UI,不碰 RAG)

7 项 UI 修复:来源加 [1][2] 编号、拒答加黄色标签、顶部加能力说明、输入框自动清空、片段 200→400 字 + 展开全文、标题居中、chunk_id 加悬停提示。

⚠️ 关键纪律:RAG 已经在 W6 冻结,不改检索逻辑。 因为这些 UI 问题改了算法,我所有的历史数字都会作废。


八、我学到的最重要三件事

1. 评估集是自己编的,测的是想象力不是需求

从 72.7% 到 5% 可答率,这个差距不是"算法不好",是需求理解错了。

采集真实数据的成本很低(一周,每天 5 分钟),但不做的代价很高。

2. "没报错但数据错了"是最难发现的问题

三个坑:

  • 三个 Qdrant 集合(没报错,只是数字不对)
  • get("chunk_id", idx) 静默回落(没报错,只是 id 不对)
  • PowerShell 自动加 UTF-8 BOM(没报错,只是块数少了 1)

这类 bug 比崩溃难查十倍。 防御方法:关键标识缺失就抛异常,绝不静默兜底。

3. 会说出"我不知道"比会说"我知道"更重要

94.7% 的拒答准确率,意味着系统在 19 个超范围问题里,18 个能正确说"未找到"。

在一个政策问答场景里,编造一个答案的代价,远高于说"我不知道"。


九、目前的状态

能用的:网站(额度小就不展示了)(流式问答,首字 1.6 秒)

指标:

  • core(我编的题):24/33 = 72.7%
  • user(真实提问):1/20 = 5% 可答率
  • 拒答:18/19 = 94.7%

已知缺什么:

  • 院校投档线("230分能上公办吗"答不了)
  • 专业对照表("我能报什么专业"答不了)
  • 招生计划("护理学能填哪些学校"答不了)

下一步:Agent(W8-W11),让系统能查手册、算分数、并行调用多个数据源。


十、给同样在做 RAG 的人三句话

  1. 先验证数据同源,再做对比实验------三个集合能让你一周半的结论作废
  2. 自己编的评估集不可信------去抄 20 条真实用户提问,你会看到一个完全不同的分数
  3. "我不知道"是个功能,不是 bug------尤其在对准确性要求高的场景

项目地址:GitHub ([github.com/frdomis/ls-...](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Ffrdomis%2Fls-rag "https://github.com/frdomis/ls-rag")) 技术栈:Qdrant + BGE-M3 + DeepSeek-V3 + FastAPI + Docker + Nginx 数据来源:河南省教育考试院公开政策文件

相关推荐
用户425693999962010 分钟前
我的 HashMap 查找全废了,但 36 个测试全绿
后端
小番茄程序猿16 分钟前
Redis 编码压测 59 轮:HSET 快 23 倍是假象,SMEMBERS 慢 30 倍是真的
redis·后端
Moment1 小时前
代码都能让 Agent 写了,我们为什么还要学前端?
前端·后端·面试
AI入职记1 小时前
Spring AI 实测:不挂工具,模型真的会瞎编吗?
后端·ai编程
wei_shuo1 小时前
SQL Server数据迁移实战:内置故障诊断工具与根因分析实践
后端
一条Go1 小时前
告警指纹:一次故障为什么只产生一条 FIRING 记录
后端·架构·github
鶴哥只手遮天1 小时前
《Redis 8.10.3 深度剖析》系列 ·第 13 章 Set:三种编码、集合运算与随机采样
redis·后端·开源
王中阳Go1 小时前
你把重试加到 2 次那天,200 条里成功还是 181 条,你回看过吗?
人工智能·后端·架构