Graph 执行引擎源码:Eino 的节点调度是怎么工作的(第56篇-E42)

发布时间:2026/7/19 9:24:23

Graph 执行引擎源码:Eino 的节点调度是怎么工作的(第56篇-E42)
系列「企业级 AI Agent 实现拆解」E42 篇Part 9 源码深度篇第七章。E40 讲了 ReAct Graph 的拓扑E41 讲了 HITL 中断恢复。这篇拆最底层Graph 编译完之后runner是如何驱动节点一轮一轮执行的。读完这篇你会知道runner是什么graph.Compile()产出的是什么数据结构Graph 的两种执行模式Pregel允许循环vs DAG禁止循环主执行循环的三步提交任务 → 等待完成 → 计算下一批通道系统节点怎么知道自己可以跑了分支节点怎么路由Interrupt 检测是如何嵌入主循环的runner编译产物不是代码生成调用graph.Compile(ctx, opts...)之后Eino 返回的不是生成的代码而是一个runner结构体typerunnerstruct{chanSubscribeTomap[string]*chanCall// 节点名 → 执行描述符successorsmap[string][]string// 节点名 → 下游节点名列表dataPredecessorsmap[string][]stringcontrolPredecessorsmap[string][]stringchanBuilderfunc(isStreambool)map[string]channel// 通道工厂eagerbool// 是否提前提交DAG 模式dagbool// trueDAG, false允许循环(Pregel)checkPointer checkPointerWrapper interruptBeforeNodes[]stringinterruptAfterNodes[]stringoptions graphOptions// maxRunSteps 等runCtxfunc(ctx)ctx// State 注入器}每个chanCall是对某个节点的完整描述typechanCallstruct{action*composableRunnable// 实际执行逻辑ChatModel/ToolsNode/Lambda...writeTo[]string// 输出写进哪些通道writeToBranches[]string// 分支节点需要先跑分支条件函数controls[]string// 控制依赖影响谁的 就绪 判断preProcessor*composableRunnable// modelPreHandle 之类postProcessor*composableRunnable}runner是编译完的图的执行描述只要有runner执行可以随时启动也可以从 checkpoint 恢复。两种执行模式// graph.go 注释// runTypePregel: 支持循环节点激活条件 AnyPredecessor任一前置完成就激活// runTypeDAG: 有向无环节点激活条件 AllPredecessor全部前置完成才激活模式允许循环触发模式用途dag字段Pregel✅AnyPredecessorReAct AgentfalseDAG❌AllPredecessorWorkflow并行分支汇聚trueReAct 必须用 PregelChatModel 有两个前置START 和 ToolsNodeAnyPredecessor让它每次 Tools 跑完就能立刻进入下一轮不需要再等 START。DAG 模式下maxRunSteps不起作用r.dagtrue时主循环不检查步数上限因为无环图一定会终止。主执行循环run(ctx, isStream, input, opts)是入口函数核心是三件事1. 初始化 or 从 checkpoint 恢复 2. 拿到第一批 nextTasks 3. 进入主循环 submit(nextTasks) ↓ completedTasks, canceled tm.wait() ↓ 检测 interrupt ↓ nextTasks calculateNextTasks(completedTasks) ↓ 检测 interruptBeforeNodes ↓ 回到步骤 1loop// 主循环forstep:0;;step{// 超步检查只对 Pregel 有效DAG 跳过if!r.dagstepmaxSteps{returnnil,ErrExceedMaxSteps}// Step 1: 提交当前轮的任务tm.submit(nextTasks)// Step 2: 等待任意一批任务完成completedTasks,canceled,canceledTasks:tm.wait()// Step 3: 检测 interrupt组件内 StatefulInterrupt / 子图中断tempInfo:newInterruptTempInfo()tempInfo.collectCanceledInfo(canceled,canceledTasks,completedTasks)r.resolveInterruptCompletedTasks(tempInfo,completedTasks)iflen(tempInfo.subGraphInterrupts)len(tempInfo.interruptRerunNodes)0{// 有中断 → waitAll 等其他并行任务完成 → 保存 checkpoint → 返回 interruptErrorreturnnil,r.handleInterruptWithSubGraphAndRerunNodes(...)}// Step 4: 把完成的任务输出写进通道判断下一批就绪节点nextTasks,result,isEnd,errr.calculateNextTasks(ctx,completedTasks,...)ifisEnd{returnresult,nil}// Step 5: 检测 interruptBeforeNodes配置级下一批任务执行前ifkeys:getHitKey(nextTasks,r.interruptBeforeNodes);len(keys)0{// waitAll 等其他任务 → 保存 checkpoint → 返回 interruptErrorreturnnil,r.handleInterrupt(...)}}通道系统节点怎么知道自己可以跑了每个节点都有一个通道可以理解为收件箱。节点的所有前置节点输出数据时都往这个收件箱里投一条消息。channelManager维护所有通道的状态// 每轮循环结束时nodeMap,err:cm.updateAndGet(ctx,writeChannelValues,controls)// writeChannelValues: key通道名, value数据// controls: 控制依赖更新影响 AllPredecessor 的就绪判断内部流程resolveCompletedTasks(completedTasks) → 对每个完成的任务 计算 branch哪个下游节点被选中哪些被跳过 把输出 copy 到 writeTo 的每个通道里 把控制信号写入 controls cm.updateValues(values) // 实际写入通道 cm.updateDependencies(deps)// 更新控制依赖计数 cm.getFromReadyChannels() // 返回已经就绪的节点 map就绪条件AnyPredecessorPregel只要有一个前置通道收到数据就返回这个节点AllPredecessorDAG所有前置通道都有数据才返回这个节点当 branch 节点选择路径 A 而跳过 B 时reportSkip会把 B 的通道标记为已跳过避免 B 永远等不到数据。分支路由calculateBranch是在resolveCompletedTasks里被调用的func(r*runner)calculateBranch(ctx,curNodeKey,call,input,isStream,cm)([]string,error){// 调用分支条件函数用户提供的 func(ctx, output) string// 返回被选中的下游节点列表// 同时调用 cm.reportBranch 标记被跳过的节点}ReAct 的 ChatModel 分支条件函数firstChunkStreamToolCallChecker检测 LLM 输出里有没有ToolCalls选中nodeKeyTools或compose.END另一侧自动被reportSkip标记不会饿死等数据Task Managergoroutine 池typetaskManagerstruct{runWrapper runnableCallWrapper// invoke 或 transform流式done*internal.UnboundedChan[*task]// 完成队列无界runningTasksmap[string]*task cancelChchan*time.Duration// 外部取消信号}func(t*taskManager)execute(currentTask*task){deferfunc(){// panic 保护panic 变成 err 存进 task.errifpanicInfo:recover();panicInfo!nil{currentTask.errsafe.NewPanicErr(panicInfo,debug.Stack())}t.done.Send(currentTask)// 完成后入队}()// 初始化节点级 CallbackctxinitNodeCallbacks(currentTask.ctx,currentTask.nodeKey,...)currentTask.output,currentTask.errt.runWrapper(ctx,call.action,input,opts...)}submit(tasks)→ 每个 task 开一个 goroutine 执行wait()→ 从done通道里读若干条完成的 task非阻塞等待一批waitAll()→ 阻塞直到所有运行中的 task 都完成UnboundedChan保证不论多少 task 并发完成done.Send永远不会阻塞。Checkpoint中断时保存了什么handleInterrupt把这些东西存进 checkpointcp:checkpoint{Channels:cm.channels,// 所有通道的当前值Inputs:nextTasks[].input,// 待执行节点的输入恢复时直接调度SkipPreHandler:map[string]bool{...},// 哪些节点恢复时跳过 preProcessorState:deepCopyState(state),// Graph State 的深拷贝JSON 序列化InterruptID2Addr:...,// 中断 ID → 地址 映射InterruptID2State:...,// 中断 ID → 组件 state 映射SubGraphs:map[string]*checkpoint{},// 子图 checkpoint}deepCopyState用 JSON 序列化做深拷贝保证 checkpoint 里的 state 和运行时完全独立。恢复时// 从 ctx 或 store 加载 checkpointcp:getCheckPointFromStore(ctx,checkPointID)// 恢复通道状态cm.loadChannels(cp.Channels)// 恢复 Statectxcontext.WithValue(ctx,stateKey{},internalState{state:cp.State})// 直接生成待执行任务不重新从 START 推导nextTasksrestoreTasks(ctx,cp.Inputs,cp.SkipPreHandler,cp.RerunNodes,...)已完成的节点不会重跑执行从中断点继续。全景一次 ReAct 调用的路径agent.Generate(ctx, messages) ↓ runner.run(ctx, isStreamfalse, inputmessages, opts) ↓ initChannelManager() // 建所有通道 initTaskManager() // 准备 goroutine 调度器 calculateNextTasks([START_task]) → 写入 ChatModel 通道 → nodeMap {ChatModel: messages} → nextTasks [{ChatModel}] --- 循环 step0 --- submit([ChatModel_task]) → go execute(ChatModel_task) // 调 LLM wait() → completedTasks [ChatModel_task{output: msg_with_tool_calls}] resolveInterruptCompletedTasks() // 无中断 calculateNextTasks([ChatModel_task]) → calculateBranch: 检测 tool_calls → 选 Tools → 写入 Tools 通道 → nodeMap {Tools: msg} → nextTasks [{Tools}] --- 循环 step1 --- submit([Tools_task]) → go execute(Tools_task) // 并行跑所有工具 wait() → completedTasks [Tools_task{output: tool_results}] calculateNextTasks([Tools_task]) → 检查 ReturnDirectlyToolCallID → 选 ChatModel → nextTasks [{ChatModel}] --- 循环 step2 --- submit([ChatModel_task]) → go execute(ChatModel_task) // 最终回答 wait() → completedTasks [ChatModel_task{output: final_msg}] calculateNextTasks([ChatModel_task]) → calculateBranch: 无 tool_calls → END → nodeMap {END: final_msg} → isEnd true → return result ✅小结Eino 的 Graph 执行引擎本质是一个通道驱动的超步循环概念实现编译产物runnerstructchanSubscribeTo chanBuilder节点就绪判断channelManager.getFromReadyChannels()AnyPredecessor/AllPredecessor任务调度taskManagergoroutine/panic 保护/UnboundedChan分支路由calculateBranch条件函数 reportSkip中断嵌入每轮 wait 之后检测InterruptSignal命中则handleInterrupt恢复checkpoint 恢复通道 StaterestoreTasks直接调度待执行节点循环、通道、goroutine、三步——把 ReAct 的思考→工具→思考变成了可测量、可中断、可恢复的执行流程。代码来源eino/compose/graph_run.go · eino/compose/graph_manager.go

相关新闻

2026视频去水印教程:手机电脑免费方法与软件推荐

2026视频去水印教程:手机电脑免费方法与软件推荐

2026/7/19 9:24:23

2026年日常使用短视频、素材剪辑学习的场景愈发普遍,很多人都会遇到视频自带平台水印、LOGO、文字标识的问题,影响个人收藏、素材整理和学习剪辑体验。不少用户迫切想要找到简单、免费、画质无损的去水印方式,却分不清各类工具的适配场景&…

西门子 TIA Portal 下载到 软件(仅更改)功能

西门子 TIA Portal 下载到 软件(仅更改)功能

2026/7/19 9:24:23

只下载改动的程序块、变量注释,完全跳过硬件组态下载:不会刷新 PLC 里的 IO 地址分配表;不会覆盖 Profinet 从站的 IO 映射;“不会下载到地址” 不会更新、改动 IO 地址映射关系。优势:PLC 不停机,称重 Pro…

2026聊城黄金回收白银回收铂金回收中检持证鉴定师铂金银饰高价回收门店联系方式推荐

2026聊城黄金回收白银回收铂金回收中检持证鉴定师铂金银饰高价回收门店联系方式推荐

2026/7/19 9:24:23

2026聊城黄金白银铂金回收实测榜单|公安备案中检认证无折旧费门店推荐 聊城本地贵金属回收店铺近年来遍地丛生,但行业套路层出不穷,不少市民变现时遭遇虚高报价、克扣损耗、未经同意熔金压价等问题。为帮助本地居民规避消费陷阱,小…

Claude 4新特性未公开文档泄露(内部测试版实测报告):多模态推理延迟下降64%,但存在3个兼容性陷阱

Claude 4新特性未公开文档泄露(内部测试版实测报告):多模态推理延迟下降64%,但存在3个兼容性陷阱

2026/7/19 14:54:38

更多请点击: https://codechina.net 第一章:Claude 4多模态推理性能跃迁与风险认知 Claude 4标志着Anthropic在多模态基础模型上的重大突破,其核心能力已从纯文本扩展至跨模态联合理解——支持图像、图表、手写公式、结构化表格及嵌入式代码…

力扣-高频 SQL 50 题(基础版)-1484. 按日期分组销售产品

力扣-高频 SQL 50 题(基础版)-1484. 按日期分组销售产品

2026/7/19 14:54:38

一、完整建表 SQL(MySQL 语法):CREATE TABLE Activities (sell_date DATE,product VARCHAR(30) );该表没有主键(具有唯一值的列)。它可能包含重复项。 此表的每一行都包含产品名称和在市场上销售的日期。INSERT INTO Activities(sell_date, p…

如何在5分钟内构建企业级3D点云标注系统:从技术选型到生产部署

如何在5分钟内构建企业级3D点云标注系统:从技术选型到生产部署

2026/7/19 14:54:38

如何在5分钟内构建企业级3D点云标注系统:从技术选型到生产部署 【免费下载链接】point-cloud-annotation-tool 项目地址: https://gitcode.com/gh_mirrors/po/point-cloud-annotation-tool 在自动驾驶、机器人感知和工业检测领域,3D点云标注是算…

聚焦 SAP 中国峰会智能化变革,科莱特双轨模式破解转型壁垒

聚焦 SAP 中国峰会智能化变革,科莱特双轨模式破解转型壁垒

2026/7/19 14:54:38

2026年6月3日,SAP中国峰会在北京举办。本届大会以“自主运营企业”为核心主题,释放出明确的产业信号:AI不再是企业的外挂工具,而是重塑运营范式的核心力量。SAP大中华区总裁原欣在峰会上一针见血地指出,AI能为企业创造…

XmlSchemaClassGenerator核心功能解析:为什么它是XML Schema转C的首选工具

XmlSchemaClassGenerator核心功能解析:为什么它是XML Schema转C的首选工具

2026/7/19 14:54:38

XmlSchemaClassGenerator核心功能解析:为什么它是XML Schema转C#的首选工具 【免费下载链接】XmlSchemaClassGenerator Generate C# classes from XML Schema files 项目地址: https://gitcode.com/gh_mirrors/xm/XmlSchemaClassGenerator XmlSchemaClassGen…

Claude提示词失效预警!这6类语法陷阱正在 silently 损耗你的API配额——附自动检测CLI工具(限时开源)

Claude提示词失效预警!这6类语法陷阱正在 silently 损耗你的API配额——附自动检测CLI工具(限时开源)

2026/7/19 14:44:38

更多请点击: https://intelliparadigm.com 第一章:Claude提示词失效预警与配额损耗本质解析 当Claude模型突然返回“Unable to process request”或空响应,且账户配额异常减少时,往往并非网络故障,而是提示词结构触发…

微服务进阶:服务网格与Istio

微服务进阶:服务网格与Istio

2026/7/19 0:03:49

541|微服务进阶:服务网格与Istio 上篇文章我们聊了微服务的基本概念和拆分方法。 但微服务多了,问题也多了: 服务之间怎么通信? 怎么监控每个服务的调用链路? 熔断、限流、重试怎么做? 安全认证怎么统一? 以前这些都靠SDK库(比如Hystrix、Feign),每个服务都要集成…

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

2026/7/19 0:03:49

一、零售门店全域协同业务背景与行业痛点 1.1 门店超级终端设备矩阵(连锁便利店/商超标准配置) 自助收银Kiosk一体机:顾客结算、自助核销优惠券、商品素材预览;运营折叠平板:店长后台商品上新、图片录入、活动配置、…

噗叽短视频界面分析

噗叽短视频界面分析

2026/7/19 0:03:49

1 和小红书类似,可以采用类似判断方法------------其实他比小红书好判断,因为他没有图片,控件位置几乎是固定的,都不用判断------------2 因为他没有点赞按钮------------而且几乎所有控件位置都是完全一样的,所以我就…

微服务进阶:服务网格与Istio

微服务进阶:服务网格与Istio

2026/7/19 0:03:49

541|微服务进阶:服务网格与Istio 上篇文章我们聊了微服务的基本概念和拆分方法。 但微服务多了,问题也多了: 服务之间怎么通信? 怎么监控每个服务的调用链路? 熔断、限流、重试怎么做? 安全认证怎么统一? 以前这些都靠SDK库(比如Hystrix、Feign),每个服务都要集成…

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

2026/7/19 0:03:49

一、零售门店全域协同业务背景与行业痛点 1.1 门店超级终端设备矩阵(连锁便利店/商超标准配置) 自助收银Kiosk一体机:顾客结算、自助核销优惠券、商品素材预览;运营折叠平板:店长后台商品上新、图片录入、活动配置、…

噗叽短视频界面分析

噗叽短视频界面分析

2026/7/19 0:03:49

1 和小红书类似,可以采用类似判断方法------------其实他比小红书好判断,因为他没有图片,控件位置几乎是固定的,都不用判断------------2 因为他没有点赞按钮------------而且几乎所有控件位置都是完全一样的,所以我就…