ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用LangGraph4j在Spring Boot中实现Multi-Agent Supervisor模式

用LangGraph4j在Spring Boot中实现Multi-Agent Supervisor模式 最近在搞一个 Java 服务里的多智能体协作需求需要在同一个进程里跑几个职责不同的 Agent再由一个 Supervisor 统一调度、分配任务。最早我用 Python 版的 LangGraph 搭原型逻辑很快跑通但到了交付阶段团队不想为一个小功能额外引一套 Python 运行时于是转向 LangGraph4j。折腾了两周我把 Multi-Agent Supervisor 模式完整落地在 Spring Boot 服务里这里记录一下完整的设计思路、关键代码、踩过的坑以及很多人纠结的“现在到底用 Spring AI 还是 LangGraph4j”这个问题。1. 为什么在 JVM 团队里做 Multi-Agent我最终选了 LangGraph4j1.1 我的场景需要一个“老板”来分活的 Agent 系统我做的业务是一个工单助手用户提交一个问题系统要决定是去查知识库、还是生成一段代码、还是让用户补充信息最后汇总成答案。最初我用一个大 prompt 把“理解意图、检索、生成、总结”全塞给一个 Agent看起来简单实际用起来问题很多。prompt 越长模型越容易忽略关键指令检索和写代码的逻辑互相干扰出了错也很难定位到底是哪一步的问题。后来我改成 Multi-Agent 架构核心就是 Supervisor 模式一个 Supervisor Agent 负责读用户请求判断该叫哪个子 Agent 干活所有子 Agent 干完活都把结果交回给 SupervisorSupervisor 再决定下一步是继续派活还是收尾。就像一个小团队主管不亲自写代码但是负责分配任务和验收结果。这种模式下每个子 Agent 的职责很单一prompt 可以写得很聚焦Supervisor 只做决策不淹没在具体操作细节里。问题是Java 生态里一直没有特别顺手的编排框架直到我注意到 LangGraph4j。1.2 LangGraph4j 与 Python 版 LangGraph 的关系LangGraph4j 是社区把 LangGraph 的设计思想移植到 JVM 上的实现核心概念和 Python 版一致StateGraph、Node、Edge、Conditional Edge、Checkpoint甚至执行方式都尽量对齐。我最早担心它只是“照着画了个葫芦”实际用下来基础的状态流转、条件路由、流式输出都是可用的。对我来说最大的价值不是 API 完全一致而是思想一致。我在 Python 版里验证过的 Supervisor 循环可以在 Java 里几乎一比一复刻。团队不用重新学一套“多 Agent 设计哲学”只需要补 Java 语法和库 API 就行。另外LangGraph4j 天然适合 Java 技术栈状态能定义成强类型对象节点能复用 Spring Service日志链路可以用现成的 Java 日志框架测试也更顺手。对我们这种长期维护 Spring Boot 项目的团队来说Hybrid 架构比引入异构运行时稳妥得多。1.3 和 Spring AI 的对比它不是替代品而是编排层最近常看到有人在问“用 Spring AI 还是 LangGraph4j”我的结论很直接它俩不是同层的东西别做成二选一。Spring AI 解决的是“怎么跟模型说话”它帮你封装了 ChatClient、Prompt、结构化输出、工具调用、Embedding 等能力让 Java 代码可以声明式地调用大模型。但 Spring AI 本身不关心你编排几个 Agent、谁先谁后、条件路由怎么走。LangGraph4j 解决的是“多个 Agent 怎么协作”它负责把整个工作流描述成一张有向图定义哪个节点运行完走哪条边也支持暂停、恢复、保存状态。节点内部具体怎么调模型它不关心。所以最舒服的组合是LangGraph4j 充当 Multi-Agent 的“骨架”Spring AI 充当每个 Agent 的“大脑连接器”。在后面代码里你会看到我在 LangGraph4j 的节点里面直接调用 Spring AI 的 ChatClient二者完全可以共存。维度Spring AILangGraph4j核心价值模型访问、Prompt、工具调用封装多 Agent 流程编排、状态管理、循环与恢复适合场景单个 Agent、简单多轮对话、RAGSupervisor、多专家协作、人工审批、条件分支和 LangFlow 类工具的关系不冲突可以互相配合不冲突可以互相配合学习成本较低中等需要理解图执行模型如果你只是希望“给 Spring Boot 项目接一个会调用工具的聊天助手”Spring AI 完全够不需要引入 LangGraph4j。但当你发现一个 Agent 里塞了太多职责、代码越来越乱、开始出现“根据上一步结果决定下一步要谁来干”的复杂流程时就是该上 LangGraph4j 的时候了。2. Supervisor 模式到底是什么用状态机的思路理解多 Agent 调度2.1 三种常见的多 Agent 模式社区里常见的设计模式有三种搞清楚它们的区别才知道自己到底需要什么并行扇出一个任务拆成多个子任务多个 Agent 同时执行最后汇总。适合“多路搜索、对比分析”这类场景。流水线Agent 按顺序执行上一个的输出是下一个的输入。适合“生成大纲-扩写-校对”这种严格串行流程。Supervisor主管中央控制器根据当前状态动态决定下一步执行哪个子 Agent子 Agent 完成后把控制权交还给主管形成循环直到主管判定任务完成。Supervisor 模式最大的优点是灵活它不是一个固定流程而是一个带“决策节点”的循环流程。每一步都可能不一样更像是真正的团队协作。缺点是决策本身有开销因为每轮循环都要调一次模型做路由判断而且如果路由不稳定可能陷入来回切换的循环。2.2 Supervisor 循环的关键条件边和控制权交接理解了 Supervisor 模式再看 LangGraph4j 实现关键就是两个条件边和控制权回传。条件边是指从一个节点出发时根据当前状态的不同走不同的目标节点。在 Supervisor 里就是 Supervisor 节点看完请求后决定下一步是去 researcher 还是 reporter 还是直接结束。控制权回传的意思是子 Agent 干完活之后不能直接走到 END而是必须回到 Supervisor。这样 Supervisor 才能根据子 Agent 的产出判断“任务是否完成”或者“要不要换一个 Agent 再来一次”。如果子节点直接连 ENDSupervisor 就失去了最后一次决策机会。这个模型特别像状态机状态是当前用户的请求、对话历史、各子 Agent 的产出事件是节点执行完毕转移规则由条件边描述。把 Multi-Agent 当成状态机来设计比靠感觉拼 prompt 要可靠得多。2.3 最少可用的执行流一个最精简的 Supervisor 执行流是这样的开始用户请求进入 START。Supervisor 节点运行调用模型输出下一跳。条件路由如果模型输出 “research”走 researcher 节点输出 “report”走 reporter 节点输出 “finish”走 END。子 Agent 节点运行researcher 或 reporter 干活把结果写回状态。控制权返回子 Agent 的边指向 Supervisor再次进入 Supervisor 节点。循环直到路由结果为 “finish”。在这个流程里子 Agent 的数量可以任意扩展只要在路由表里注册一个 key 和对应节点即可。Supervisor 不关心某个 Agent 内部怎么实现只关心它返回的结果是否已经写入了共享状态。3. 用 LangGraph4j 实现 Supervisor状态、节点、条件路由3.1 状态 State所有子 Agent 共享的“共享白板”LangGraph4j 里最重要的概念是 State。它会在整个图执行过程中传递每个节点都能读、能改。我把 State 理解为一张放在会议桌上的白板谁拿到笔都能写但必须按照约定写不能随意覆盖别人的内容。我用的 State 是HashMap的子类好处是扩展字段方便。LangGraph4j 官方示例里有不少是直接用HashMap的但对于复杂工程我建议还是定义一个语义明确的类型public class AgentState extends HashMapString, Object { public AgentState() { super(); } public String getRequest() { return (String) this.get(request); } SuppressWarnings(unchecked) public ListMapString, String getMessages() { return (ListMapString, String) this.get(messages); } public String getNext() { return (String) this.get(next); } public void setNext(String next) { this.put(next, next); } }我这个 project 里常用的字段包括request用户原始请求、messages所有 Agent 产生的消息历史、next路由决策、instruction给子 Agent 的额外指令、steps循环次数、finalAnswer最终答案。状态字段越清晰节点逻辑就越容易写。有一点要注意State 是可变对象节点返回值会合并更新。别在节点里偷偷把 State 换成新对象否则可能导致后续节点读不到之前写的数据。3.2 Supervisor 节点让 LLM 决定下一步该找谁Supervisor 节点其实是整个系统里最简单也最关键的节点它做的事情只有一件调用模型让模型从预定义的 Agent 集合里选一个并输出给对应 Agent 的指令。我让模型输出严格 JSON这样解析方便public AgentState supervisorNode(AgentState state) { String request state.getRequest(); ListMapString, String history state.getMessages(); String prompt 你是 Multi-Agent 系统的 Supervisor。 根据用户请求和当前历史从下面三个动作中选一个 - research需要深入调研交给 researcher 节点 - report需要整理报告交给 reporter 节点 - finish任务已经完成可以给出最终答案 只输出 JSON不要其他解释格式如下 {next:research,instruction:给子 Agent 的指令} ; String response chatClient.prompt() .system(prompt) .user(request) .call() .content(); try { JsonNode node objectMapper.readTree(response); state.setNext(node.get(next).asText()); state.put(instruction, node.get(instruction).asText()); state.put(steps, ((Integer) state.getOrDefault(steps, 0)) 1); } catch (JsonProcessingException e) { state.setNext(finish); } return state; }这里有几个实践要点。一是必须限制输出格式不然路由没法做。二是我加了steps自增后面会用来做循环上限保护。三是如果 JSON 解析失败我宁可让它走finish也不要随便走一个子节点因为在生产环境里不确定的决策落到某个 Agent 上比直接收尾危险得多。3.3 子 Agent 节点干完活把结果写回状态子 Agent 节点和普通节点没有本质区别只是职责更纯粹。比如 researcher 节点它的任务就是根据 Supervisor 给的instruction去检索数据并把结果写回 Statepublic AgentState researcherNode(AgentState state) { String instruction (String) state.getOrDefault(instruction, ); String request state.getRequest(); // 这里可以调用知识库检索、外部 API 或工具函数 String knowledge knowledgeBaseService.search(request); String result chatClient.prompt() .system(你是一名研究员请基于检索内容给出客观回答。) .user(instruction \n检索内容 knowledge) .call() .content(); state.put(researchResult, result); ListMapString, String messages state.getMessages(); messages.add(Map.of(role, researcher, content, result)); state.put(messages, messages); return state; }reporter 节点类似它不关注怎么调研只关注怎么把已有的researchResult和request组合成一份结构化报告段落、要点、结论。两个节点干完活之后都不会自己决定结束而是通过图定义中的边回到 supervisor这就保证了“谁决定的开始谁负责结束”。3.4 主流程装配与编译运行现在到了最核心的部分把上面这些节点用 LangGraph4j 装配成一张可执行的图。我以我项目里锁定的 API 版本为例整体结构如下StateGraphAgentState graph new StateGraph(AgentState::new) .addNode(supervisor, this::supervisorNode) .addNode(researcher, this::researcherNode) .addNode(reporter, this::reporterNode) .addEdge(START, supervisor) .addConditionalEdges(supervisor, this::routeFromSupervisor, Map.of( research, researcher, report, reporter, finish, END )) .addEdge(researcher, supervisor) .addEdge(reporter, supervisor); CompiledGraphAgentState compiledGraph graph.compile();注意最后两条固定边researcher - supervisor和reporter - supervisor。它们实现了我前面说的“控制权回传”。没有它们子 Agent 执行完就结束Supervisor 就没有机会验收结果。路由函数里我做了归一化处理避免模型输出不一致导致路由失败public String routeFromSupervisor(AgentState state) { String next state.getNext(); if (next null) { return finish; } String normalized next.trim().toLowerCase(); if (normalized.contains(research)) { return research; } else if (normalized.contains(report)) { return report; } return finish; }执行的时候也很简单MapString, Object input new HashMap(); input.put(request, 帮我查一下最近日志里的错误原因); input.put(messages, new ArrayList()); input.put(steps, 0); MapString, Object output compiledGraph.invoke(Input.of(input)); System.out.println(output.get(finalAnswer));更推荐在调试阶段用 stream 方式逐步观察compiledGraph.stream(Input.of(input)) .stream() .forEach(System.out::println);这样你能看到每个节点的进入和退出排查问题时不用瞎猜。4. 让 Supervisor 更可靠Checkpoint、人工审批、超时与重试4.1 Checkpoint 保存现场Agent 中途挂了可以从头恢复LangGraph4j 支持 Checkpoint核心作用是保存每一步 State 的快照。一旦某个子 Agent 调用失败我们可以从最近一个正确的快照恢复而不是整个流程重新跑。我的做法是为每个用户请求分配一个独立的threadId把 threadId 和用户请求绑定。执行前经过 Checkpoint 保存执行失败后用同一个 threadId 再次发起调用框架会把状态恢复到最近完成的节点然后继续往下走。MapString, Object input new HashMap(); input.put(threadId, order_10086); input.put(request, 分析订单 10086 的交付延迟原因); input.put(messages, new ArrayList()); input.put(steps, 0); compiledGraph.stream(Input.of(input, order_10086));Checkpoint 在生产环境里特别重要因为多 Agent 流程通常比单 Agent 长中途失败的概率也更高。如果没有持久化状态用户一个问题可能要重新跑好几分钟体验极差。4.2 人工审批把控制权交给“人”来确认很多业务场景里Agent 不能完全自主行动。比如“自动生成一封发给客户的道歉信”Supervisor 可以写草稿但发送前必须由人工确认。这种需求不适合硬编码成子 Agent更适合设计成“挂起流程”。我的通用做法是在 State 里放一个approvalRequired字段当 Supervisor 判断需要人工审批时把状态设置成挂起然后让流程自然结束。真正的人工审批接口不在 LangGraph4j 内部而是通过外部 REST API 触发PostMapping(/orders/{orderId}/approve) public void approve(PathVariable String orderId) { MapString, Object input new HashMap(); input.put(threadId, order_ orderId); input.put(approvalResult, approved); compiledGraph.invoke(Input.of(input)); }关键思路是一个threadId对应一份完整的 State人工审批只是“重新唤醒”这个 threadId让 Supervisor 读到最新的approvalResult再决定下一步走向。这样既实现了人工介入又保持了图的统一性。别试图把审批按钮塞进图里的某个节点那会把流程编排和业务接口耦合在一起维护起来很痛苦。4.3 超时、重试与预算控制Multi-Agent 系统最容易被忽略的问题是“失控”。因为每个子 Agent 都在调用大模型每一步都有成本和时间开销。Supervisor 又是一个循环如果模型判断失误可能一直在几个子 Agent 之间来回切换十几轮都结束不了。我做了三层保护第一层循环次数上限。每次 Supervisor 执行steps加一超过 5 次强制finish。第二层每个子 Agent 调用模型时在 Spring AI 的 ChatClient 上设置最大 token 数和超时时间。第三层整个图执行外面包一层CompletableFuture超时超过 120 秒直接终止。public AgentState supervisorNode(AgentState state) { int steps (Integer) state.getOrDefault(steps, 0); if (steps 5) { state.setNext(finish); state.put(finalAnswer, 已经多次尝试为避免循环直接给出当前已有结论 state.getOrDefault(researchResult, 暂无结果)); return state; } // 正常调用模型做决策 return state; }这套保护在测试阶段救过我很多次。有一回我在 prompt 里写“如果信息不足继续调查”模型真的就一直在 research 和 supervisor 之间循环了七八轮直到我加上步骤上限才停下来。5. 实测中的坑和调试技巧5.1 条件路由的返回值不稳定正则兜底我最早天真地认为模型会稳定输出{next:research}结果实际返回五花八门有时候是“research”有时候是“Research”有时候是“next”: “researcher”甚至带一些前后缀解释。如果你直接拿它去 Map 里查随时会命中不了预设路由。后来我在路由函数里做了三步归一化先 trim再转小写再 contain 判断。这样 “researcher” 也能命中 “research”“report” 和 “reporting” 都能命中 “report”。如果归一化后仍匹配不到默认走finish绝不让图执行在中途断裂。5.2 节点间数据复用与并发问题LangGraph4j 的图本身是支持多实例并发执行的但如果你在节点里偷懒把中间结果存在类字段里就会出现严重串数据问题。比如我一开始为了省事在 Service 类里写了一个currentState字段结果两个用户同时发任务时后一个用户直接覆盖前一个导致 A 的请求跑到一半拿到 B 的数据。排查了很久才意识到State 必须通过方法的入参传递不能塞进 Bean 的成员变量。正确做法是节点方法参数里接收 State返回值也是 State所有的临时变量都放在 State 或方法局部变量里。团队里所有人都要遵守这个约定否则并发一上来就出事故。5.3 如何观察图执行过程逐步输出排查 Multi-Agent 问题最痛苦的是“你只知道结果不对不知道卡在哪一步”。我用stream方式输出后明显效率提升。每个节点进入、退出、路由方向都能看到compiledGraph.stream(Input.of(input)) .stream() .forEach(event - System.out.println(event));LangGraph4j 的输出里包含事件类型和状态比如ON_NODE_START、ON_NODE_END、路由结果等。配合日志文件的 traceId基本能还原一条完整的“用户请求 - Supervisor 决策 - 子 Agent 执行 - 回到 Supervisor”的链路。还有一个很土但很有用的技巧在节点代码里加一个短日志打印当前 State 里几个关键字段的摘要。实测不需要打印整个 State因为消息历史可能很长刷屏刷到没法看。打印steps、next、instruction就够了。5.4 关于“Spring AI 还是 LangGraph4j”的最终判断回到开头那个问题我认为真正的判断标准不是看谁更火而是看你的流程复杂度。如果只是单一 Agent 工具函数 对话补全用 Spring AI 是最省事的。它和 Spring Boot 融合得很好写个 ChatClient 就能干活。这时候引入 LangGraph4j纯属给自己增加概念负担。但如果你的业务里已经出现了“多个角色协作”、需要条件分支、需要人工确认、需要从失败现场恢复那就应该选 LangGraph4j。你可以在它的节点里继续用 Spring AI 调模型两者配合起来才是完整方案。还有一点LangGraph4j 的官方文档更新速度相当快不同版本的 API 可能略有差异。我在项目里直接把依赖版本锁死并且维护了一套从最小图到完整图自动跑通的集成测试。每次升级依赖先跑一遍测试确认行为没变再合入主干能省掉一大半莫名其妙的线上故障。最后分享一个很土但实用的建议如果你也是第一次接触 LangGraph4j不要一上来就设计十几个节点的复杂图。先把 Supervisor 两个子 Agent 的最小循环跑通确认路由和状态流转符合预期再逐步加 Checkpoint、人工审批、超时控制。多 Agent 系统最怕的不是模型不行而是编排逻辑自己先乱成一锅粥。
返回列表