版本:LangGraph4j 1.8.25
目标:蓝图
compile成CompiledGraph之后,怎么用invoke/stream跑起来;两套 Config 各管什么;跑的时候状态怎么一步步变。本章不接模型、不落盘。

先分清两套配置:CompileConfig 在编译时定一次;RunnableConfig 每次运行可换。中间的 CompiledGraph 编译一次,反复 invoke / stream。
18.1 compile:从蓝图到可执行实例
StateGraph 是可变蓝图。对外跑的是:
CompiledGraph<S> compiled = stateGraph.compile();
// 或带配置:
CompiledGraph<S> compiled = stateGraph.compile(compileConfig);
compile 大致做三件事:
-
校验拓扑:有没有从
START出去的边、边是否指向不存在的节点、条件映射是否为空等。失败抛GraphStateException。 -
收成可执行结构:节点动作和边整理进内部表,运行时按表推进。
-
固化
CompileConfig:这份配置跟着CompiledGraph走,运行时不再改。
无参 compile() 使用默认 CompileConfig,其中 recursionLimit = 25。
CompileConfig compileConfig = CompileConfig.builder()
.recursionLimit(25)
.graphId("hello-run") // 进程里多张图并存时好区分,可选
.build();
| 项 | 含义 |
|---|---|
recursionLimit | 单次运行最多走多少步,默认 25,必须 > 0 |
graphId | 这张图的标识,可选 |
实践三点:
-
compile 一次,复用
CompiledGraph。不要每个请求重新addNode/addEdge再 compile。 -
compile 之后不要再改原来的
StateGraph,还指望已发布的实例跟着变。要改拓扑,改完再 compile 出新实例。 -
在 Spring 里通常做成单例 Bean;请求里只换输入和
RunnableConfig。
18.2 一整段能跑的代码
下面用 START → greet → polish → END。先跑通,后面几节按这段拆。
import static org.bsc.langgraph4j.StateGraph.END;
import static org.bsc.langgraph4j.StateGraph.START;
import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.bsc.langgraph4j.CompileConfig;
import org.bsc.langgraph4j.CompiledGraph;
import org.bsc.langgraph4j.GraphRepresentation;
import org.bsc.langgraph4j.RunnableConfig;
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;
public class RunCompiledGraph {
public static class DemoState extends AgentState {
public static final String REPLY = "reply";
public static final String LOGS = "logs";
public static final Map<String, Channel<?>> SCHEMA = Map.of(
REPLY, Channels.base(() -> ""),
LOGS, Channels.appenderWithDuplicate(ArrayList::new)
);
public DemoState(Map<String, Object> initData) {
super(initData);
}
}
public static void main(String[] args) throws Exception {
NodeAction<DemoState> greet = state -> {
String name = state.<String>value("name").orElse("world");
return Map.of(
DemoState.REPLY, "Hello, " + name + "!",
DemoState.LOGS, List.of("greet")
);
};
NodeAction<DemoState> polish = state -> Map.of(
DemoState.REPLY, state.<String>value(DemoState.REPLY).orElse("") + " ✓",
DemoState.LOGS, List.of("polish")
);
CompiledGraph<DemoState> compiled = new StateGraph<>(DemoState.SCHEMA, DemoState::new)
.addNode("greet", node_async(greet))
.addNode("polish", node_async(polish))
.addEdge(START, "greet")
.addEdge("greet", "polish")
.addEdge("polish", END)
.compile(CompileConfig.builder()
.recursionLimit(25)
.graphId("hello-run")
.build());
// invoke:只要最终状态
var runConfig = RunnableConfig.builder().threadId("demo-1").build();
compiled.invoke(Map.of("name", "LangGraph4j"), runConfig)
.ifPresent(s -> {
System.out.println(s.value(DemoState.REPLY).orElse(""));
System.out.println(s.value(DemoState.LOGS).orElse(List.of()));
});
// → Hello, LangGraph4j! ✓
// → [greet, polish]
// stream:逐步看节点
var streamConfig = RunnableConfig.builder()
.threadId("demo-2")
.streamMode(CompiledGraph.StreamMode.VALUES)
.build();
compiled.stream(Map.of("name", "Tom"), streamConfig)
.forEachAsync(out ->
System.out.println(
out.node()
+ " | END=" + out.isEND()
+ " → " + out.state().data()))
.join();
System.out.println(compiled.getGraph(
GraphRepresentation.Type.MERMAID, "Run Demo", true).content());
}
}
18.3 跑的时候状态怎么变
输入:{name: LangGraph4j}。schema 里 reply 默认 "",logs 默认空列表。name 不在 schema 里,按覆盖合入。
| 步骤 | 动作 | 合并后状态(要点) |
|---|---|---|
| 0 | 输入合入 | name=LangGraph4j,reply="",logs=[] |
| 1 | 跑 greet,返回 reply + logs=[greet] | reply=Hello, LangGraph4j!,logs=[greet] |
| 2 | 跑 polish,改写 reply,返回 logs=[polish] | reply=Hello, LangGraph4j! ✓,logs=[greet, polish](追加) |
| 3 | 边到 END | invoke 返回这一份最终状态 |
执行循环:
初始 state = Channel 默认值 + 输入
循环,直到走到 END,或步数超过 recursionLimit:
取下一节点 → 执行 → partial Map → 按 Channel 合并 → 按边求下一跳
invoke → 最终 State
stream → 一路上的 NodeOutput
直线两三个节点碰不到 recursionLimit。图上有环又退不出去时,会打满上限后停——先保证有退出边,再考虑调大上限。
18.4 invoke 与 stream

