☰
Java AI Agent开发:LangGraph4j状态图入门与选型实践
2026/11/4 10:02:43 网站建设 项目流程

先别急着把状态图塞进业务里。我最近在公司帮 Java 组搭 Agent 框架,发现一个特别典型的现象:不少人一上来就奔着 LangGraph4j 这种状态图框架去,结果业务逻辑总共三步,非要用图画出个迷宫来;另一部分人正好相反,业务里明明有循环、有分支、有人工介入,却硬用 while 加 if 拼出来,代码写到后面连自己都看不懂。

这篇文章我站在 Java 开发者这边,聊聊 LangGraph4j 入门,以及更关键的一个问题:AI Agent 什么时候该上状态图。文章里不会有那种“从入门到放弃”的空话,所有代码都来自我简化过的真实项目。适合正在用 Java 做 Agent 开发,或者被业务流程里的分支循环搞到头大的朋友。版本相关的地方我会特别标注,你照着思路改到自己业务里就行。

1. 先想清楚:你的 Agent 到底要不要状态图

1.1 没有状态图也行的 Agent

先别急着引入框架。我见过太多团队,把最简单的对话流程硬改成状态图,最后图比代码还复杂。一个典型的反例:用户发消息 -> 调用大模型 -> 返回回答。总共三步,普通 Java 方法就能写得很干净。

String answer = chatClient.call(userMessage); return answer;

哪怕要加点工具调用,无非就是先让模型决定调哪个工具,再执行工具,再把结果带回模型。这种线性流程,用一个 for 循环就能控制,最多包一层函数。

什么时候开始别扭呢?当你发现自己需要记录“当前走到哪一步了”,需要支持用户中途撤回,或者模型在某一步反复调用同一个工具不退出。这时候你的线性代码里已经开始塞入各种状态标记位、嵌套 if 和 break。代码表面还在跑,但你已经很难向同事讲清楚整个流程。

我自己的经验是:如果流程是“顺序执行,偶尔回退”,而且状态字段少于五个,先不要上状态图。你需要的只是一个更规范的工具调用封装,而不是一张完整的图。

1.2 出现三个信号再考虑状态图

信号一:分支和循环本身就是业务流程的一部分。比如 Agent 要先判断问题类型,再选择不同的工具链;工具调用失败后,要换一种策略重试,而不是直接把错误抛给用户。这种逻辑用 if/else 写出来会越来越乱。

信号二:需要审计和断点续跑。用户会取消任务,会暂停任务,需要人工介入,系统得能保存当前状态,并在未来某个时间点恢复。典型场景是“AI 生成内容 -> 人工确认 -> 执行后续操作”,人工确认可能要等几分钟甚至一天。

信号三:多人协作维护业务逻辑。状态图让整个流程可视化,每个节点由不同团队维护,边界清晰。而一堆 Service 类里的隐式调用,新同事根本不知道从哪里看起。

你可以给三个信号打个分:满足两个,直接上状态图;只满足一个,再等等;一个都不满足,别折腾。

判断维度线性编排状态图
流程复杂度低,顺序为主高,分支、循环、并行
状态管理方法局部变量够用需要跨节点共享和持久化
人工介入很难优雅实现通过中断和恢复天然支持
可观测性靠日志拼凑节点、边、状态均可视化
团队协作各写各的 Service按节点划分责任

2. 状态图解决的是控制流与可观测性问题

2.1 一个生活化类比

状态图不是什么高深概念。你网购时的订单状态就是活生生的状态图:待支付、已支付、待发货、已签收。这些状态之间不是乱跳的,支付成功才能从待支付到已支付,退款只能发生在特定状态。

Agent 的状态图是同一个道理。节点是“动作”,边是“什么条件触发下一个动作”,State 是全局账本,记录用户问题、中间结果、工具调用次数等信息。模型判断该调工具了,就走“调用工具”这条边;工具返回了,再走“回到模型”这条边;最终答案生成完毕,才走到 END。

很多人觉得状态图只是为了画一张漂亮的流程图,其实不是。它把顺序编程里那些隐式的控制流,变成了显式的数据和边。你不必在代码里翻来翻去找“下一步到底是谁在调用”,看一眼图就全明白了。

2.2 相比手写 while,状态图赢在哪里

手写 while 加 if 也能实现 Agent 循环,但代价是控制流散落在代码各处。状态图把“下一步去哪”集中到了边的定义里,这对维护是质的改变。

