版本: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上,不要为此重建整张图。