上图同一张图、两种用法:左边 invoke 只要最终状态;右边 stream 按帧吐出 NodeOutput。
| 方法 | 返回 | 适用 |
|---|---|---|
invoke(inputs) / invoke(inputs, config) | Optional<State> | 同步接口、断言最终字段 |
invokeFinal(input, config) | Optional<NodeOutput<State>> | 还要最终那一帧的节点名 |
stream(inputs) / stream(inputs, config) | AsyncGenerator<NodeOutput<State>> | 看轨迹、按节点推送 |
底层都是按边推进;invoke 相当于把 stream 跑完,只取最后一帧的 state()。
输入除了 Map,还可以包 GraphInput:
import org.bsc.langgraph4j.GraphInput;
compiled.invoke(GraphInput.args(Map.of("name", "Tom")), runConfig);
compiled.invoke(GraphInput.noArgs(), runConfig); // 无业务输入,用 Channel 默认值起步
日常直接传 Map 即可,内部会变成 GraphInput.args(...)。
stream 时每一帧是 NodeOutput:
| 方法 | 含义 |
|---|---|
node() | 当前节点 id;结束帧为 __END__ |
state() | 合并到目前为止的完整状态,不是节点返回的那一小段 Map |
isSTART() / isEND() | 是否 __START__ / __END__ 帧 |
所以 polish 帧上的 logs 已经是 [greet, polish]。forEachAsync 里处理完本帧即可;要等全部跑完,对返回的 CompletableFuture 调 join()。
StreamMode.VALUES(默认)逐步给出合并后的业务状态。枚举里还有 SNAPSHOTS;看节点进度用 VALUES。
stream 输出大致像:
greet | END=false → {name=Tom, reply=Hello, Tom!, logs=[greet]}
polish | END=false → {name=Tom, reply=Hello, Tom! ✓, logs=[greet, polish]}
__END__ | END=true → {…同上…}
(是否多一帧 __START__,以本机输出为准。)
18.5 RunnableConfig:每次运行可换什么
对照开篇配图的右栏:
RunnableConfig runConfig = RunnableConfig.builder()
.threadId("user-42-req-7")
.streamMode(CompiledGraph.StreamMode.VALUES)
.addMetadata("userId", "42") // 可选,便于日志关联
.build();
| 项 | 含义 |
|---|---|
threadId | 这一次运行的标识。多路请求各自换一个 |
streamMode | VALUES:逐步业务状态 |
addMetadata | 请求级标签,可选 |
和 CompileConfig.graphId 不是一回事:graphId 管「哪张图」,threadId 管「这一次运行」。
同一份 CompiledGraph 连续跑两次:
compiled.invoke(Map.of("name", "A"), RunnableConfig.builder().threadId("t-a").build());
compiled.invoke(Map.of("name", "B"), RunnableConfig.builder().threadId("t-b").build());
| CompileConfig | RunnableConfig | |
|---|---|---|
| 何时定 | compile(...) 时 | 每次 invoke / stream |
| 本章用到的 | recursionLimit、graphId | threadId、streamMode、metadata |
| 生命周期 | 跟着 CompiledGraph 固定 | 每次运行新建一份即可 |
18.6 导出拓扑
// 第三个参数:是否打印条件边(有分支时建议 true)
System.out.println(compiled.getGraph(
GraphRepresentation.Type.MERMAID, "Run Demo", true).content());
StateGraph 在 compile 前也能导出;compile 之后用 CompiledGraph.getGraph 看的是实际要跑的那份。条件边对不上、漏接 END,对着图容易发现。
18.7 常见问题
| 现象 | 处理 |
|---|---|
invoke 得到空 Optional | 看是否中途抛错;直线图正常跑完应有最终状态 |
stream 里 state() 不像节点返回值 | 那是合并后的全量状态;partial 已按 Channel 合进去 |
| 两次请求结果互相干扰 | 每次运行设不同的 threadId |
| Mermaid 里看不到条件边 | getGraph(type, title, true) |
| 改了节点或边,行为还是旧的 | 重新 compile,换掉旧的 CompiledGraph |
| 有环时跑到一半停 | 是否打满 recursionLimit;先修退出边,再调上限 |
每个请求都 new StateGraph + compile | 启动时 compile 一次,请求里只 invoke / stream |
请求级差异(谁在跑、要不要逐步输出、日志标签)写在 RunnableConfig 上,不要为此重建整张图。
488



被折叠的 条评论
为什么被折叠?



