LangGraph4j

最後更新: 2026-04-22 22:26:49 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

LangGraph4j 是一個 Java 程式庫, 用於建構具狀態、以圖為基礎的 AI 應用程式。圖由執行工作的節點、 選擇下一個節點的邊,以及隨圖執行而變更的共享狀態所組成。 它的設計靈感來自 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 #

有向無環圖(directed acyclic graph,DAG)適合單向管線:

text
load -> transform -> validate -> publish

代理迴圈則往往具有循環:

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

LangGraph4j 支援這兩種形式。圖可以分支、循環、平行執行互相獨立的分支、 合併結果、叫用子圖、在設定的節點暫停,然後結束。 路由可以由具決定性的 Java 程式碼決定,也可以採用經驗證的模型決策結果。

執行階段採用類似 Pregel 的超級步驟模型。在一個步驟中啟用的節點可以 同時執行,接著合併其更新,再執行後續工作。因此,共享狀態的合併規則 相當重要。

核心概念 #

狀態 #

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"),附加器通道會保留兩筆項目。 標準附加器不允許重複值;若重複值具有意義,請使用 Channels.appenderWithDuplicate。

當平行分支寫入相同索引鍵時,reducer 至關重要。它們應具決定性且不含 副作用,因為檢查點復原與重播可能再次套用相同的狀態轉換。

處理聊天訊息時,請使用對應 LangChain4j 或 Spring AI 整合所提供的 訊息通道功能,而不要將訊息編輯視為不加判斷的清單串接。

節點 #

節點接收目前狀態,並傳回更新的 map。它可以呼叫模型、查詢 向量儲存庫、叫用工具、驗證資料、發出稽核項目, 或呼叫子圖。

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

將 null 傳給這個 stream 多載會繼續執行已儲存的圖輸入。 在服務中,請將執行緒 ID 連同其租戶與擁有者儲存在伺服器端。 除非已驗證存取權,否則絕不可載入任意由用戶端提供的執行緒 ID。

中斷是節點層級的中斷點,不是授權系統。核准畫面必須顯示具體且 已驗證的動作及其引數。如果動作會部署服務、刪除資料或變更金錢, 「繼續?」並不構成有意義的同意。

持久化不代表恰好執行一次 #

檢查點可以繼續計算,但檢查點和外部副作用並不會自動成為同一筆交易:

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 可檢查執行緒已儲存的狀態。請將這些介面視為具有特殊權限的維運工具, 並套用其他應用程式資料所採用的相同租戶隔離、保留、刪除及加密規則。

平行節點與合併規則 #

多條向外延伸的邊可以在同一個超級步驟中啟用互相獨立的工作:

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

只有在分支互相獨立時,平行處理才有用。各分支的寫入必須可合併: 通道可以附加候選項目、自訂基礎通道 reducer 可以合併具命名空間的 map, 而單一擁有者可以寫入最終答案。

平行模型請求會增加成本,也可能耗盡供應商的限制。請使用應用程式執行器 限制並行程度、傳播取消動作,並決定當一個分支失敗而另一個成功時的處理方式。 平行節點本身不能具有彼此不相容的多條下游路由,因此請設計由合併節點 做出下一個具決定性的決策。

子圖與多代理工作流程 #

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 singleton 時,請讓其節點與相依項目 保持無狀態且具執行緒安全性。每次執行的資料應置於圖輸入和檢查點 儲存空間中,而非 singleton 的可變欄位。

設計準則 #

在 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 上閱讀或評論(發表評論需要登入)