可读性:一张状态图能直接看清完整流程,同事拿到代码不用从头追到尾。可测试性:每个节点是独立单元,单独喂数据就能测,不需要从入口一路模拟到目标节点。可恢复性:配合 Checkpoint,可以把每个节点执行完的状态快照保存下来,异常或人工中断后从指定节点继续跑。可扩展性:加一个节点、加一条边,不影响其他逻辑。

我记得第一次在项目里用状态图重构一个“先检索再生成再重试”的 Agent 时,代码量没少太多,但调试时间明显缩短了。以前出问题要靠日志猜流程,现在直接把当前节点和 State 打出来,问题在哪个环节一目了然。

3. LangGraph4j 快速上手:从 0 到 1 搭一个带状态图的 Agent

3.1 引入依赖

LangGraph4j 是 Java 生态里对标 Python LangGraph 的库,核心思想一致,但不是简单翻译。目前我使用的版本基于 4.x,Maven 依赖大概是这样的:

<dependency> <groupId>org.bsc.langgraph4j</groupId> <artifactId>langgraph4j-core</artifactId> <version>1.0.0</version> </dependency>

注意,这个库版本迭代挺快,而且 Java 生态里它不像 Spring Cloud 那样统一,建议先去看官方 GitHub 仓库确认最新版本和 API 变化。下面的代码我会用比较通用的写法,你自己对照版本调整。

3.2 核心概念先过一遍

在写代码之前,先把 LangGraph4j 的几个概念和 Java 对象对应上,后面才不会懵。

概念Java 对应物作用
State自定义 Java 类所有节点共享读写的数据
Node一个方法或类接收 State,处理完返回要更新的字段
Edge连接两个节点表示执行流向
ConditionalEdge根据 State 返回路由 key动态决定下一个节点
Checkpoint状态持久化机制保存快照,支持恢复
END特殊终点节点图执行结束

Node 在 LangGraph4j 里通常是这样:输入当前 State,返回一个 Map,里面是你要更新的字段。返回空或 null 表示这次不改状态。设计上有点像函数式编程里的“不可变更新”,Java 里用 map 或 with 方法都很自然。

3.3 最小可运行的图

先看一个最简例子,两个节点,一条边,主要帮助你理解执行流程。

public class DemoAgent { public static void main(String[] args) { StateGraph<AgentState> graph = new StateGraph<>(AgentState::new) .addNode("greet", DemoAgent::greet) .addNode("answer", DemoAgent::answer) .addEdge("greet", "answer") .addEdge("answer", END) .compile(); var result = graph.invoke(AgentState.builder().query("你好").build()); System.out.println(result.value("reply").orElse("")); } static Map<String, Object> greet(AgentState state) { return Map.of("greeting", "你好,我是 DemoAgent"); } static Map<String, Object> answer(AgentState state) { return Map.of("reply", state.query() + ",我来帮你查一下"); } }

AgentState 是一个普通 Java 类,包含 query、reply 这些字段。invoke 是入口,传入初始状态,走完整个图,返回最终状态。这个例子虽然简单,但已经能看出状态图的骨架:节点负责干活,边负责指路,State 负责传递数据。

3.4 带工具调用的 Agent 循环

真正的 Agent 能力来自“模型决定调工具 -> 执行工具 -> 回到模型”的循环。用状态图实现这个循环非常直观。

StateGraph<AgentState> graph = new StateGraph<>(AgentState::new) .addNode("agent", this::runModel) .addNode("tool", this::runTool) .addEdge("agent", "tool") .addConditionalEdge("tool", this::nextAfterTool, Map.of("continue", "agent", "done", END)) .compile();

这里的关键是条件边。runModel 节点调用大模型,解析输出:如果模型说要调用某个工具,就把工具名和参数写进 State 的 toolCalls 字段;如果没有工具调用,就直接生成最终回答。runTool 节点负责真正执行工具,把结果写回 State。

nextAfterTool 方法判断是否继续:

String nextAfterTool(AgentState state) { if (state.stepCount() < MAX_STEPS && state.hasPendingToolCalls()) { return "continue"; } return "done"; }

这个循环看起来像递归,但在 LangGraph4j 里,图执行器内部是迭代调度,不是 Java 方法递归,所以不用担心栈溢出。但要注意,业务上必须限制最大迭代次数。否则模型一旦陷入“反复调用同一个工具”的怪圈,你的钱包会比代码先崩溃。

4. 和 Spring AI 编排怎么选

4.1 简单对话任务用 Spring AI 就够

