☰
第 18 章 · 跑通 AI 图:CompiledGraph 的 compile、invoke、stream
2026/9/28 21:20:37 网站建设 项目流程

版本: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大致做三件事:

  1. 校验拓扑:有没有从START出去的边、边是否指向不存在的节点、条件映射是否为空等。失败抛GraphStateException。

  2. 收成可执行结构:节点动作和边整理进内部表,运行时按表推进。

  3. 固化CompileConfig:这份配置跟着CompiledGraph走,运行时不再改。

无参compile()使用默认CompileConfig,其中recursionLimit = 25。

CompileConfig compileConfig = CompileConfig.builder() .recursionLimit(25) .graphId("hello-run") // 进程里多张图并存时好区分,可选 .build();
项含义
recursionLimit单次运行最多走多少步,默认 25,必须> 0
graphId这张图的标识,可选

实践三点:

  1. compile 一次,复用CompiledGraph。不要每个请求重新addNode/addEdge再 compile。

  2. compile 之后不要再改原来的StateGraph,还指望已发布的实例跟着变。要改拓扑,改完再 compile 出新实例。

  3. 在 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边到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)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这一次运行的标识。多路请求各自换一个
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 合进去
两次请求结果互相干扰每次运行设不同的threadId
Mermaid 里看不到条件边getGraph(type, title, true)
改了节点或边,行为还是旧的重新compile,换掉旧的CompiledGraph
有环时跑到一半停是否打满recursionLimit;先修退出边,再调上限
每个请求都new StateGraph+compile启动时 compile 一次,请求里只invoke/stream

请求级差异(谁在跑、要不要逐步输出、日志标签)写在RunnableConfig上,不要为此重建整张图。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询