LangGraph4j 是一個 Java 程式庫, 用於建構具狀態、以圖為基礎的 AI 應用程式。圖由執行工作的節點、 選擇下一個節點的邊,以及隨圖執行而變更的共享狀態所組成。 它的設計靈感來自 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 #
有向無環圖(directed acyclic graph,DAG)適合單向管線:
load -> transform -> validate -> publish代理迴圈則往往具有循環:
model -> tool -> model -> tool -> model -> answerLangGraph4j 支援這兩種形式。圖可以分支、循環、平行執行互相獨立的分支、 合併結果、叫用子圖、在設定的節點暫停,然後結束。 路由可以由具決定性的 Java 程式碼決定,也可以採用經驗證的模型決策結果。
執行階段採用類似 Pregel 的超級步驟模型。在一個步驟中啟用的節點可以 同時執行,接著合併其更新,再執行後續工作。因此,共享狀態的合併規則 相當重要。
核心概念 #
狀態 #
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"),附加器通道會保留兩筆項目。
標準附加器不允許重複值;若重複值具有意義,請使用
Channels.appenderWithDuplicate。
當平行分支寫入相同索引鍵時,reducer 至關重要。它們應具決定性且不含 副作用,因為檢查點復原與重播可能再次套用相同的狀態轉換。
處理聊天訊息時,請使用對應 LangChain4j 或 Spring AI 整合所提供的 訊息通道功能,而不要將訊息編輯視為不加判斷的清單串接。
節點 #
節點接收目前狀態,並傳回更新的 map。它可以呼叫模型、查詢 向量儲存庫、叫用工具、驗證資料、發出稽核項目, 或呼叫子圖。
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);
}將 null 傳給這個 stream 多載會繼續執行已儲存的圖輸入。
在服務中,請將執行緒 ID 連同其租戶與擁有者儲存在伺服器端。
除非已驗證存取權,否則絕不可載入任意由用戶端提供的執行緒 ID。
中斷是節點層級的中斷點,不是授權系統。核准畫面必須顯示具體且 已驗證的動作及其引數。如果動作會部署服務、刪除資料或變更金錢, 「繼續?」並不構成有意義的同意。
持久化不代表恰好執行一次 #
檢查點可以繼續計算,但檢查點和外部副作用並不會自動成為同一筆交易:
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
可檢查執行緒已儲存的狀態。請將這些介面視為具有特殊權限的維運工具,
並套用其他應用程式資料所採用的相同租戶隔離、保留、刪除及加密規則。
平行節點與合併規則 #
多條向外延伸的邊可以在同一個超級步驟中啟用互相獨立的工作:
+-> search internal docs -+
question -> route +-> search issue tracker --+-> combine -> answer
+-> search source code ----+只有在分支互相獨立時,平行處理才有用。各分支的寫入必須可合併: 通道可以附加候選項目、自訂基礎通道 reducer 可以合併具命名空間的 map, 而單一擁有者可以寫入最終答案。
平行模型請求會增加成本,也可能耗盡供應商的限制。請使用應用程式執行器 限制並行程度、傳播取消動作,並決定當一個分支失敗而另一個成功時的處理方式。 平行節點本身不能具有彼此不相容的多條下游路由,因此請設計由合併節點 做出下一個具決定性的決策。
子圖與多代理工作流程 #
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 singleton 時,請讓其節點與相依項目 保持無狀態且具執行緒安全性。每次執行的資料應置於圖輸入和檢查點 儲存空間中,而非 singleton 的可變欄位。
設計準則 #
在 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 上閱讀或評論(發表評論需要登入)