Spring AI 确实把 Java 接入大模型的门槛拉低了一大截。@Tool 注解注册工具、ChatClient 流式输出、Advisor 包装 RAG 和对话历史,这些功能开箱即用。我之前在公司内部做知识库问答机器人,纯 Spring AI,一个月上线,一点问题没有。

如果你只是做一个问答机器人,或者给内部系统加一个自然语言入口,真没必要上状态图。你需要的只是“接收问题 -> 检索知识库 -> 调用大模型 -> 返回答案”,这套流程用 Spring AI 的 API 编排非常顺手。

4.2 复杂流程交给 LangGraph4j

但到了多轮自主决策场景,情况就不一样了。Spring AI 的 ChatClient 每次调用基本是无状态的,流程控制还是得自己写。你可能会在 Service 里手写一个“意图 -> 策略 -> 工具调用 -> 重试”的大方法,刚开始还好,后面加需求会越来越痛苦。

LangGraph4j 正好补上这块:把节点、边、状态、持久化都变成一等公民。它不是为了替代 Spring AI,而是为了流程编排。Spring AI 管“怎么和模型对话”,LangGraph4j 管“什么时候该和模型对话”。

对比维度Spring AI 编排LangGraph4j
核心优势模型接入和工具注解状态管理和流程控制
分支循环自己写 if/while条件边天然支持
状态持久化弱配合 Checkpoint 强
可视化基本没有节点边可导出查看
学习曲线平缓有一定门槛
典型场景对话、RAG、工具调用多步骤规划、人工介入、审计

4.3 两者结合的现实套路

很多团队是两者混着用的,我也一样。Spring Boot 里正常注入 ChatClient,负责所有和模型交互的地方,包括工具定义和流式输出;LangGraph4j 负责整张状态图,流程的下一步是该调用模型还是执行工具,由状态图里的边决定。Node 内部直接调用 ChatClient 拿到结果。

这样做的最大好处是解耦。擅长 Spring 的同事继续写熟悉的代码,专门维护流程的人只需要关心节点和边。Spring AI 的 Tool 可以理解为“工具执行器”,LangGraph4j 只在合适的时机去触发它,两边各司其职。

5. 实操中的五个坑与排查方法

5.1 死循环:条件边一直在继续

这是所有人都会踩的坑,我也不例外。最常见的原因是 nextAfterTool 方法里判断条件写错,比如工具调用列表清空失败,导致 state.hasPendingToolCalls() 永远返回 true。

解决办法:每个涉及循环的图都必须带 stepCount 字段,超过上限强制返回 done。另外在 runModel 节点里把 stepCount 和当前工具调用列表打出来,一眼就能看出卡在哪一步。

5.2 条件边返回值映射不到节点

条件边返回一个字符串,图执行器要在配置的 Map 里找对应节点,找不到就直接抛异常。这个错误经常发生在字符串手写拼错的情况下,比如返回的 key 是 agent,Map 里存的却是 Agent。

我现在的做法是定义常量类,把所有节点名和路由 key 都做成 static final String,包括 agent、tool、continue、done 这种。然后在单元测试里遍历所有条件边,断言返回的 key 都能映射到已知节点,跑一遍就放心了。

5.3 共享状态被并发修改

默认情况下图是顺序执行节点的,但如果你引入了并行节点,或者某些节点本身起线程执行,State 对象就可能被多个线程同时读写。Java 里最容易踩坑的就是往 List 里 add 数据,另一个节点同时在遍历。

建议所有节点不修改传入的 State 对象,而是通过返回新 Map 的方式更新字段。如果返回的字段是 List,就创建一个包含旧数据加新数据的新 List。这样看起来多写几行代码,但能省掉并发问题带来的排查成本。

5.4 序列化与持久化问题

要用 Checkpoint 做断点续跑或者人工中断恢复,State 里所有字段必须可以序列化。Java 里自定义对象要实现 Serializable 接口,这很容易忽略。

我踩过一次坑:把 RedisTemplate 放进了 State,结果恢复时直接序列化异常,查了很久才发现。记住,State 里只放业务数据和轻量的快照对象,数据库连接、HTTP 客户端、大模型的客户端这类重资源一律不能进 State。这也符合状态图的设计原则:State 是账本,不是资源池。

5.5 调试依赖日志而不是断点

状态图的执行器会在一层循环里调度节点,传统的断点调试不太友好。我现在的做法是给每个 Node 统一打日志,输出节点名、关键字段、耗时。代码上可以封装一个 NodeWrapper,在真正执行前打印入参摘要,执行后打印出参摘要。

