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

2026/7/19 9:24:06阅读更多 →
西门子 TIA Portal 下载到 软件(仅更改)功能

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

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

2026/7/19 9:24:06阅读更多 →
多维聚合后的数据操纵:从立方体思维到工程化实践

多维聚合后的数据操纵:从立方体思维到工程化实践

1. 项目概述:这不是简单的“分组求和”,而是多维数据空间的精准导航 你有没有遇到过这样的场景:销售报表里要同时按“地区产品线季度”三个维度看销售额,还要在每个交叉格子里显示同比变化率、环比变化率、占区域总销售额比重、以…

2026/7/19 9:22:06阅读更多 →
Prometheus+Grafana 配置Elasticsearch ,Kafka的启动【Windows环境入门】

Prometheus+Grafana 配置Elasticsearch ,Kafka的启动【Windows环境入门】

Elasticsearch 官网下载:elasticsearch-9.4.3-windows-x86_64.zip 解压 3.修改config/elasticsearch.yml # 本地调试开发单机模式 discovery.type: single-node # 集群模式需要设置 #cluster.initial_master_nodes ["node_1"] # 这几个都改成false访…

2026/7/19 14:45:04阅读更多 →
如何快速构建企业级一站式数据开发平台:DataSphere Studio终极指南

如何快速构建企业级一站式数据开发平台:DataSphere Studio终极指南

如何快速构建企业级一站式数据开发平台:DataSphere Studio终极指南 【免费下载链接】DataSphereStudio DataSphereStudio is a one stop data application development& management portal, covering scenarios including data exchange, desensitization/clean…

2026/7/19 14:45:04阅读更多 →
GitHub Copilot、LangChain 与 Claude-code:三大AI编程工具深度解析

GitHub Copilot、LangChain 与 Claude-code:三大AI编程工具深度解析

摘要在AI技术席卷软件开发领域的今天,GitHub Copilot、LangChain和Claude-code是开发者们最常提及的三个名字。它们看似都与“AI编程”相关,但定位、工作方式和适用场景却截然不同。本文将深入剖析这三者的核心差异:Copilot是集成在IDE中的智…

2026/7/19 14:45:04阅读更多 →
无人机集群厘米级定位技术挑战与UWB-IMU融合解决方案

无人机集群厘米级定位技术挑战与UWB-IMU融合解决方案

无人机集群厘米级定位技术挑战与UWB-IMU融合解决方案 【免费下载链接】uwb-localization Accurate 3D Localization for MAV Swarms by UWB and IMU Fusion. ICCA 2018 项目地址: https://gitcode.com/gh_mirrors/uw/uwb-localization 在无人机集群协同作业、室内自主导…

2026/7/19 14:45:04阅读更多 →
MPI-IS Mesh可视化指南:meshviewer工具的10个实用技巧

MPI-IS Mesh可视化指南:meshviewer工具的10个实用技巧

MPI-IS Mesh可视化指南:meshviewer工具的10个实用技巧 【免费下载链接】mesh MPI-IS Mesh Processing Library 项目地址: https://gitcode.com/gh_mirrors/mesh6/mesh MPI-IS Mesh Processing Library是一款功能强大的网格处理工具包,其中的meshv…

2026/7/19 14:45:04阅读更多 →
终极指南:如何快速将GIMP界面变身为Photoshop风格

终极指南:如何快速将GIMP界面变身为Photoshop风格

终极指南:如何快速将GIMP界面变身为Photoshop风格 【免费下载链接】GimpPs Gimp Theme to be more photoshop like 项目地址: https://gitcode.com/gh_mirrors/gi/GimpPs 你是否习惯了Photoshop的操作界面,但又想使用免费开源的GIMP图像编辑软件&…

2026/7/19 14:43:04阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/19 0:01:04阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/19 0:01:04阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/19 0:01:04阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/19 0:01:04阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/19 0:01:04阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/19 0:01:04阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/18 22:49:46阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/18 14:49:24阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/18 18:49:35阅读更多 →