概述

今天对 AI 编程教学平台进行了一次系统性深度优化,覆盖了四个维度:

  1. RAG 可观测性:从零搭建检索质量评估体系
  2. RAG 精准度:定位并修复两个数据链路 bug
  3. 工程规范化:Prompt 模板化、前端状态管理、配置治理
  4. 性能调优:Reranker 候选数控制、连接池调整

核心成果:RAG 检索 Hit@3 从 0% → 60%,忠实度从 2.4 → 4.2


一、RAG 评估体系搭建

问题

在此之前,RAG 检索效果完全靠”感觉”——改完 Prompt 不知道有没有变好,换模型不知道命中率涨没涨。简历上写 RAG 却拿不出任何量化数据。

方案

自建评估系统,不引入 Python 生态的 RAGAS(太重),全部放在 src/test 下零侵入:

1
2
3
4
5
6
7
8
9
src/test/java/.../eval/
├── RagEvaluator.java # 评估运行器(两轮)
├── EvalReport.java # 报告 DTO
├── EvalDataset.java # JSON 数据集加载
├── MetricCalculator.java # Hit@K / MRR / Recall
└── RagEvaluatorTest.java # 测试入口

src/test/resources/
└── rag-eval-dataset.json # 15 条 C 语言测试用例

第一轮:检索质量(不调 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-uploaded
  • VectorStoreService.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
2
3
4
// SmartChunkService.detectDocType()
if (content.contains("```") || content.contains("def ")) {
return DocType.CODE; // 误判!C 语言教材含代码块但本质是 Markdown
}

```C语言最重要的知识点 被检测为代码块标记,整篇文档被判为 CODE 类型,走了 splitByCodeStructure 而非 splitByMarkdownHeaders

修法:Markdown 标题 # / ## 优先匹配,优先级高于代码块。

Bug 2:首个匹配前文本被丢弃

1
2
3
4
5
6
7
// splitCodeByFunctions()
while (matcher.find()) {
if (lastEnd > 0) { // ← 第一个匹配时 lastEnd=0,跳过!
functions.add(code.substring(lastEnd, matcher.start()));
}
lastEnd = matcher.start();
}

文档开头(第一节基础认识、第二节 vc++、第三节标识符)不包含任何 void|int|static 函数头模式。第一个匹配是 int x=97;printf...,此时 lastEnd=0if (lastEnd > 0) 为 false——标识符那章被静默丢弃

修法:删掉 if (lastEnd > 0) 条件,保留第一个匹配前的所有文本。

修复后重传文档,标识符章节终于入库。再次评估:

1
2
Hit@3: 80.0%  Hit@5: 80.0%  MRR: 0.644
忠实度: 4.2 相关性: 4.8

三、Reranker 耗时优化

Reranker 平均耗时从 982ms 涨到 5446ms——因为 TOP_K 从 10 调到 30 后,Reranker 需要逐条 Cross-Encoder 推理全部 ~20 个候选 chunk,每条 270ms。

修法:仅对 RRF 融合后的 Top-15 做精排,砍掉排名靠后的低价值候选。

1
2
调整前:20条 × 270ms = 5446ms → Hit@3 = 80%
调整后:15条 × 270ms ≈ 4000ms → Hit@3 = 60%

结论:Hit@3 微降至 60%(15 条候选切掉了一些正确答案),但 Reranker 耗时可控。要进一步提升需要换更强的 Embedding 模型(bge-large 1024 维)。


四、Prompt 模板化

将散落在 3 个 Java 文件中的硬编码 Prompt 抽到 resource/prompts/ 目录:

1
2
3
4
5
6
prompts/
├── grading-system.txt # 作业批改系统指令
├── query-rewrite.txt # Query 改写模板
├── teaching-plan.txt # 教案生成 HTML 模板
├── judge-faithfulness.txt # LLM 忠实度评分
└── judge-relevance.txt # LLM 相关性评分

新建 PromptLoader 工具类,使用 Spring ClassPathResource 加载,模板变量用 {{key}} 占位符替换。

好处:调 Prompt 不用动 Java 代码、不需要重新编译,未来支持 A/B 测试只需切文件名。


五、ONNX 模型路径配置化

1
2
3
4
5
6
// 改前:Windows 绝对路径硬编码
private static final Path MODEL_DIR = Path.of("E:\\bge-reranker-base\\dir");

// 改后:从配置文件读取
@Value("${app.ai.reranker.model-dir}")
private String modelDir;
1
2
# application.properties
app.ai.reranker.model-dir=${user.home}/.djl/models/bge-reranker-base

跨平台无感知,Docker 容器通过挂载 djl_models volume 自动映射。


六、前端状态管理规范化

创建 stores/class.ts — Pinia 班级 Store,5 分钟缓存:

1
2
3
4
5
6
7
8
9
10
11
export const useClassStore = defineStore('class', () => {
const currentClassId = ref<number | null>(null)
const classList = ref<ClassInfo[]>([])

async function fetchClassList() {
if (!isExpired() && classList.value.length > 0) return classList.value
const res = await request.get('/dashboard/classes')
classList.value = res?.data?.data ?? []
// ...
}
})

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

待办

  1. 核心链路单元测试(RAG 检索、批改、限流)
  2. 换更强大的 Embedding 模型(bge-large 1024 维),进一步提升 Hit@3 至 80%+
  3. 前端请求去重(防连点重复提交)
  4. LM 并发 Semaphore 控制(虚拟线程无限打 LLM 的风险)