如果你们团队能把图结构导出成 DOT 格式,建议配合 Graphviz 可视化看流程。还有一个很实战的小技巧:把每次执行的关键 State 快照保存到本地文件,线上出问题后拿快照本地复跑,比看日志猜状态高效得多。

6. 一个完整案例:带人工确认的代码审查 Agent

6.1 业务场景

假设你要做一个代码审查 Agent:拿到一个 PR 编号,拉取 diff 内容,让大模型分析并生成评审意见,然后需要管理员人工确认是否采纳。确认通过后,Agent 自动把评审意见发到 PR 上并执行合并;确认拒绝,直接丢弃意见结束。

这个场景里天然有分支:人工确认的结果只有两个方向;也有循环:如果生成的评审意见质量不行,最多重试一次;还有人机协作:大模型不能替管理员做最终决定。非常适合用状态图实现。

6.2 状态与节点设计

先设计 State。一个简单的 Java 类,字段不要太多,但足够覆盖整个流程:

public class ReviewState { private String prNumber; private String diffContent; private List<String> comments = new ArrayList<>(); private boolean approved; private int attemptCount; }

节点分四个:fetch_diff,根据 prNumber 拉取 diff;review,调用大模型分析 diff,生成评审意见;human_approve,读取人工审批结果;apply_review,根据审批结果执行后续操作。

var graph = new StateGraph<>(ReviewState::new) .addNode("fetch_diff", this::fetchDiff) .addNode("review", this::reviewByLlm) .addNode("human_approve", this::humanApprove) .addNode("apply_review", this::applyReview) .addEdge("fetch_diff", "review") .addEdge("review", "human_approve") .addConditionalEdge("human_approve", state -> state.approved ? "approved" : "rejected", Map.of("approved", "apply_review", "rejected", END)) .addEdge("apply_review", END) .compile();

fetch_diff 和 review 都是普通节点,内部调用已有的 Service。review 节点会把大模型返回的评论列表写进 state 的 comments 字段。

6.3 条件边与中断恢复

human_approve 是这个案例里最核心的节点。它不实际等待,而是从外部“审批表”里读取人工审批结果。如果审批还没完成,图执行到这一步就停下来,State 快照被 Checkpoint 保存;管理员在系统里点完“通过”或“拒绝”后,图从断点继续执行。

这个设计比在代码里写一个阻塞队列或定时轮询要干净得多。状态图的恢复语义天然契合“人不在电脑前等你”的场景。具体到 LangGraph4j,Checkpoint 的落盘方式可以接 Redis 或数据库,把每个节点的 State 快照保存起来。

失败重试的逻辑也简单:review 节点里如果发现 attemptCount 小于阈值,并且评论内容为空,就重新调用一次大模型,attemptCount 加一。这本质上是一个自环边,状态图里画出来非常清楚。

6.4 这个案例的取舍

我做完这个案例最大的感受是:人工介入不再是一个特殊处理,而是一个普通节点。团队里其他同事看状态图,一眼就明白整个流程:拉取代码 -> 生成意见 -> 等人确认 -> 执行操作。

代价也不是没有。状态图多了之后,每个节点都要有单元测试,条件边要做路由测试,Checkpoint 要做持久化测试。如果是一个两个月就上线的临时工具,这些成本可能不划算。但对于要长期维护的业务流程,前期多写一点测试,后期省下的排查时间远超成本。

7. 我踩过几次坑之后留下的两个习惯

最后分享两个我养成的习惯,真的很管用。

第一,画图在先,写码在后。我不管用官方可视化、draw.io 还是直接在白板上画,一定先把节点和边画出来,状态字段就标注在旁边,然后再动代码。画的过程中你就能发现哪些状态字段是多余的,哪些边根本走不到。先画图再编码,看起来多了一步,实际上能改掉大半流程设计错误。

第二,给每个节点写一个最小单元测试。喂一个构造好的 State,断言返回的 Map 字段和下一个路由 key。状态图框架很容易让人觉得“图是整体的,测不了局部”,其实完全相反。每个 Node 都是纯函数般的单元,独立测试非常舒服。

如果看完你还不确定该不该上状态图,那就用最笨的标准判断:你的流程里是不是有两个以上不同方向的下一步?是不是有“失败后换一条路重试”的诉求?是不是需要人来拍板?只要中了其中一条,就可以用 LangGraph4j 替换掉手写流程;如果全都没有,普通 Service 代码就是最好的方案。状态图是个好工具,但它不是拿来炫技的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询