读 graph-core 源码时,一个反直觉的地方是:调用入口 app.invoke(inputs) 看起来是同步执行,但底层真实执行模型是流式的。invoke 本质是 stream(...).last().map(NodeOutput::state).block()——把同步调用包装在流式执行之上。

这意味着整个图的执行是用 Flux 把每个节点输出串成一条响应式流,而不是一次性递归算完再返回;invoke 只是订阅这条流并取最后状态。理解这条链路,是理解 graph-core 执行机制的关键。这里把从整体执行流程到 Flux 订阅机制的内容整理下来。

整体执行流程

主线流程:

graph LR
    A[定义图] --> B[编译图]
    B --> C[执行图]
    C --> D[节点返回局部状态]
    D --> E[框架合并到全局状态]
    E --> F[根据边选择下一节点]
    F --> G[最终返回 OverAllState]

精确拆开是八步:

1. 定义 StateGraph
   - 注册 node 节点
   - 注册 edge / conditional edge
   - 注册 KeyStrategy 合并策略

2. compile()
   - 校验图合法性
   - 固化节点、边、配置
   - 生成 CompiledGraph

3. invoke(initialState)
   - invoke 本质是 stream(...).last().map(NodeOutput::state).block()
   - 所以底层真实执行模型是流式执行

4. GraphRunner.run()
   - 创建 GraphRunnerContext
   - context 持有 compiledGraph、config、overallState、currentNodeId、nextNodeId

5. MainGraphExecutor.execute()
   - 处理 START / END / interrupt / resume / stop
   - 普通节点交给 NodeExecutor

6. NodeExecutor.executeNode()
   - 根据 nextNodeId 找到当前 node action
   - 执行 action.apply(overallState, config)
   - action 返回局部状态 Map<String, Object>

7. 状态合并与路由
   - mergeIntoCurrentState(updateState)
   - OverAllState.updateState(...) 按 KeyStrategy 合并
   - nextNodeId(...) 根据固定边或条件边选下一节点
   - 构造 NodeOutput
   - 回到 MainGraphExecutor 继续推进

8. 最终结果
   - 到 END 后流结束
   - invoke 取最后一个 NodeOutput.state
   - 返回 Optional<OverAllState>

各组件职责分工:

  1. StateGraph:负责定义图
  2. CompiledGraph:承载编译后的图和执行入口门面
  3. GraphRunner / Executor:负责推进执行
  4. OverAllState:保存全局状态
  5. KeyStrategy:决定局部状态如何合并

CompiledGraph 不是执行器本体

一个容易混淆的点:CompiledGraph 看起来像执行引擎,因为调用入口是 app.invoke(inputs),但它更像编译后的图模型加上执行入口门面。真正推进执行的是:

GraphRunner
-> MainGraphExecutor
-> NodeExecutor

CompiledGraph.invoke(...) 只是把同步调用包装在流式执行之上:

Optional.ofNullable(stream(inputs, config).last().map(NodeOutput::state).block());

拆开看:

invoke = stream + last + state + block

NodeExecutor 怎么流转回 MainGraphExecutor

执行链的核心在 NodeExecutor 执行完当前节点后,把控制权交回 MainGraphExecutor 的这段代码:

return Flux.just(GraphResponse.of(output))
    .concatWith(Flux.defer(() -> mainGraphExecutor.execute(context, resultValue)));

这段代码表达四件事:

  1. 先发出当前节点的 GraphResponse
  2. 当前节点输出发完后
  3. 再调用 mainGraphExecutor.execute(context, resultValue)
  4. MainGraphExecutor 根据 context.nextNodeId 决定下一步

NodeExecutor 不自己用 while 直接跑完整张图,它只负责当前节点:

graph LR
    A[执行当前 node action] --> B[合并局部状态]
    B --> C[计算 nextNodeId]
    C --> D[构造当前 NodeOutput]
    D --> E[把控制权交回 MainGraphExecutor]

MainGraphExecutor 继续判断:是否 START、是否 END、是否中断、是否恢复、是否继续执行普通节点。两者共同构成执行循环。要理解这个循环怎么在响应式流里实现,需要先看几个 Reactor 概念。

concatWith:顺序拼接

简化例子:

Flux.just("A")
    .concatWith(Flux.just("B"));

concatWith 是顺序拼接:先订阅并消费左边的 Flux,左边发完并 onComplete 后,再订阅并消费右边的 Flux。输出顺序一定是 A 然后 B。

放回 graph-core:

Flux.just(GraphResponse.of(output))
    .concatWith(...下一轮执行...)

意思是先发当前节点 output,当前 output 的 Flux 完成后,再进入下一轮执行。

Flux.defer:延迟创建

Flux.defer 的作用是延迟创建 Flux:

Flux.defer(() -> Flux.just("B"))

现在先不创建 B 这个 Flux,等真正被订阅时,再执行 Supplier,创建 Flux.just("B")

graph-core 这里需要 defer 的原因,是下一轮执行依赖当前节点执行后的 context

currentNodeId 已更新
nextNodeId 已更新
overallState 已合并

不用 defer,下一轮 mainGraphExecutor.execute(...) 可能会在构造流时过早执行。用了 defer 后,语义变成:等当前节点 GraphResponse 发完,concatWith 开始订阅右侧 Flux,此时 defer 才调用 mainGraphExecutor.execute(...),下一轮使用的是最新 context。

两条核心记忆:

  1. concatWith 保证顺序:左边 onComplete 后才订阅右边
  2. defer 保证下一轮晚点创建:被订阅时才执行 Supplier

订阅触发执行

