版本LangGraph4j 1.8.25目标蓝图compile成CompiledGraph之后怎么用invoke/stream跑起来两套 Config 各管什么跑的时候状态怎么一步步变。本章不接模型、不落盘。先分清两套配置CompileConfig在编译时定一次RunnableConfig每次运行可换。中间的CompiledGraph编译一次反复invoke/stream。18.1 compile从蓝图到可执行实例StateGraph是可变蓝图。对外跑的是CompiledGraphS compiled stateGraph.compile(); // 或带配置 CompiledGraphS 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必须 0graphId这张图的标识可选实践三点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 MapString, Channel? SCHEMA Map.of( REPLY, Channels.base(() - ), LOGS, Channels.appenderWithDuplicate(ArrayList::new) ); public DemoState(MapString, Object initData) { super(initData); } } public static void main(String[] args) throws Exception { NodeActionDemoState greet state - { String name state.Stringvalue(name).orElse(world); return Map.of( DemoState.REPLY, Hello, name !, DemoState.LOGS, List.of(greet) ); }; NodeActionDemoState polish state - Map.of( DemoState.REPLY, state.Stringvalue(DemoState.REPLY).orElse() ✓, DemoState.LOGS, List.of(polish) ); CompiledGraphDemoState 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输入合入nameLangGraph4jreplylogs[]1跑greet返回replylogs[greet]replyHello, LangGraph4j!logs[greet]2跑polish改写reply返回logs[polish]replyHello, LangGraph4j! ✓logs[greet, polish]追加3边到ENDinvoke返回这一份最终状态执行循环初始 state Channel 默认值 输入 循环直到走到 END或步数超过 recursionLimit 取下一节点 → 执行 → partial Map → 按 Channel 合并 → 按边求下一跳 invoke → 最终 State stream → 一路上的 NodeOutput直线两三个节点碰不到recursionLimit。图上有环又退不出去时会打满上限后停——先保证有退出边再考虑调大上限。18.4 invoke 与 stream上图同一张图、两种用法左边invoke只要最终状态右边stream按帧吐出NodeOutput。方法返回适用invoke(inputs)/invoke(inputs, config)OptionalState同步接口、断言最终字段invokeFinal(input, config)OptionalNodeOutputState还要最终那一帧的节点名stream(inputs)/stream(inputs, config)AsyncGeneratorNodeOutputState看轨迹、按节点推送底层都是按边推进invoke相当于把stream跑完只取最后一帧的state()。输入除了Map还可以包GraphInputimport 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()合并到目前为止的完整状态不是节点返回的那一小段 MapisSTART()/isEND()是否__START__/__END__帧所以polish帧上的logs已经是[greet, polish]。forEachAsync里处理完本帧即可要等全部跑完对返回的CompletableFuture调join()。StreamMode.VALUES默认逐步给出合并后的业务状态。枚举里还有SNAPSHOTS看节点进度用VALUES。stream输出大致像greet | ENDfalse → {nameTom, replyHello, Tom!, logs[greet]} polish | ENDfalse → {nameTom, replyHello, Tom! ✓, logs[greet, polish]} __END__ | ENDtrue → {…同上…}是否多一帧__START__以本机输出为准。18.5 RunnableConfig每次运行可换什么对照开篇配图的右栏RunnableConfig runConfig RunnableConfig.builder() .threadId(user-42-req-7) .streamMode(CompiledGraph.StreamMode.VALUES) .addMetadata(userId, 42) // 可选便于日志关联 .build();项含义threadId这一次运行的标识。多路请求各自换一个streamModeVALUES逐步业务状态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());CompileConfigRunnableConfig何时定compile(...)时每次invoke/stream本章用到的recursionLimit、graphIdthreadId、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 合进去两次请求结果互相干扰每次运行设不同的threadIdMermaid 里看不到条件边getGraph(type, title, true)改了节点或边行为还是旧的重新compile换掉旧的CompiledGraph有环时跑到一半停是否打满recursionLimit先修退出边再调上限每个请求都new StateGraphcompile启动时 compile 一次请求里只invoke/stream请求级差异谁在跑、要不要逐步输出、日志标签写在RunnableConfig上不要为此重建整张图。