AI 教学平台 — 从 RAG 到 Agentic RAG 的全链路实战与面试复盘
项目亮点
本项目是一个基于 Spring Boot + Vue 3 的 AI 教学平台,核心亮点:
- 自建 RAG 全链路:本地 ONNX Embedding(bge-small)+ pgvector + 双路检索(向量 + 关键词)+ RRF 融合 + Reranker 精排 + Query Rewrite,不是调包 Demo
- Agentic RAG:通过 MCP Tool Server 注册 5 个工具,LLM 自主决策检索知识库、查成绩、看作业、查课表、获取时间
- 本地模型部署:DJL + ONNX Runtime + bge-small-zh-v1.5(Embedding)+ bge-reranker-base(Reranker),多源镜像自动下载 + 哈希降级
- 智能分块策略:文档类型检测 → 结构切割 → 语义边界切割 → 滑动窗口 → 上下文引用
- DAG 工作流引擎:GRADE → ERROR_ANALYSIS → SUGGESTION,条件路由 + 失败回退 + 类型安全状态
- 分布式基础设施:Bucket4j 令牌桶限流 + 布隆过滤器防穿透 + Cache-Aside 防击穿 + Redis Stream 异步批改
- 多端覆盖:Web 聊天(SSE/Flux)+ OneBot QQ 群机器人
- 可观测性:轻量 RagTrace,每步耗时/召回/Top-N 结果结构化日志
面试话术
开场(1 分钟自我介绍)
“我做过一个 AI 教学平台,核心是自建了一套完整的 RAG 管线。从本地的 ONNX Embedding 模型部署,到 pgvector 向量检索,到 RRF 多路融合和 Reranker 精排,再到 MCP Tool Server 实现 Agentic RAG——LLM 可以自己判断什么时候查知识库、什么时候查学生成绩。整套链路不是调包跑的,是我从 Embedding 层到检索策略到基础设施全部都自己写过的。”
为什么不用 LangChain(必问题)
“LangChain 封装层太重,很多内部行为不可控。我选用的是 LangChain4j 只做 OpenAI 协议兼容的薄薄一层,核心的 Embedding、检索、融合、精排全自己实现。这样出了问题我能定位到具体哪一步,而不是翻 LangChain 源码。而且 ONNX 本地部署、pgvector、RRF 这些本身就不依赖任何 AI 框架。”
为什么用 ONNX 不用 PyTorch(必问题)
“目标场景是 CPU 推理。PyTorch 依赖太重,ONNX Runtime 跨平台、无 GPU 依赖、启动快。bge-small 512 维 30ms,bge-reranker-base 80ms,加上 LLM 流式生成总延迟在 5s 内,CPU 完全不是瓶颈。多源镜像下载 + 哈希降级保证了模型获取的高可用。”
为什么用 RRF 不用线性加权(加分题)
“向量相似度 0.92 和关键词匹配 17 分不在同一量纲,线性加权需要归一化,归一化函数怎么选、权重怎么调都是坑。RRF 只关心排名不关心绝对值——
score = Σ 1/(k+rank)——k=60 的平滑常数让排名靠后的结果仍有非零贡献。学术界验证过的方案比我自己拍脑袋的权重靠谱。”
MCP 和 Function Calling 的区别(加分题)
“MCP 是 Anthropic 提出的 Model Context Protocol,标准化了工具注册、发现、调用协议。Function Calling 绑定 OpenAI 生态,MCP 不绑定任何 LLM 厂商。我实现了 JSON-RPC 协议——initialize/tools/list/tools/call——OpenClaw Gateway 发现我的 MCP Server 后自动注册为 Agent 可用工具。LLM 自主决定什么时候调哪个工具,这是 Agentic RAG 的核心。”
如果知识库到百万级怎么扩展(系统设计题)
“pgvector 支持 HNSW 索引,百万级向量检索在 10ms 内没问题。再往上考虑:① 分库分表按 userId 做 tenant 隔离;② 粗排用 pgvector,精排本地 Reranker,把精排候选集控制在 20 条内;③ 再往上考虑换 Milvus/Qdrant 这种专用向量数据库。”
模型效果怎么评估(加分题)
“我用了一个轻量 RagTrace 做可观测——每一步的耗时、召回量、精排 Top-N 的文档名和分数全记录为结构化 JSON。这个设计对齐了 RAGAS 的评估思路:Context Precision、Context Recall。后续规模化时可以直接接到 LangFuse,Trace 数据模型是一致的。”
今日工作内容
1. 项目诊断与 RAG 升级方案制定
从 AI 应用工程师视角分析了项目现状,识别出核心短板:
- RAG 检索策略单一:向量和关键词两路各自取 TopK,无交叉融合
- 缺少 Reranker 精排
- 缺少 Query Rewrite
- Embedding 降级方案脆弱
- Prompt 无来源标注
- 流式调用不稳定(LangChain4j 兼容问题)
- 无 LLM 可观测性
2. RRF 融合实现
新建文件:rag/RrfFusionService.java(~30 行)
1 | RRF_score(chunk) = Σ 1 / (k + rank_i) |
向量路和关键词路按各自排名融合,双路都命中的 chunk 自然排前,单路命中的不会丢失。
1 | for (int i = 0; i < vectorResults.size(); i++) { |
3. Reranker 精排实现
新建文件:rag/OnnxRerankerTranslator.java(40 行)、90 行)、rag/RerankerService.java(rag/QueryDocPair.java
以 bge-reranker-base(278M 参数)替代旧方案 bge-reranker-v2-m3(568M,CPU 跑太慢)。
- 模型路径:
E:\bge-reranker-base\dir\onnx\model.onnx - 架构:Cross-Encoder,query + document 拼接 → [CLS] token → sigmoid → 0~1 分数
- 与现有的
bge-small-zh-v1.5(Embedding)同厂商,tokenizer 逻辑一致,下载路径已通 hf-mirror.com
4. Prompt 来源标注升级
LLM 生成的回答要求引用来源编号 [1][2],同时在上下文组装时显式标注文档名:
1 | [1] 来源:排序算法讲义 |
5. 流式调用修复
问题:LangChain4j OpenAiStreamingChatModel 和 OpenClaw Gateway 的 SSE 握手协议不兼容,报 Invalid HTTP method。
解决方案:弃用 LangChain4j 流式模型,改用 Spring WebClient 直连 SSE。
1 | webClient.post() |
遇到的坑:Reactor Flux 不允许 map() 返回 null,filter(Objects::nonNull) 救不回来。改用 .handle() 一步搞定。
6. Query Rewrite 实现
新建文件:rag/QueryRewriter.java(~70 行)
短 query(<15 字)或含指代词(”上次那个””这道题”)时,用 LLM(temperature=0, max_tokens=50)改写成检索友好的关键词形式。内部静默调用,不走 session 记录。
1 | "上次那个数组的题怎么做" → "数组排序 冒泡排序 选择排序 代码实现" |
7. Agentic RAG 全线落地
KnowledgeSearchTool(MCP)升级为全管线:
1 | Query → Rewrite → Embedding → pgvector + ILIKE → RRF → Reranker → 格式化输出 |
LLM 通过 OpenClaw Gateway 的 MCP 协议自主调用此工具。前端聊天框和 OneBot QQ 群都不需要改。
8. 工作流引擎类型安全改造
GradingWorkflow.GradingState 从字符串 key 的 setAttr("gradeResult") 改为编译期检查的 setGradeResult():
1 | // 之前:运行时才暴露 typo |
9. LLM 可观测性(RagTrace)
新建文件:rag/RagTrace.java(~90 行)
每次检索输出结构化 JSON:
1 | { |
接入点:MCP KnowledgeSearchTool(每次 LLM 调工具就产出一条)和 RerankerService(精排结果回传)。
10. 清理残留接口
删除上版本遗留的 /api/rag/query、/api/rag/query/stream、/api/rag/query/flux、/api/rag/query/stored 端点及相关私有方法(-228 行)。RAG 检索统一走 MCP Agentic RAG 模式。
遇到的核心问题与解决方案
| 问题 | 根因 | 解决方案 |
|---|---|---|
LangChain4j 流式 Invalid HTTP method |
OpenAiStreamingChatModel HTTP 客户端和 Gateway SSE 不兼容 | 改用 WebClient 直连 |
WebClient NPE mapper returned null |
Reactor Flux 不允许 map() 返回 null | 改用 handle() + sink.next/sink.complete |
| LLM 不主动调 searchKnowledge | 没有 System Prompt 约束,LLM 凭记忆回答 | MCP 链路本身通,后续加 System Prompt 解决 |
| Git 中文乱码 | 文件编码不一致 | 重写为英文注释 |
| RAG 检索结果不理想 | 只有单路向量检索,关键词路未融合 | RRF 双路融合 + Reranker Cross-Encoder 精排 |
最终架构
1 | 用户请求(Web 聊天 / OneBot QQ) |
面试八股文准备清单
Java 基础
- HashMap 底层原理(1.7 vs 1.8,红黑树转换条件)
- ConcurrentHashMap 如何保证线程安全(CAS + synchronized)
- JVM 内存模型(堆、栈、方法区、GC 算法)
- volatile 和 synchronized 的区别
- 线程池参数和拒绝策略
- AOP 原理(JDK 动态代理 vs CGLIB)
Spring Boot
- IoC 和 DI 原理
- Bean 生命周期
- Spring Boot 自动配置原理(@EnableAutoConfiguration)
- 拦截器和过滤器的区别
- @Transactional 失效场景
MySQL / PostgreSQL
- B+ 树索引原理,为什么不用 B 树
- 聚簇索引 vs 非聚簇索引
- 慢查询优化思路(EXPLAIN 字段解读)
- 事务隔离级别和 MVCC
- pgvector 的索引类型(IVFFlat vs HNSW)
Redis
- 数据结构底层实现(SDS、ziplist、skiplist)
- 缓存穿透/击穿/雪崩 解决方案
- Redis 分布式锁(Redisson 看门狗机制)
- 过期策略和内存淘汰策略
- Redis Stream 和 Kafka 对比
分布式
- 布隆过滤器原理和误差率计算
- 令牌桶 vs 漏桶 vs 滑动窗口限流
- CAP 理论
- 分布式事务
AI 方向
- RAG 全链路(Embedding、检索、融合、精排)
- Bi-Encoder vs Cross-Encoder
- ONNX Runtime 原理和优势
- RRF 公式和 k 值选择
- MCP 协议 vs Function Calling
- Agentic RAG vs Naive RAG
- query rewriting 策略
- 向量数据库选型(pgvector/Milvus/Qdrant)
- RAGAS 评估指标
最终文件改动统计
| 类型 | 文件 | 说明 |
|---|---|---|
| 新建 | rag/RrfFusionService.java |
RRF 多路融合 |
| 新建 | rag/OnnxRerankerTranslator.java |
Cross-Encoder ONNX 翻译器 |
| 新建 | rag/RerankerService.java |
精排服务 |
| 新建 | rag/QueryDocPair.java |
输入对 record |
| 新建 | rag/QueryRewriter.java |
Query 改写 |
| 新建 | rag/RagTrace.java |
可观测性 Trace |
| 改造 | rag/RAGController.java |
升级管线 + 清理残留 |
| 改造 | mcp/tools/KnowledgeSearchTool.java |
全管线 + Trace |
| 改造 | agent/workflow/GradingWorkflow.java |
类型安全状态 |
| 改造 | Service/ServiceImpl/OpenClawServiceImpl.java |
WebClient 流式 |
| 改造 | config/SecurityConfig.java |
/api/rag 放行 |
| 总计 | 11 个文件,约 600 行新增 |




