EduMind Agent 架构演进:从 RAG、手写循环到 LangChain4j
这不是一次孤立的框架迁移,而是 EduMind Agent 层的演进总档案。早期 Tool Calling、动态课程路由、移除 OpenClaw 和源码复盘中的有效内容均归并到本文。
架构演进一图看懂
flowchart TB
A["普通 RAG"] --> B["Tool Calling"]
B --> C["课程上下文路由"]
C --> D["OpenClaw 网关"]
D --> E["应用内手写 Agent 循环"]
E --> F["LangChain4j AiServices"]
F --> G["声明式工具 + 会话记忆"]
背景:710 行的「手工耿」Agent
EduMind 项目的 Agent 层经历了三代:
| 代 | 实现 | Agent 循环 | 工具调用 | HTTP 客户端 | 状态 |
|---|---|---|---|---|---|
| 1 | OpenClaw 网关 | 网关托管 | 网关路由 | RestClient | 已删除 |
| 2 | AgentLoopServiceImpl | 手写 while 循环 | 手动 JSON Schema | RestClient/WebClient | 本文主角 |
| 3 | LangChain4jAgentService | AiServices 内置 | @Tool 注解 | ChatModel | 当前 |
第二代 AgentLoopServiceImpl 是一个 714 行的单体类,核心逻辑大概长这样:
1 | // 手写 Agent 循环(简化版) |
这代码能跑,但问题也很明显:
- while 循环:LangChain4j 和 Spring AI 都不需要你写,框架内置
- 手动 JSON Schema:
buildToolSchemas()方法 30 行,把ToolDefinition.inputSchema()转成 OpenAI function calling 格式——这件事框架可以自动做 - SSE 解析:
parseDeltaContent()手写data:行切割 + JSON 解析——框架有现成的TokenStream - 工具并行化:自己用
CompletableFuture+ 虚拟线程池搭的——框架原生支持
选型:Spring AI vs LangChain4j
两个框架都考察了,最终选 LangChain4j。理由很直接:
| 维度 | Spring AI (1.1.7) | LangChain4j (1.15.1) |
|---|---|---|
| Agent 能力 | ⚠️ Advisors 拦截器链(参考实现级别) | ✅ AiServices 内置循环 + langchain4j-agentic 实验模块 |
| 工具调用 | @Tool 注解 |
@Tool 注解 + 自动并行执行 |
| 多 Agent | ❌ 无原生支持 | ✅ SupervisorAgent + @LoopAgent |
| 声明式 AI | ChatClient API | ✅ @AiService 接口自动代理 |
| 流式 | Flux 手动处理 | TokenStream → Flux 桥接 |
| 可观测性 | ✅ Micrometer 原生 | ⚠️ 需手动配置 |
| Spring 集成 | ✅ 深度绑定 | ✅ 支持但不强制 |
核心差距在 Agent 编排。Spring AI 的 Agent 是通过 Advisors 链模拟的——先加一个 MessageChatMemoryAdvisor、再加一个 ToolCallingAdvisor,行为上像 Agent 但缺少原生循环、缺少声明式 Agent 定义、缺少多 Agent 路由。
LangChain4j 的 AiServices 不需要写一行循环代码:
1 | // 定义接口,框架生成动态代理——Agent 循环、工具调用、记忆管理全自动 |
迁移过程:踩过的坑
坑一:API 大面积改名
LangChain4j 1.15.1 相比文档和教程里的版本,API 改了不少:
1 | ChatLanguageModel → ChatModel |
都是编译期就能发现的,不算大坑,但改起来有十几个地方。
坑二:@MemoryId 强制要求 ChatMemoryProvider
TeachingAgent 接口用了 @MemoryId 做会话隔离。为结构化输出场景(作业批改)建了一个无状态的 agent 实例:
1 | // ❌ 这样会报 IllegalConfigurationException |
教训:带 @MemoryId 的接口必须配 chatMemoryProvider。不想配就单独建一个不带 @MemoryId 的接口。
坑三:自我反思的幻觉工具调用
我们在 AiServices 拿到最终回答后,会再用 ChatModel.chat() 做一轮自我反思审查。但 DeepSeek 模型在推理过程中可能决定”我需要调工具验证数据”,然后输出:
1 | <tool_call>searchKnowledge</tool_call> |
反思调用没带 tools,模型输出了一个字符串形式的工具调用标记——505 字的正常回答被替换成 38 字的坏输出。
修复:三层防护
1 | ① Prompt 明确禁止:禁止调用任何工具、禁止输出工具调用指令 |
顺手做的优化
自我反思:不要每句话都反思
最初自我反思无条件触发。用户说”下午好”,AI 回 72 字,反思调用再耗 16 秒 + 900 token,返回的和原稿一模一样。
加了双重条件:
1 | // 仅当工具被调用过 AND 回答 >= 100 字才触发反思 |
OneBot Markdown 剥离
AI 回复常带 Markdown 格式(**加粗**、# 标题、`代码`、表格等),QQ 文本框不渲染这些符号。加了一个 stripMarkdown() 方法在发送前剥离所有 Markdown 标记,12 条正则,50 行代码。
最终成果
1 | 迁移前: AgentLoopServiceImpl(714) + OpenClawServiceImpl(406) + OpenClawProperties(25) |
代码少了四成,但能力反而多了:
| 能力 | 手写时代 | LangChain4j |
|---|---|---|
| Agent 循环 | while 循环,30 行 | AiServices 内置 |
| 工具 Schema | 手写 JSON,20 行 | @Tool 自动生成 |
| 工具并行 | CompletableFuture,40 行 | executeToolsConcurrently() |
| SSE 解析 | 手写切割,20 行 | TokenStream → Flux |
| 会话记忆 | 无 | ChatMemory + @MemoryId |
| 多 Agent | 无 | langchain4j-agentic(待接入) |
总结
最初犹豫要不要上框架——毕竟手写版本也能跑,而且完全可控。但迁移完后回头看,手写 Agent 循环本质上是在应用层重写了一个简化版的 LangChain4j——而且写得远不如框架好(没有内存管理、没有自动并行、没有声明式接口)。
如果你也在纠结要不要把手写 Agent 换成框架,我的建议是:只要你的 Agent 逻辑超过了「调一次 LLM + 返回结果」的简单程度,就值得换。 用框架不是放弃控制,而是把控制权从”怎么调 API”上升到”怎么编排 Agent”的层次。
710 → 456 行,少的不只是代码,更是心智负担。
完整演进:为什么不是一开始就用框架
第一阶段:普通 RAG
系统最初只需要根据课程资料回答问题。请求进入后完成检索、拼接上下文、调用模型三个步骤,没有工具,也没有循环。这个阶段使用普通 Service 最简单,Agent 框架反而会增加概念负担。
第二阶段:Tool Calling
当模型需要查询作业、课程、知识点和课堂状态时,单纯把所有信息塞进 Prompt 已经不可维护。系统开始向模型提供工具描述,由模型选择工具,再由后端执行并返回结果。
这一阶段解决了“模型如何使用业务能力”,也暴露出三个工程问题:工具 Schema 重复、执行结果格式不统一、单个 Agent 面对多门课程时上下文容易串线。
第三阶段:课程上下文路由
课程 Prompt 被拆成三层:稳定的 Agent 行为、课程预设、当前会话上下文。QQ群、课程和知识库之间建立明确映射,请求进入后先确定课程,再注入对应 Prompt 和工具权限。
这一步比增加更多提示词更重要,因为它把“AI 应该知道什么”变成了可配置的数据边界。
第四阶段:移除 OpenClaw
OpenClaw 帮助项目快速完成 QQ 与模型链路验证,但随着工具、会话和权限进入核心业务,外部网关开始带来重复配置、链路不可观测和部署耦合。于是 Agent 编排回到应用内部,OneBot 只负责消息通道。
第五阶段:手写循环
自研循环带来了完全控制权:可以观察每一步工具调用、设置最大步数、处理流式输出,也能做并行工具执行。但当单体类增长到 700 多行时,维护成本开始超过收益。
第六阶段:LangChain4j
LangChain4j 接管通用机制,业务代码只描述工具、记忆和编排规则。迁移的收益不是“少写几百行”本身,而是把精力从请求体、SSE 和 JSON Schema 转回课程路由、工具权限和教学策略。
最终边界
当前架构中各组件职责保持清晰:
- OneBot 负责消息接入,不负责 Agent 决策;
- LangChain4j 负责模型调用、工具循环和会话记忆;
- Spring Boot Service 负责业务规则、权限和事务;
- RAG 负责提供课程知识,不直接执行有副作用的操作;
- MCP/工具描述负责能力暴露,但不能替代后端权限校验。
这也是整次演进最重要的结论:框架可以负责通用编排,业务边界仍然必须掌握在自己的应用里。





