LangGraph4j

最后更新: 2026-04-22 22:26:49 +0800 +0800
原文以英文撰写。您正在阅读AI翻译版本。

LangGraph4j 是一个用于构建有状态、基于图的 AI 应用的 Java 库。图由执行工作的节点、选择下一个节点的边,以及随图运行而变化的共享状态组成。它的设计受到 LangGraph 启发,并与 LangChain4j 和 Spring AI 集成,但图节点也可以使用普通 Java 代码。

text
                    +------------+
                    | call model |
                    +------+-----+
                           |
                    tool requested?
                     /           \
                   yes            no
                   /               \
          +-------v-------+      +--v--+
          | execute tool  |      | END |
          +-------+-------+      +-----+
                  |
                  +----------> call model

循环是有意设计的。循环使应用能够进行推理、执行操作、检查结果,然后再次推理。LangGraph4j 将这种控制流及其状态显式化,并提供 reducer、条件路由、检查点、断点、流式处理、图可视化和子图。

本教程使用 LangGraph4j 1.8.24 和 Java 17。教程聚焦于核心模块,因此示例不依赖特定的模型提供商或 Web 框架。

LangGraph4j 解决的问题 #

一个小型 AI 功能可以是单次请求:

text
request -> prompt -> model -> response

代理型应用可能需要:

  1. 对请求进行分类;
  2. 检索私有文档;
  3. 让模型选择工具;
  4. 验证并执行该工具;
  5. 重试可恢复的失败;
  6. 在高风险操作前暂停;
  7. 跨请求保留进度;
  8. 恢复同一次运行;以及
  9. 在处理过程中报告进度。

不使用图库,也可以通过循环、条件、队列和数据库表实现这些功能。困难之处在于:即使模型的决策具有非确定性,也要让执行过程易于理解、可中断、可恢复且安全。

LangGraph4j 提供执行模型,但不会替你选择工作流、提示词、工具、授权规则或业务策略。这些仍然属于应用的职责。

图不限于 DAG #

有向无环图(DAG)适合单向流水线:

text
load -> transform -> validate -> publish

代理循环通常是循环结构:

text
model -> tool -> model -> tool -> model -> answer

LangGraph4j 同时支持这两种形式。图可以分支、循环、并行运行相互独立的分支、合并结果、调用子图、在配置的节点处暂停并最终完成。路由既可以是确定性的 Java 代码,也可以是经过验证的模型决策结果。

运行时遵循类似 Pregel 的 superstep 模型。一个步骤中被激活的节点可以并发运行,然后在后续工作开始前合并它们的更新。因此,共享状态的合并规则非常重要。

核心概念 #

状态 #

AgentState 是贯穿图运行过程的共享数据。它封装了一个 Map<String, Object>,通常通过类型化访问器进行扩展:

java
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);
    }
}

节点返回更新,而不是替换完整的状态对象:

java
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 会将值累积到列表中。

java
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 适配器可将同步操作转换为异步节点操作。

保持节点小而专注。让一个节点同时负责分类、检索、调用多个工具、写入一行数据和生成回答,会将工作流隐藏在一个方法中,从而使检查点和失败难以解释。

边 #

普通边始终选择同一个目标:

java
graph.addEdge("retrieve", "generate");

条件边会评估状态并返回路由标签。图会将该标签映射到声明的目标:

java
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,以保持核心模块和可选模块的版本一致:

xml
<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 图 #

下面的确定性图会起草发布公告、审核公告,然后发布或拒绝。简单的起草逻辑让图的机制保持清晰;生产代码可以将起草节点替换为模型调用。

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 作为幂等键,验证最终负载,并在产生外部副作用之前执行授权。

条件路由和有界循环 #

条件边可以显式表示重试或工具循环:

text
call model -> tool needed? -- yes --> execute tool --+
      |                                              |
      no                                             |
      v                                              |
     answer <----------------------------------------+

路由操作应返回少量标签中的一个。对于重试循环,将尝试次数保存在状态中,并在达到限制后路由到失败节点。CompileConfig.builder().recursionLimit(...) 也会设置图迭代次数的正上限;默认值为 25。

不要依赖模型来停止昂贵或危险的循环。在应用状态或可观测性数据中跟踪模型调用、工具调用、经过的时间、令牌使用量和重试次数。每个循环都需要成功路径、有界重试路径和失败路径。

检查点、断点和恢复 #

检查点保存器会根据 RunnableConfig 的线程 ID 保存快照。这样,应用可以检查状态,在配置的节点处停止,并在稍后继续同一线程。

java
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 会返回一个用于标识更新后检查点的配置;提供作出决定的节点后,运行时可以计算下一条路由:

java
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 #

检查点可以恢复计算,但检查点和外部副作用不会自动成为同一个事务:

text
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 会提供面向检查点的状态快照。

java
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 中激活相互独立的工作:

text
                   +-> search internal docs -+
question -> route  +-> search issue tracker --+-> combine -> answer
                   +-> search source code ----+

只有在分支相互独立时,并行化才有价值。分支的写入必须可合并:通道可以追加候选项,自定义基础通道 reducer 可以合并带命名空间的映射,而单个所有者可以写入最终答案。

并行模型请求会增加成本,并可能耗尽提供商限制。使用应用执行器限制并发,传播取消信号,并决定一个分支失败而另一个分支成功时的处理方式。并行节点本身不能拥有互不兼容的多个下游路由,因此应设计一个合并节点,以确定下一步的确定性决策。

子图和多代理工作流 #

可以将 StateGraph 或 CompiledGraph 添加为另一个图的节点。子图适用于可复用的研究流程、专用工具循环、团队负责的模块或可单独测试的工作流。

java
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 #

这些组件处于不同层次:

组件主要作用
LangGraph4jJava 状态图、路由、检查点、断点和执行
LangChain4jJava 模型、消息、工具、检索和代理抽象
Spring AISpring 模型客户端、工具、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 分析:

text
durable business workflow
  -> starts an AI analysis graph
  -> receives a proposed decision
  -> applies deterministic policy and approval
  -> performs the authoritative transaction

应选择能够表达所需控制流的最小抽象。当工作流确实需要分支、循环、暂停和恢复时,图很有价值;否则,它可能只是围绕已经清晰的代码构建了一个冗长的流程图。

延伸阅读 #

评论

在 GitHub 上阅读或评论(发表评论需要登录)