前一阵子在做一个 Java 后端团队的技术调研报告生成助手,需求很朴素:用户丢一个技术主题,它负责查资料、抽数据、写报告。试了两周单智能体方案,终态效果总是不稳定——不是资料查全了但报告结构乱,就是报告漂亮但数据算错。后来我把架构切到 LangGraph4j 的 Multi-Agent Supervisor 模式,用 supervisor 调度三个专职 worker,问题才算系统性解决。
这篇博客就把这次从调研到落地的过程讲清楚。内容覆盖:为什么多智能体拓扑里 Supervisor 是最容易先落地的形态;LangGraph4j 环境搭建和最小 StateGraph 怎么写;worker 节点、supervisor 路由、checkpointer 的完整实现思路;我在实测中踩过的三个典型坑;以及团队里反复纠结的 "Spring AI 还是 LangGraph4j",我会给出一个实操向的选型结论。不管你是在评估多智能体方案,还是已经在写 LangGraph4j 代码,这篇都值得花十分钟看完。
1. 单智能体撞墙之后,我才开始认真研究 Multi-Agent 拓扑
1.1 一个 Prompt 塞下所有能力的代价
最开始的方案非常朴素:一个ChatLanguageModel挂 12 个工具,系统 Prompt 里写"你是全能的科技情报分析师,你可以搜索资料、计算指标、生成报告"。demo 数据还不错,但很快就出现三类问题。
第一,指令遵循率下降。工具一多,模型经常选错工具。我一开始以为是工具描述写得不够好,后来发现根本原因是:工具间的边界在模型眼里是模糊的,尤其是"搜索资料"和"计算指标"这种需要明确上下文的动作。第二,上下文相互污染。搜索返回的资料和生成报告的指令在同一个 dialog 上下文里,报告很容易被原始资料里的噪声带偏,出现"资料里有什么就写什么"的复读机行为。第三,单次请求耗时长。一次调用要完成搜索、读取、分析、撰写四件事,执行时间完全不可控,用户等十秒才看到第一句话的情况都有。
我不否认,"更精细的 Prompt + 更少的工具"可以把单智能体调到可用水平。但我的判断是:这个方向的天花板太低。业务场景一旦把任务拆细(比如"先查某开源监控系统的可用性数据,再对比三家厂商的指标,最后按财报格式输出"),单智能体维护成本就是指数级上升;而多智能体把每个子任务封装成独立节点,天然可扩展、可观测、可单独调优。
这里有一个朴素但重要的认知转变:多智能体不是为了"看起来高级",而是为了把"一个模型干所有事"变成"每个模型只干一件事"。一旦接受了这个设定,后面的拓扑选择和实现逻辑就顺了。
1.2 Supervisor 拓扑与通信机制:为什么不是每个智能体都互相喊话
我在调研多智能体拓扑时,最常见的是三类形态:
| 拓扑形态 | 通信方式 | 优点 | 缺点 |
|---|---|---|---|
| Peer-to-peer | 智能体之间直接传消息 | 灵活、去中心化 | 难收敛、监控困难 |
| Supervisor(中心化) | 所有消息汇聚到 supervisor 再分发 | 可控、可观测 | supervisor 是单点 |
| Hierarchical | 子 supervisor 分级管理 | 适合复杂任务树 | 实现成本高 |
我最终选了 Supervisor,理由很直接:第一,通信机制最简单。worker 之间不需要知道对方的存在,所有消息先汇到 supervisor,再由 supervisor 决定下一步。第二,状态可观测。每个节点产出什么,supervisor 都能看见,排查问题的时候相当于有一个总览视角。第三,实现成本可控。LangGraph4j 的 StateGraph 天然支持这种"单入口 + 条件边 + 多节点"的模型,不需要额外引入消息队列或事件总线。
这里需要强调一个认知:很多人以为多智能体的通信就是让智能体互相发消息,其实图编排框架里的通信本质上是通过"共享状态 + 节点读写"完成的。每个节点读当前状态,经处理之后写回新的状态,边决定状态加工的路径。也就是说,消息不是点对点投递的,而是沉淀在一个可被所有节点读取的状态空间里。这个概念理解透了,LangGraph4j 用起来才会顺手。
2. LangGraph4j 环境与最小图:先把编排跑起来
2.1 选择 LangGraph4j 的理由和依赖引入
团队原本考虑过两个方向:一个是纯手写状态机 +CompletableFuture,另一个是引入 Spring AI 的 Agent 基础能力。手写状态机的问题在于:多智能体调度里的重试、Checkpoint、条件路由都要自己造轮子,写出来的代码注定只有维护者敢碰。Spring AI 很好,但对图编排、状态持久化、interrupt 这些多智能体的核心设施,当时还比较少文档和 API 支持,硬上会变成"Spring AI + 自己的状态机"。
LangGraph4j 是 LangGraph 的 Java 移植版本,核心能力和 Python 版对齐:StateGraph、节点、边、条件边、CheckpointSaver、interrupt 都有。因为团队全是 Java 技术栈,不需要额外部署 Python 服务,直接嵌进 Spring Boot 线程池里跑就行。Maven 依赖(3.x 系列)大致这样:
<dependency> <groupId>org.bsc.langgraph4j</groupId> <artifactId>langgraph4j-core</artifactId> <version>3.1.1</version> </dependency> <dependency> <groupId>org.bsc.langgraph4j</groupId> <artifactId>langgraph4j-langchain4j</artifactId> <version>3.1.1</version> </dependency>注意,LangGraph4j 早期版本的坐标是org.bsc.langgraph4j,后来社区有往com.langgraph4j迁移的趋势。如果你在 Maven Central 上看到不同 groupId,说明版本分支不同,建议直接以官方 README 里给出的坐标为准,不要照抄网上的老配置。我用的 3.1.1 在 JDK 17 和 Spring Boot 3 下跑得很稳,没有遇到引入冲突。
2.2 状态、节点、边的三要素
在写 supervisor 之前,我先用最小图跑通 LangGraph4j 的三要素。
- 状态(State):一个可读写的对象,保存 messages、当前路由目标等。
- 节点(Node):一个函数,读状态、处理、返回要更新的字段。
- 边(Edge):决定调用顺序;条件边根据状态里的某个字段决定走向。
一个最简单的"入口节点 + 结束"示例,逻辑和你熟悉的"先 A 后 B"完全一样:
// 伪代码级示意:不同版本 API 命名有差异,重点看节点和边的结构 StateGraph<AgentState> graph = new StateGraph<>(AgentState.SCHEMA) .addNode("greet", state -> Map.of("messages", "hello from greeter")) .addEdge(START, "greet") .addEdge("greet", END) .compile();这里AgentState的核心数据结构大致是:
public class AgentState { public static final StateKey<List<Object>> MESSAGES = StateKey.of("messages"); public static final StateKey<String> NEXT = StateKey.of("next"); public static final StateKey[] SCHEMA = new StateKey[] { MESSAGES, NEXT }; }节点返回的Map.of("messages", ...)会覆盖或追加到状态里,条件边读取NEXT来决定下一步。我把这个最小图拿给组里两个原先坚持"手写状态机"的同事看,他们很快意识到:LangGraph4j 把状态机里最容易出 bug 的部分(状态跟踪、转移触发、终止条件)变成了声明式配置,业务需求变化时改节点的连接关系就行,不用删改一坨命令式逻辑。这是选型层面的最大收益。
3. Worker 节点:让每个智能体只干一件事
3.1 岗位说明书的制作:系统 Prompt 的分工要点
把任务拆成三个 worker:researcher(搜索资料)、analyst(计算指标)、writer(撰写报告)。每个 worker 都是一个独立节点,只负责完成自己的子任务。
每个 worker 有自己的系统 Prompt,我称之为"岗位说明书"。例如 researcher 的 prompt 是:
你是一个研究助理,只做信息检索与事实整理。你从用户输入和对话上下文中提取检索词,调用工具进行搜索,输出结构化的研究笔记。你不负责撰写报告,也不负责数值分析。
关键点:不要在 worker 的 prompt 里写入"如果...也可以..."这样的柔性授权。兼职思维会让模型自行跨边界,导致 supervisor 看到的产出变得不统一。worker 的职责范围越窄,模型选工具越准,prompt 也越好维护。
3.2 工具注册与节点返回值约定
worker 的工具通过 LangChain4j 的@Tool注解方式注册。比如 researcher 的搜索工具:
public class SearchDocsTool { @Tool("根据关键词搜索内部知识库,返回相关文档摘要") public String search(String keyword) { // 调用搜索服务 return searchService.search(keyword, 5); } }绑定模型时,我用了 LangChain4j 的AiServices组装,然后在 LangGraph4j 节点里调用。核心的 execute 方法大致长这样:
public Map<String, Object> execute(State state) { String task = state.value("messages").toString(); // 调用带 tool 的 LLM,得到回答 var response = aiServices.chat(task); return Map.of("research_notes", response); }这段代码我简化了工具调用组装细节。核心要理解的是:worker 节点返回的不是一段面向用户的话,而是一个"给 supervisor 看的中间产物",例如research_notes、analysis_result、report_draft。中间产物以结构化字段的形式写回状态,而不是和用户消息混在一个 messages 列表里。这样 supervisor 在下一轮路由时,能准确知道每个 worker 干了什么。
我在实际测试中发现,节点返回的字段命名最好带业务含义,不要用result1、result2这种序号。因为图的状态会越来越大,字段名就是业务语义的一部分,命名混乱会让后续调试 cost 变得极高。
4. Supervisor 的路由机制:LLM 调度员如何干活
4.1 核心代码:路由指令的生产
supervisor 节点同样是一个 LLM 调用,只不过它不执行具体工具,而是输出一个"下一个节点"决策。最直接的做法是把决策建模成一个结构化输出(record):
public record SupervisorDecision(String next, String reason) {} var decision = model.generate(List.of( SystemMessage.from(supervisorSystemPrompt), UserMessage.from(currentConcentratedContext) ), SupervisorDecision.class);next只有四种取值:researcher、analyst、writer、FINISH。是的,没有第五种,这是为了保证能收敛。我在上一版里加过"next 也可以等于 human",结果模型经常在需要深度分析时把任务推回给人,体验很糟。
supervisor 系统 Prompt 里我明确写了调度原则:
- 你需要判断当前用户的意图,是查资料、算数据,还是最终要一份成文报告。
- 如果还需要补充事实,路由给 researcher;如果已经拿到事实但需要计算指标,路由给 analyst;如果事实和指标齐备,路由给 writer。
- 只有当所有子任务都完成、产出达到交付标准时,才输出 FINISH。
- 不要反复路由给同一个 worker,除非该 worker 明确报告前一步失败。
4.2 条件边如何把决策变成图路径
得到SupervisorDecision后,怎么驱动图走下去?LangGraph4j 的条件边写法类似这样:
graph.addConditionalEdges( "supervisor", state -> { String next = state.value("next").orElse("FINISH"); return next; }, Map.of( "researcher", "researcher", "analyst", "analyst", "writer", "writer", "FINISH", END ) );每次任意 worker 节点执行完毕后,我会把一条带"谁产出"的中间消息追加到状态,同时让图回到 supervisor 入口,形成"supervisor 决策 -> worker 执行 -> supervisor 复核"的循环。
有些人会担心这样很慢:每次路由都调用一次 LLM。实测下来,对大多数任务来说,这个开销是值得的——因为决策模型只做路由,一次只生成少数 token 的决策,比让一个大模型长时间思考再输出最终答案要快,也更可控。
4.3 通信机制在 Supervisor 拓扑里的实际载体
再回到通信机制。supervisor 能做出好决策,前提是它需要看到:
- 用户的最新问题;
- 上一个 worker 的中间产物(可能是
research_notes,也可能是analysis_result); - 最近几轮的路由历史,避免重复调度。
这些数据全部存在一个共享状态对象里。supervisor 节点的实现里,我通常会拼一个 compact 的 context:
String currentConcentratedContext = Stream.of( latestUserMessage(state), state.value("research_notes").orElse(""), state.value("analysis_result").orElse(""), state.value("report_draft").orElse(""), routingHistory(state) ).filter(s -> !s.isBlank()) .collect(Collectors.joining("\n---\n"));这就是多智能体之间的"通信"本质:不通过消息总线点对点传,而是统一读写状态。好处是异常发生时有完整的状态快照,出问题可以直接 dump 出来看。对于 Java 团队来说,这比在业务代码里到处传引用要清爽得多。
5. 会话记忆与隔离:没有 Checkpointer 的 supervisor 等于失忆调度员
5.1 为什么多轮对话必须有会话状态
如果只是单轮问答,无状态调用也能跑。但真实场景里用户会追问:"第二家公司的数据给我再看一眼",或者"刚才那份报告里,把图表改成三年对比"。如果 supervisor 每次都是白纸一张,它既不知道"第二家公司"是哪家,也不知道"刚才那份报告"是谁写的。
LangGraph 系列框架给出的标准解法是 Checkpointer:每次节点执行后,把状态快照持久化下来,并关联一个threadId。下一次调用时,传入同一个threadId,图会从快照中恢复上下文。
5.2 内存版 Checkpointer 的接入
LangGraph4j 里提供了CheckpointSaver接口,实测中我用内存版就够:
CheckpointSaver saver = new MemorySaver(); StateGraph<AgentState> graph = workflow.compile(saver); // 调用端 var config = Map.of("configurable", Map.of("thread_id", "thread-001")); var result = graph.invoke(input, config);这里有两个细节值得注意。
第一,threadId最好由业务侧生成并持久化,比如存在用户会话表里。否则重启服务后线程 ID 没了,用户上下文就丢了。第二,内存版MemorySaver不能用多实例部署。如果你有两个服务实例轮询转发用户的请求,一定得换成 Redis/DB 的 checkpoint 实现,否则不同实例各自维护一份状态,用户会感觉到上下文"随机失忆"。
我用一个真实测试场景验证过:同一个 threadId 连续问了三个问题,第三个问题要求"基于前两个问题的结论做总结",答案准确;切到新 threadId 再问同样的问题,模型明确说"我看不到之前的对话"。这说明 checkpoint 在 LangGraph4j 中是真实生效的。
6. 实测翻车记录:三个必须提前防范的坑
6.1 坑一:supervisor 在两个 worker 之间反复横跳
第一次联调时,我输入"搜索某厂商的基站功耗数据,并和上一季度对比"。结果 supervisor 在 researcher 和 analyst 之间循环了整整七次。翻查日志发现:researcher 每次产出一份新的研究笔记,analyst 每次产出新的指标,supervisor 看到"有新增产出"就又认为是新信息需要再次路由,永远不会输出 FINISH。
修复动作有三个:
- 系统 Prompt 里加强约束:"只有新增产出是实质性的回答进展,才继续路由;如果产出没有导致信息增量,直接 FINISH。"
- 在 State 里记录路由历史,当出现"同一个 worker 被推荐超过两次"时,条件边直接强制走 FINISH。
- 给图执行加上最大步数限制,超过则中断并返回当前中间产物。这样即使模型抽风,也不会烧掉大量 token。
这三步叠加后,循环基本消失。需要注意,Prompt 约束是软性的,代码兜底才是硬性的。我建议每个生产环境的 Supervisor 项目都要有最大步数兜底,否则一个循环可能让一次请求耗费几十次 LLM 调用。
6.2 坑二:上下文爆炸与决策质量下降
上线前几天,supervisor 的决策突然变得很"敷衍",经常直接 FINISH,或者给错 worker。查看日志后发现,每次调用我都把完整 messages 列表传给 supervisor,几轮对话后 token 数轻松过万,模型在长上下文里反而抓不住重点。
我给 supervisor 的输入做了两层裁剪:
- 窗口化:只保留最近 3 轮用户消息 + 最近一轮 worker 产出。
- 摘要化:新增一个
summarize节点,当消息条数超过阈值时,把旧历史压成一段 summary,保存在状态里。
实际操作中,summarize 节点比较耗时,我不会每次都跑,只是"超过 12 条才触发"。这个阈值我调过几轮,太早触发会丢失细节,太晚触发又会把上下文撑爆。12 条在 OpenAI 和本地化模型上都有不错表现。
6.3 坑三:worker 工具调用失败导致整图中断
某次演示时,researcher 调搜索工具超时,整个图直接抛异常中断,supervisor 没有机会做 fallback。这暴露了我对节点异常处理的疏忽。
LangGraph4j 不会替你做业务级容错,节点内部必须自己 catch。我在每个 worker 节点外层包了一个safeExecute方法:
try { return agent.execute(state); } catch (Exception e) { return Map.of("error", e.getMessage(), "next", "FINISH"); }同时,工具的 LLM 调用层我还加了两次重试,第一次超时后等待 500ms 再试。对内部知识库这样的低延迟服务,重试的成功率很高。
值得强调的是:worker 失败时,不要把错误直接抛到图外面,最好是转成一个结构化错误信息写回状态,让 supervisor 在下一次决策时看到"这个工人刚才失败了,需要换策略",这样整个系统才有自愈能力。
7. 团队选型纠结:Spring AI 和 LangGraph4j 到底怎么选
7.1 两者的舒适区完全不同
我理解很多人纠结这个问题,因为我们团队也纠结了挺久。先说结论:这不是一个简单的替代关系,Spring AI 的舒适区是"Spring Boot 应用里快速接入 LLM 能力并做好工具抽象",LangGraph4j 的舒适区是"以图编排的方式管理复杂智能体生命周期"。
| 维度 | Spring AI | LangGraph4j |
|---|---|---|
| 上手成本 | 低,依赖 Spring 生态 | 中,需要理解图/状态/节点概念 |
| 典型场景 | 单 Agent + 工具,固定顺序链 | 多 Agent 协作、动态路由、跨会话恢复 |
| 状态持久化 | 有简单的 ChatMemory | CheckpointSaver,支持快照/回放 |
| 条件路由 | 需要手写逻辑 | 原生条件边,节点即路由 |
| Human-in-the-loop | 支持有限 | 原生 interrupt 机制 |
| 与 Spring Boot 集成 | 无缝 | 可作为独立库嵌入 |
Spring AI 适合的场景:
- 只需要一个 ChatClient,接上模型,挂三五个工具,开箱即用;
- 想用 Spring 家族的 AutoConfiguration 管理各种 model provider;
- 需要对话记忆和简单的 advisor 链,不需要复杂条件路由。
LangGraph4j 适合的场景:
- 多个智能体分工协作,且需要动态路由(supervisor);
- 需要跨会话恢复状态、human-in-the-loop、节点级可重放;
- 节点之间的流转逻辑经常变动,希望以图配置代替命令式代码。
最常见的误用是:用 Spring AI 硬写多智能体,最后自己维护一个巨大的 if-else 状态切换器;或者反过来,只为了一个简单问答就引入 LangGraph4j,成本明显不划算。
7.2 我的实操建议:能用编排绝不用手写状态机
给团队的建议是按复杂度分层:
- 如果调用链是固定顺序(A 完了 B,B 完了 C),且分支不超过两个,直接用 Spring AI + 代码顺序调用,最简单。
- 如果任务需要根据内容动态分配到不同 worker,或者需要跨会话状态,直接上 LangGraph4j,不要自己去写状态机。
- 如果项目已经用了 Spring AI 做基础设施,也不冲突:LangGraph4j 可以单独作为一条支线运行在 Spring Boot 里,两者各管一段,把 LLM 调用层统一用 LangChain4j 抽象即可。
我个人的体会是,对于 Multi-Agent Supervisor 这类架构,选 LangGraph4j 的收益是长期的。前期多花的一个下午,会在后期排查路由问题、加新 worker、做断点恢复时成倍赚回来。最后再分享一个当时帮了大忙的小技巧:把每次 LLM 调用的输入输出都落到日志里,特别是 supervisor 的决策理由。多智能体系统的黑盒感很强,有了决策理由日志,用户质问"为什么让研究员重做一遍"的时候,你能立刻给出答案,这比任何监控面板都管用。