响应式编程里一个核心概念:Flux 本身只是数据流水线的描述,数据并不会自动流动。单独写这段代码什么都不会输出:

Flux.just("A")
    .concatWith(Flux.defer(() -> Flux.just("B")));

它只是描述了一条流水线——以后如果有人订阅我:先发 A,A 完成后再创建并发 B。真正触发执行的是终端操作:

.subscribe(System.out::println)
.block()
.collectList().block()

响应式流有三个基础信号:

  1. onNext(value):发出一个数据
  2. onComplete():正常结束
  3. onError(error):异常结束

concatWith 依赖的就是 onComplete():左边 Flux 发出 onComplete 后,右边 Flux 才会被订阅。

用 A/B 例子串起来

Flux.just("A")
    .concatWith(Flux.defer(() -> Flux.just("B")))
    .subscribe(System.out::println);

执行过程拆成 14 步:

1.  subscribe 订阅整个 concat Flux
2.  concat Flux 先订阅左边 Flux.just("A")
3.  左边发出 onNext("A")
4.  println 收到 A
5.  左边发出 onComplete
6.  concat Flux 收到左边完成信号
7.  concat Flux 订阅右边 Flux.defer(...)
8.  defer 此时才调用 Supplier
9.  Supplier 返回 Flux.just("B")
10. concat Flux 订阅 B Flux
11. B Flux 发出 onNext("B")
12. println 收到 B
13. B Flux 发出 onComplete
14. 整条 concat Flux 完成

「等 A 被订阅完成」更准确的说法是:等左边 Flux 被订阅,并且发完 A 后发出 onComplete,右边 Flux 才会被订阅。

block 如何触发 graph-core 执行

回到 invoke

Optional.ofNullable(
    stream(inputs, config)
        .last()
        .map(NodeOutput::state)
        .block()
);

这里的 block() 会订阅上游整个 Flux<NodeOutput>。上游链路:

graph TB
    S1[stream inputs, config] --> S2[streamFromInitialNode]
    S2 --> S3[runner.run overAllState]
    S3 --> S4[MainGraphExecutor.execute]
    S4 --> S5[NodeExecutor.execute]
    S5 --> S6[Flux.just GraphResponse.of output]
    S6 --> S7[concatWith defer 下一轮]
    S7 --> S8[flattenGraphResponsesPreservingOrder]
    S8 --> S9[Flux NodeOutput]
    S9 --> S10[last]
    S10 --> S11[map NodeOutput::state]
    S11 --> S12[block 返回结果]

一个类型转换细节:NodeExecutor 发出的是 Flux<GraphResponse<NodeOutput>>,而 CompiledGraph.streamFromInitialNode 会通过 flattenGraphResponsesPreservingOrder(...) 把它压平成 Flux<NodeOutput>。所以完整链路是:

NodeExecutor 发出 GraphResponse.of(output)
-> GraphRunner.run 返回 Flux<GraphResponse<NodeOutput>>
-> flatten 解出 NodeOutput
-> stream(...) 得到 Flux<NodeOutput>
-> last() 等最终节点
-> map(NodeOutput::state) 取最终 OverAllState
-> block() 返回结果

last 和 map 的关键细节

stream(inputs, config)
    .last()
    .map(NodeOutput::state)
    .block()

map(NodeOutput::state) 不会对每个节点都执行,因为前面有 last()。真实行为:

  1. 每个节点的 NodeOutput 都会往下游发
  2. last() 持续接收,但只保留最后一个
  3. 等整个 Flux complete 后
  4. last() 才把最后一个 NodeOutput 发给 map
  5. map 只对最后一个 NodeOutput 执行
  6. block 拿到最终 state

图是 START -> A -> B -> END 时,执行过程可能发出:

NodeOutput(START)
NodeOutput(A)
NodeOutput(B)
NodeOutput(END)

last() 的行为:

sequenceDiagram
    participant Flux as 上游 Flux
    participant Last as last()
    participant Map as map(NodeOutput::state)
    
    Flux->>Last: onNext(NodeOutput(START))
    Note over Last: 暂存为 last
    Flux->>Last: onNext(NodeOutput(A))
    Note over Last: 覆盖 last
    Flux->>Last: onNext(NodeOutput(B))
    Note over Last: 覆盖 last
    Flux->>Last: onNext(NodeOutput(END))
    Note over Last: 覆盖 last
    Flux->>Last: onComplete()
    Last->>Map: 发出 NodeOutput(END)
    Note over Map: 只对最后一个执行
    Map->>Map: 取出 OverAllState

然后 map(NodeOutput::state) 只对最后的 NodeOutput(END) 执行,取出里面的 OverAllState

心智模型

把 graph-core 的执行和 Reactor 放在一起看:

  1. block():触发订阅整条执行链
  2. MainGraphExecutor:负责判断当前阶段,是 START、END、中断,还是普通节点
  3. NodeExecutor:负责执行当前节点 action,合并状态,计算 nextNodeId
  4. Flux.just(output):把当前节点输出发出去
  5. concatWith(defer(…)):把下一轮执行按顺序挂在当前输出之后
  6. last():忽略中间节点,只保留最后一个 NodeOutput
  7. map(NodeOutput::state):取出最终全局状态 OverAllState

核心设计:graph-core 用 Flux 把每个节点输出串成一条响应式执行流,而不是一次性递归算完再返回;invoke 只是订阅这条流并取最后状态。这种设计让流式输出、背压处理、节点级中断恢复都建立在同一套响应式机制上——执行链本身就是流,中断和恢复只是流的暂停和继续。