LangGraph4j 是一个用于构建有状态、基于图的 AI 应用的 Java 库。图由执行工作的节点、选择下一个节点的边,以及随图运行而变化的共享状态组成。它的设计受到 LangGraph 启发,并与 LangChain4j 和 Spring AI 集成,但图节点也可以使用普通 Java 代码。
+------------+
| call model |
+------+-----+
|
tool requested?
/ \
yes no
/ \
+-------v-------+ +--v--+
| execute tool | | END |
+-------+-------+ +-----+
|
+----------> call model循环是有意设计的。循环使应用能够进行推理、执行操作、检查结果,然后再次推理。LangGraph4j 将这种控制流及其状态显式化,并提供 reducer、条件路由、检查点、断点、流式处理、图可视化和子图。
本教程使用 LangGraph4j 1.8.24 和 Java 17。教程聚焦于核心模块,因此示例不依赖特定的模型提供商或 Web 框架。
LangGraph4j 解决的问题 #
一个小型 AI 功能可以是单次请求:
request -> prompt -> model -> response代理型应用可能需要:
- 对请求进行分类;
- 检索私有文档;
- 让模型选择工具;
- 验证并执行该工具;
- 重试可恢复的失败;
- 在高风险操作前暂停;
- 跨请求保留进度;
- 恢复同一次运行;以及
- 在处理过程中报告进度。
不使用图库,也可以通过循环、条件、队列和数据库表实现这些功能。困难之处在于:即使模型的决策具有非确定性,也要让执行过程易于理解、可中断、可恢复且安全。
LangGraph4j 提供执行模型,但不会替你选择工作流、提示词、工具、授权规则或业务策略。这些仍然属于应用的职责。
图不限于 DAG #
有向无环图(DAG)适合单向流水线:
load -> transform -> validate -> publish代理循环通常是循环结构:
model -> tool -> model -> tool -> model -> answerLangGraph4j 同时支持这两种形式。图可以分支、循环、并行运行相互独立的分支、合并结果、调用子图、在配置的节点处暂停并最终完成。路由既可以是确定性的 Java 代码,也可以是经过验证的模型决策结果。
运行时遵循类似 Pregel 的 superstep 模型。一个步骤中被激活的节点可以并发运行,然后在后续工作开始前合并它们的更新。因此,共享状态的合并规则非常重要。
核心概念 #
状态 #
AgentState 是贯穿图运行过程的共享数据。它封装了一个 Map<String, Object>,通常通过类型化访问器进行扩展:
import java.util.List;
import java.util.Map;
import org.bsc.langgraph4j.state.AgentState;
final class SupportState extends AgentState {
static final String REQUEST = "request";
static final String DOCUMENTS = "documents";
static final String PROPOSED_ACTION = "proposed_action";
static final String APPROVED = "approved";
static final String ATTEMPTS = "attempts";
static final String ANSWER = "answer";
SupportState(Map<String, Object> data) {
super(data);
}
String request() {
return value(REQUEST).map(Object::toString).orElse("");
}
List<String> documents() {
return this.<List<String>>value(DOCUMENTS).orElse(List.of());
}
boolean approved() {
return this.<Boolean>value(APPROVED).orElse(false);
}
}节点返回更新,而不是替换完整的状态对象:
Map<String, Object> incrementAttempt(SupportState state) {
int attempts = state.<Integer>value(SupportState.ATTEMPTS).orElse(0);
return Map.of(SupportState.ATTEMPTS, attempts + 1);
}状态不仅是聊天历史。将决策、标识符、工件和状态保存在命名字段中,比从文本消息中恢复这些信息更容易验证。
不要将开放连接、执行器、请求对象或机密信息放入检查点状态。检查点必须可序列化,并且可能被运维人员检查,或持久化到进程之外。
通道和 Reducer #
状态模式将一个键映射到 Channel<?>。通道定义该键的默认值以及如何合并更新。使用基础通道时,新值会替换旧值。Channels.appender 会将值累积到列表中。
import java.util.ArrayList;
import java.util.Map;
import org.bsc.langgraph4j.state.Channel;
import org.bsc.langgraph4j.state.Channels;
static final Map<String, Channel<?>> SCHEMA = Map.of(
"status", Channels.base(() -> "new"),
"audit", Channels.appender(ArrayList::new));如果一个节点返回 Map.of("audit", "classified"),后续节点返回 Map.of("audit", "retrieved"),appender 通道会保留两个条目。标准 appender 不允许重复值;如果重复值具有意义,请使用 Channels.appenderWithDuplicate。
当并行分支写入同一个键时,reducer 至关重要。由于检查点恢复和重放可能会再次应用相同的状态转换,reducer 应当是确定性的,并且不产生副作用。
对于聊天消息,请使用适当的 LangChain4j 或 Spring AI 集成提供的消息通道功能,而不要将消息编辑视为盲目的列表拼接。
节点 #
节点接收当前状态,并返回更新映射。它可以调用模型、查询向量存储、调用工具、验证数据、生成审计条目或调用子图。
NodeAction<S> 是同步的。AsyncNodeAction<S> 返回 CompletableFuture,适合非阻塞的模型、数据库和 HTTP 客户端。node_async 适配器可将同步操作转换为异步节点操作。
保持节点小而专注。让一个节点同时负责分类、检索、调用多个工具、写入一行数据和生成回答,会将工作流隐藏在一个方法中,从而使检查点和失败难以解释。
边 #
普通边始终选择同一个目标:
graph.addEdge("retrieve", "generate");条件边会评估状态并返回路由标签。图会将该标签映射到声明的目标:
import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async;
graph.addConditionalEdges(
"review",
edge_async(state -> state.approved() ? "publish" : "reject"),
Map.of("publish", "publish", "reject", "reject"));该映射是允许列表。不要将模型生成的字符串用作不受限制的目标或函数名。应先验证模型输出,再将少量接受的决策映射到图节点。
StateGraph.START 和 StateGraph.END 标记图的入口和终止位置。在定义节点和边后调用 compile()。编译过程会验证结构,并生成可运行的 CompiledGraph。
安装 LangGraph4j #
LangGraph4j 1.8.x 要求 Java 17 或更高版本。导入 BOM,以保持核心模块和可选模块的版本一致:
<properties>
<langgraph4j.version>1.8.24</langgraph4j.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-bom</artifactId>
<version>${langgraph4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-core</artifactId>
</dependency>
</dependencies>仅在应用需要时添加集成模块。LangGraph4j 可以编排使用提供商 SDK、LangChain4j、Spring AI、数据库客户端或普通应用代码的节点。
完整的 Java 图 #
下面的确定性图会起草发布公告、审核公告,然后发布或拒绝。简单的起草逻辑让图的机制保持清晰;生产代码可以将起草节点替换为模型调用。
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.bsc.langgraph4j.GraphStateException;
import org.bsc.langgraph4j.StateGraph;
import org.bsc.langgraph4j.action.NodeAction;
import org.bsc.langgraph4j.state.AgentState;
import org.bsc.langgraph4j.state.Channel;
import org.bsc.langgraph4j.state.Channels;
import static org.bsc.langgraph4j.StateGraph.END;
import static org.bsc.langgraph4j.StateGraph.START;
import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async;
import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async;
final class ReleaseState extends AgentState {
static final String RELEASE_ID = "release_id";
static final String NOTES = "notes";
static final String DRAFT = "draft";
static final String APPROVED = "approved";
static final String STATUS = "status";
static final String AUDIT = "audit";
static final Map<String, Channel<?>> SCHEMA = Map.of(
STATUS, Channels.base(() -> "new"),
AUDIT, Channels.appender(ArrayList::new));
ReleaseState(Map<String, Object> data) {
super(data);
}
String releaseId() {
return value(RELEASE_ID).map(Object::toString).orElseThrow();
}
String notes() {
return value(NOTES).map(Object::toString).orElse("");
}
String draft() {
return value(DRAFT).map(Object::toString).orElseThrow();
}
boolean approved() {
return this.<Boolean>value(APPROVED).orElse(false);
}
List<String> audit() {
return this.<List<String>>value(AUDIT).orElse(List.of());
}
}
public final class ReleaseGraph {
private static final NodeAction<ReleaseState> DRAFT = state -> Map.of(
ReleaseState.DRAFT,
"Release %s%n%n%s".formatted(state.releaseId(), state.notes()),
ReleaseState.STATUS, "awaiting_review",
ReleaseState.AUDIT, "drafted");
private static final NodeAction<ReleaseState> REVIEW = state -> Map.of(
ReleaseState.AUDIT, "reviewed");
private static final NodeAction<ReleaseState> PUBLISH = state -> {
// Call a publication API here with releaseId() as an idempotency key.
return Map.of(
ReleaseState.STATUS, "published",
ReleaseState.AUDIT, "published");
};
private static final NodeAction<ReleaseState> REJECT = state -> Map.of(
ReleaseState.STATUS, "rejected",
ReleaseState.AUDIT, "rejected");
static StateGraph<ReleaseState> definition() throws GraphStateException {
return new StateGraph<>(
ReleaseState.SCHEMA,
ReleaseState::new)
.addNode("draft", node_async(DRAFT))
.addNode("review", node_async(REVIEW))
.addNode("publish", node_async(PUBLISH))
.addNode("reject", node_async(REJECT))
.addEdge(START, "draft")
.addEdge("draft", "review")
.addConditionalEdges(
"review",
edge_async(state ->
state.approved() ? "publish" : "reject"),
Map.of(
"publish", "publish",
"reject", "reject"))
.addEdge("publish", END)
.addEdge("reject", END);
}
public static void main(String[] args) throws GraphStateException {
var graph = definition().compile();
var input = Map.<String, Object>of(
ReleaseState.RELEASE_ID, "2026.08",
ReleaseState.NOTES, "Adds resumable exports and audit reports.",
ReleaseState.APPROVED, true);
for (var output : graph.stream(input)) {
System.out.println(output);
}
}
}图从 draft 开始,移动到 review,然后评估条件边。真实的审核流程不应直接接受来自不受信任浏览器请求的 approved。必须将经过身份验证的审批者绑定到特定的拟议操作,并在服务器上验证该决定。
PUBLISH 节点特意与面向模型的工作分离。它必须将 release_id 作为幂等键,验证最终负载,并在产生外部副作用之前执行授权。
条件路由和有界循环 #
条件边可以显式表示重试或工具循环:
call model -> tool needed? -- yes --> execute tool --+
| |
no |
v |
answer <----------------------------------------+路由操作应返回少量标签中的一个。对于重试循环,将尝试次数保存在状态中,并在达到限制后路由到失败节点。CompileConfig.builder().recursionLimit(...) 也会设置图迭代次数的正上限;默认值为 25。
不要依赖模型来停止昂贵或危险的循环。在应用状态或可观测性数据中跟踪模型调用、工具调用、经过的时间、令牌使用量和重试次数。每个循环都需要成功路径、有界重试路径和失败路径。
检查点、断点和恢复 #
检查点保存器会根据 RunnableConfig 的线程 ID 保存快照。这样,应用可以检查状态,在配置的节点处停止,并在稍后继续同一线程。
import java.util.Map;
import org.bsc.langgraph4j.CompileConfig;
import org.bsc.langgraph4j.RunnableConfig;
import org.bsc.langgraph4j.checkpoint.MemorySaver;
var compiled = ReleaseGraph.definition()
.compile(CompileConfig.builder()
.checkpointSaver(new MemorySaver())
.interruptBefore("publish")
.build());
var thread = RunnableConfig.builder()
.threadId("release-2026.08")
.build();
for (var output : compiled.stream(
Map.of(
ReleaseState.RELEASE_ID, "2026.08",
ReleaseState.NOTES, "Adds resumable exports and audit reports.",
ReleaseState.APPROVED, true),
thread)) {
System.out.println(output);
}使用 interruptBefore("publish") 后,执行会在指定节点之前停止。MemorySaver 仅适用于本地开发,因为进程结束时它会丢失所有状态。LangGraph4j 还提供面向 PostgreSQL、SQLite、MySQL、Redis、DynamoDB、CockroachDB、Hazelcast 和 Oracle Database 等数据库与存储的保存器模块。应根据应用的可用性、保留、事务和运维要求选择后端。
要编辑已暂停的线程,请持久化经过身份验证的审批决定,并更新检查点状态。updateState 会返回一个用于标识更新后检查点的配置;提供作出决定的节点后,运行时可以计算下一条路由:
RunnableConfig resumed = compiled.updateState(
thread,
Map.of(ReleaseState.APPROVED, true),
"review");
for (var output : compiled.stream(null, resumed)) {
System.out.println(output);
}向此 stream 重载传入 null 会恢复已保存的图输入。在服务中,应将线程 ID 与其租户和所有者一同保存在服务器端。验证访问权限之前,绝不要加载客户端任意提供的线程 ID。
中断是节点级断点,而不是授权系统。审批界面必须显示具体且已验证的操作及其参数。如果操作会部署服务、删除数据或改变资金,仅显示“继续?”并不构成有意义的同意。
持久化不等于 Exactly Once #
检查点可以恢复计算,但检查点和外部副作用不会自动成为同一个事务:
1. publish node calls an external API
2. process fails before the following checkpoint
3. the node runs again during recovery如果没有幂等键或权威事务记录,副作用可能发生两次。发送电子邮件、修改基础设施或改变业务系统的节点必须是幂等的。将模型调用视为可重复但非确定性的操作:重放可能产生费用,并返回不同的输出。
流式处理和状态检查 #
CompiledGraph.stream 会在图运行时产生一系列 NodeOutput 值。完整示例会打印每个值;Web 应用可以将安全的进度事件转换为 Server-Sent Events 或 WebSocket 消息。
默认情况下,图流式处理使用 CompiledGraph.StreamMode.VALUES。配置保存器后,streamSnapshots 或使用 StreamMode.SNAPSHOTS 的 RunnableConfig 会提供面向检查点的状态快照。
import org.bsc.langgraph4j.CompiledGraph;
import org.bsc.langgraph4j.RunnableConfig;
var config = RunnableConfig.builder()
.threadId("release-2026.08")
.streamMode(CompiledGraph.StreamMode.SNAPSHOTS)
.build();
for (var output : compiled.streamSnapshots(
Map.of(ReleaseState.APPROVED, true),
config)) {
System.out.println(output);
}不要将每个状态字段都序列化到浏览器。状态可能包含检索到的文档、工具参数、内部标识符或节点意外添加的数据。应定义明确的事件 DTO,只包含特定接收者可以看到的字段。
CompiledGraph.getState、stateOf、lastStateOf 和 getStateHistory 可检查线程的已保存状态。将这些接口视为特权运维工具,并应用与其他应用数据相同的租户隔离、保留、删除和加密规则。
并行节点和合并规则 #
多个出站边可以在同一个 superstep 中激活相互独立的工作:
+-> search internal docs -+
question -> route +-> search issue tracker --+-> combine -> answer
+-> search source code ----+只有在分支相互独立时,并行化才有价值。分支的写入必须可合并:通道可以追加候选项,自定义基础通道 reducer 可以合并带命名空间的映射,而单个所有者可以写入最终答案。
并行模型请求会增加成本,并可能耗尽提供商限制。使用应用执行器限制并发,传播取消信号,并决定一个分支失败而另一个分支成功时的处理方式。并行节点本身不能拥有互不兼容的多个下游路由,因此应设计一个合并节点,以确定下一步的确定性决策。
子图和多代理工作流 #
可以将 StateGraph 或 CompiledGraph 添加为另一个图的节点。子图适用于可复用的研究流程、专用工具循环、团队负责的模块或可单独测试的工作流。
StateGraph<ReleaseState> child = new StateGraph<>(
ReleaseState.SCHEMA,
ReleaseState::new);
StateGraph<ReleaseState> parent = new StateGraph<>(
ReleaseState.SCHEMA,
ReleaseState::new)
.addNode("prepare_release", child);父图和这种形式的子图共享同一个状态类型。在组合图之前,应明确状态所有权和收窄后的接口。不要为普通的确定性函数创建单独的、由模型协调的代理;这样会增加延迟、成本和非确定性,却不会改善控制流。
进程内子图不是独立的远程代理。A2A 处理跨代理服务边界的通信。MCP 向代理公开工具和资源。LangGraph4j 编排使用任一协议的 Java 工作流。
LangGraph4j、LangChain4j 和 Spring AI #
这些组件处于不同层次:
| 组件 | 主要作用 |
|---|---|
| LangGraph4j | Java 状态图、路由、检查点、断点和执行 |
| LangChain4j | Java 模型、消息、工具、检索和代理抽象 |
| Spring AI | Spring 模型客户端、工具、RAG、向量存储、MCP 和可观测性 |
| LangGraph4j Studio | 图可视化、执行和调试 UI |
LangGraph4j 是受 LangGraph 启发的独立项目;它不是另一个运行时的即插即用实现,也不共享检查点或部署兼容性保证。
当 LangChain4j 或 Spring AI 的提供商适配器、消息抽象、工具或检索支持适合应用时,可以在图节点内使用它们。如果使用普通 Java 客户端更简单,则应直接使用。应用需要显式循环、有状态路由、持久检查点或受控暂停时,图运行时才有价值;基本聊天和检索不需要图运行时。
部署和运维 #
LangGraph4j 是嵌入 Java 服务的库,由团队构建和运营该服务。团队负责:
- 检查点存储、迁移、保留和删除;
- 排队、并发限制、取消和超时;
- 身份验证和线程授权;
- 重试策略和幂等性;
- 流式端点和重连行为;
- 可观测性、告警和成本控制;
- 模型和工具凭据;以及
- 安全关闭和恢复。
LangGraph4j 支持 Mermaid 和 PlantUML 图表示,并提供可嵌入的 Studio 项目用于可视化检查。这些功能对开发有帮助,但不能替代生产指标、跟踪、审计日志或授权检查。
将编译后的图注册为 Spring 单例时,应保持其节点和依赖项无状态且线程安全。每次运行的数据应放在图输入和检查点存储中,而不是放在单例的可变字段中。
设计指南 #
将确定性决策保留在 Java 中 #
使用模型进行语义分类和生成。对于授权、算术、配额、模式验证和不变量约束,应使用普通 Java 代码。
使状态显式化 #
将重要决策保存到命名字段中,而不是解析旧消息。记录每个字段由哪个节点负责,以及并发更新如何合并。
为每个循环设置上限 #
跟踪尝试次数、模型调用、经过时间、令牌和工具调用。为每个循环设置终止条件和错误路径。
使副作用具备幂等性 #
重试和恢复是正常情况。对外部可见的副作用使用稳定的操作 ID 和权威记录。
分离提议和执行 #
让模型提出操作。在由独立节点执行操作之前,先进行验证、授权并获取所需批准。
只持久化可治理的数据 #
对检查点应用保留、加密、删除和租户隔离规则。图状态可能包含提示词、个人数据、检索到的文档和工具结果。
评估轨迹 #
即使图使用了错误的工具、检索了禁止访问的数据、进行了过多重试或忽略了必要审核,最终回答也可能看起来正确。除最终输出外,还应评估路由、状态转换、工具参数、证据、成本和延迟。
对图和状态进行版本管理 #
线程可以跨部署持续存在。后续版本可能重命名节点或更改状态模式,而已保存的检查点仍引用旧图。在更改生产图之前,应定义迁移和兼容性策略。
不应使用 LangGraph4j 的场景 #
以下情况不要添加图运行时:
- 一次模型调用已经足够;
- 简短的确定性流水线已经清晰;
- 普通 Spring AI 或 LangChain4j 代码已经满足需求;
- 传统工作流引擎已经负责持久化业务流程;
- 无需在请求之间保留状态;或
- 团队无法运营和评估额外的有状态系统。
对于金融或履约流程,传统工作流引擎可以继续作为权威系统,而图负责执行有界的 AI 分析:
durable business workflow
-> starts an AI analysis graph
-> receives a proposed decision
-> applies deterministic policy and approval
-> performs the authoritative transaction应选择能够表达所需控制流的最小抽象。当工作流确实需要分支、循环、暂停和恢复时,图很有价值;否则,它可能只是围绕已经清晰的代码构建了一个冗长的流程图。
评论
在 GitHub 上阅读或评论(发表评论需要登录)