RAG 检索从失效到可评估:Embedding、pgvector 与 Reranker 排障实录
本文整合了两轮 RAG 改造:第一轮解决“向量并不真实”和 pgvector 维度问题,第二轮解决“系统能返回结果,但检索质量不可控”的问题。
排障链路全景
flowchart LR
A["课程文档"] --> B["解析与 Chunk"]
B --> C["ONNX Embedding"]
C --> D["pgvector 向量召回"]
B --> E["关键词召回"]
D --> F["RRF 融合"]
E --> F
F --> G["Reranker 精排"]
G --> H["Prompt 上下文"]
H --> I["LLM 回答"]
J["固定评估集"] -. "Hit@K / MRR" .-> D
J -. "忠实度 / 相关性" .-> I
概述
今天对 AI 编程教学平台进行了一次系统性深度优化,覆盖了四个维度:
- RAG 可观测性:从零搭建检索质量评估体系
- RAG 精准度:定位并修复两个数据链路 bug
- 工程规范化:Prompt 模板化、前端状态管理、配置治理
- 性能调优:Reranker 候选数控制、连接池调整
核心成果:RAG 检索 Hit@3 从 0% → 60%,忠实度从 2.4 → 4.2。
一、RAG 评估体系搭建
问题
在此之前,RAG 检索效果完全靠”感觉”——改完 Prompt 不知道有没有变好,换模型不知道命中率涨没涨。简历上写 RAG 却拿不出任何量化数据。
方案
自建评估系统,不引入 Python 生态的 RAGAS(太重),全部放在 src/test 下零侵入:
1 | src/test/java/.../eval/ |
第一轮:检索质量(不调 LLM,3 秒)— Hit@3 / Hit@5 / MRR / Recall
第二轮:生成质量(LLM-as-Judge,仅 5 条)— 忠实度 / 相关性
首次跑出结果:Hit@3 = 0%,忠实度 = 2.4——LLM 在裸答,RAG 完全没起作用。
二、Hit@3 = 0% 的全链路排查
排查 1:仪表盘数据污染
1 | [DEBUG] fused[2]: content="## 知识点掌握度\n- 其他: 100%" |
学情统计数据被写进了 document_chunk 表,检索时混入了大量无关结果。
根因:DashboardRagServiceImpl.uploadDashboard() 把结构化统计分块 + Embedding 化后存入向量库,但这些数据与知识问答场景完全无关。
修法:删掉仪表盘 RAG 上传逻辑(数据应通过 MCP 工具直接从业务表查询),清理 document_chunk 中所有 dashboard_% 数据。
涉及文件:
DashboardRagServiceImpl.java— upload 改空操作DashboardController.java— 删 POST/upload-to-rag+ GET/check-rag-uploadedVectorStoreService.java— 删existsToday()DashboardUploadScheduler.java— 定时任务跳过V12__cleanup_dashboard_chunks.sql— 清理历史数据
排查 2:SmartChunkService 双 Bug
修复后重新上传 C 语言文档,Hit@3 仍然是 0。查 DB 发现 92 个 chunk 里根本没有”标识符”那章——第一个 chunk 从 int x=97;printf 开始,前面四节全丢了。
Bug 1:文档类型误判
1 | // SmartChunkService.detectDocType() |
```C语言最重要的知识点 被检测为代码块标记,整篇文档被判为 CODE 类型,走了 splitByCodeStructure 而非 splitByMarkdownHeaders。
修法:Markdown 标题 # / ## 优先匹配,优先级高于代码块。
Bug 2:首个匹配前文本被丢弃
1 | // splitCodeByFunctions() |
文档开头(第一节基础认识、第二节 vc++、第三节标识符)不包含任何 void|int|static 函数头模式。第一个匹配是 int x=97;printf...,此时 lastEnd=0,if (lastEnd > 0) 为 false——标识符那章被静默丢弃。
修法:删掉 if (lastEnd > 0) 条件,保留第一个匹配前的所有文本。
修复后重传文档,标识符章节终于入库。再次评估:
1 | Hit@3: 80.0% Hit@5: 80.0% MRR: 0.644 |
三、Reranker 耗时优化
Reranker 平均耗时从 982ms 涨到 5446ms——因为 TOP_K 从 10 调到 30 后,Reranker 需要逐条 Cross-Encoder 推理全部 ~20 个候选 chunk,每条 270ms。
修法:仅对 RRF 融合后的 Top-15 做精排,砍掉排名靠后的低价值候选。
1 | 调整前:20条 × 270ms = 5446ms → Hit@3 = 80% |
结论:Hit@3 微降至 60%(15 条候选切掉了一些正确答案),但 Reranker 耗时可控。要进一步提升需要换更强的 Embedding 模型(bge-large 1024 维)。
四、Prompt 模板化
将散落在 3 个 Java 文件中的硬编码 Prompt 抽到 resource/prompts/ 目录:
1 | prompts/ |
新建 PromptLoader 工具类,使用 Spring ClassPathResource 加载,模板变量用 {{key}} 占位符替换。
好处:调 Prompt 不用动 Java 代码、不需要重新编译,未来支持 A/B 测试只需切文件名。
五、ONNX 模型路径配置化
1 | // 改前:Windows 绝对路径硬编码 |
1 | # application.properties |
跨平台无感知,Docker 容器通过挂载 djl_models volume 自动映射。
六、前端状态管理规范化
创建 stores/class.ts — Pinia 班级 Store,5 分钟缓存:
1 | export const useClassStore = defineStore('class', () => { |
PageFour、PageFive、TaskDetail 全部接入,页面切换不再重复请求班级列表。
七、其他优化
- HikariCP 连接池:20 → 30,适配异步消费者场景
- 安全配置:
/api/homework/**保持公开(学生免登录提交),但POST /submit+POST /bind-qq由 Bucket4j 限流兜底 - 删除代码路径:
existsToday()、仪表盘 RAG 上传、check-rag-uploaded
最终数据
| 指标 | 优化前 | 优化后 |
|---|---|---|
| Hit@3 | 0% | 60% |
| Hit@5 | 0% | 66.7% |
| MRR | 0.000 | 0.524 |
| 忠实度 | 2.4 | 4.2 |
| 相关性 | 5.0 | 4.8 |
| Reranker 耗时 | 982ms | 514ms |
| 向量检索 | — | 14.5ms |
待办
- 核心链路单元测试(RAG 检索、批改、限流)
- 换更强大的 Embedding 模型(bge-large 1024 维),进一步提升 Hit@3 至 80%+
- 前端请求去重(防连点重复提交)
- LM 并发 Semaphore 控制(虚拟线程无限打 LLM 的风险)
补全前史:先确认“向量真的是向量”
第二轮排障之前,系统还经历过一次更基础的升级:早期为了先跑通链路,用文本哈希结果伪装成向量。它能写入数据库,也能返回相似度,但并不表达语义,中文同义句几乎没有可解释的距离关系。
升级时做了四件事:
- 使用 ONNX Runtime 在本地运行真实 Embedding 模型;
- 固定分词、输入张量和池化流程,避免不同入口产生不同维度;
- 统一 pgvector 列维度、索引和查询参数;
- 为旧数据执行全量重建,不允许新旧向量混在同一集合里。
这里最容易误判的是“SQL 能执行,所以 RAG 没问题”。事实上,维度一致只代表数据形状正确,不代表语义正确。必须拿一组人工标注的问题和目标文档做最小评估集,才能判断 Embedding 是否真的有效。
一套可复用的 RAG 排障顺序
遇到检索效果差时,我现在按下面的顺序检查:
- 数据范围:是否按用户、课程和知识库隔离,过滤条件有没有在召回前生效;
- 文档质量:解析后的文本是否乱码、空白,Chunk 是否保留标题和上下文;
- 向量一致性:写入和查询是否使用同一模型、同一维度、同一归一化方式;
- 召回结果:先打印向量召回与关键词召回的原始 TopK,不急着看最终回答;
- 融合与精排:确认 RRF 和 Reranker 没有把正确结果重新排到后面;
- Prompt 与生成:只有检索结果正确之后,才检查上下文拼接和回答模板。
这个顺序把问题分成“没有检索到”“检索到了但排序错误”“上下文正确但模型答错”三类,避免一看到最终回答不好就反复修改 Prompt。
结论
RAG 的质量不是一个模型参数,而是一条数据链路的乘积。哈希伪向量、脏数据、错误过滤、召回数量、精排耗时和 Prompt 都可能成为短板。真正让系统从不可控变成可优化的,不是换了更大的模型,而是有了固定评估集、阶段指标和可以逐层观察的日志。






