一、先看手工串服务为什么很快失控

假设公司的订单、风控和搜索服务都用 Go 编写。团队要做一个“退款风险助手”:先读订单,再查风险规则, 资料不足时向用户补问,最后边生成边返回建议。最初用几个函数按顺序调用就能跑起来。

功能一多,手工连接就开始出问题:订单函数返回结构体,模型函数需要消息列表;模型在流式输出,下一步却只接收完整值; 用户补充信息时,程序不知道应该回到哪一步;重试又可能重复调用有副作用的工具。团队真正缺的不是另一个大函数, 而是一套能约束每一步输入输出、组织先后关系,并统一处理中断和恢复的执行骨架。

二、先认识 Eino 的六层积木

Eino 的类名不少,但可以从小到大只记六层。每一层都在回答一个具体问题:

  • Component(组件):一个能力单元,例如模型、工具、检索器或提示模板;它先说清接收什么、返回什么。
  • Node(节点):组件被放进一次执行流程后得到的位置,例如“读取订单”节点。
  • Graph(图):规定节点的顺序、分支、汇合和循环,相当于任务路线图。
  • Runnable:图编译后的统一运行接口,同一条路线可以一次性执行,也可以流式接收或输出。
  • Runner:真正推进图执行的调度者,负责当前任务、数据通道和中断;配置 checkpoint store 与稳定 ID 后,才拥有可恢复的存档点。
  • Agent:建立在前五层之上的模型循环;模型决定调用哪个工具,图与 Runner 保证这次循环能被执行和管理。

因此,“Eino 是执行图框架”不是说开发者只能画图,而是说模型、工具和 Agent 最终都落到同一套可组合、可运行、 可恢复的执行规则里。理解这六层以后,后面的 Go 泛型和接口才有位置可放。

三、让一笔退款风险检查完整跑一遍

Eino 有两条合法入口,不能把它们读成依次经过的四层管线。Compose 应用路径NewGraph → Compile → Runnable 启动;ADK agent 路径Runner.Query → flowAgent → ChatModelAgent 启动,ChatModelAgent 内部再使用 ReAct graph。 下面的退款任务选择第一条 Compose 路径,因为订单、风控与审批步骤需要显式业务形状;ADK 放到后文作为另一种入口对照。

  1. 用户输入订单号,消息先进入执行图的起点。
  2. “读取订单”组件返回订单数据,类型边界保证下一节点拿到的是约定结构,而不是随意拼出的文本。
  3. 模型节点判断还需要风险信息,于是进入工具节点调用风控服务。
  4. 若资料齐全,图走向“生成建议”;Runnable 把模型的流式片段持续交给界面。
  5. 若缺少退款原因,Runner 在补问节点中断,并把当前位置和已有数据保存为 checkpoint。
  6. 用户补充原因后,Runner 恢复 channel、state 与 pending input,再由 task calculation 决定下一批节点。
  7. 图到达终点并返回建议;业务校验和外部退款系统再决定是否采用它。

第 5、6 步有前提。 应用必须给 Runner 配置 CheckPointStore,并用稳定 checkpoint ID 启动这次执行; TypedRunner.Resume 在 store 为空时会直接失败。Interrupt 告诉调用方“停在哪里、等什么”, 但不会凭一个 token 自动保存全部 channel 与 state。

Eino README 的第一句定位是:Eino 是一个 Golang 的 LLM application development framework,吸收 LangChain、Google ADK 等框架经验,同时遵循 Golang conventions。它提供四块东西:Components、Agent Development Kit、Composition 和 Examples。 证据在 Eino README overview

更短的源码注释把边界说得更像工程内核:根包提供 agent workflows、tools 和 composable graph utilities; compose 包提供 graph 与 workflow primitives,用来构建 composable、interruptible execution pipelines,并支持 callback。 证据在 doc.gocompose/doc.go

阅读契约。 这篇只抓一条线:Eino 先解决“Go 组件怎样组合、流式运行、中断和恢复”,再在这条线上实现 ADK Runner、 ChatModelAgent 和 AgentTool。读它时不要先找团队协作抽象,也不要只找 ReAct loop; 先看 Runnable[I, O]compose.Graph[I, O]

证据边界。 Eino 源码固定到 e8832e223b93a7f45b1cc3d491239c14f94717c0。 本文只基于公开 README 与源码;“Go 类型化执行图”是对公开抽象 owner 的工程归纳,不是 Eino 官方术语。

先把“查订单退款风险”这类请求走一遍,Eino 的层次会更容易读:

Compose application path used by this refund example
  RefundRequest
    -> compose.NewGraph
    -> OrderSnapshot
    -> RiskContext
    -> DecisionDraft
    -> Graph.Compile() -> Runnable
    -> runner: channels + tasks + checkpoint

ADK agent path, shown later as an alternative entry
  []*schema.Message{user("查订单退款风险")}
    -> adk.Runner.Query
    -> flowAgent
    -> ChatModelAgent
    -> internal ReAct graph

Shared compose primitive inside either path
  graph := compose.NewGraph[[]*schema.Message, *schema.Message]()
  Graph.AddEdge(START, "planner")
  Graph.AddEdge("planner", "tools")
  Graph.Compile() -> Runnable

这条链说明,Eino 的重点不是把“模型调用”包装得更顺手,而是把每个节点的输入输出、流式数据、 中断点和恢复状态都放进同一套 Go graph runtime。后面读组件、Graph、runner 和 ADK 时,都可以沿着这条链定位 owner。

四、组件层先把模型、工具和消息形状封住

先不要让通用消息类型遮住业务数据。Compose 版退款图的代表对象依次是: RefundRequest{order_id, reason} 来自用户, OrderSnapshot{status, amount} 来自订单系统, RiskContext{score, policy} 来自风控, DecisionDraft{action, rationale} 才交给生成节点。 Graph 的泛型边界约束这些转换;后面的 []*schema.Message 是 ADK 模型循环的账本形状,不是订单结构体。

Eino 的组件接口很 Go:先把类型边界收紧,再谈 agent 行为。BaseModel[M] 用 sealed type constraint 限定消息类型只能是 *schema.Message*schema.AgenticMessage,并暴露 GenerateStream 两种交互。旧的 ChatModel.BindTools 被标为 deprecated, 因为它会原地修改实例并在并发时引入 race;新的 ToolCallingChatModel.WithTools 返回不可变 variant。 这就是 Go 风格:并发安全和类型边界先行。 源码在 components/model/interface.go

工具接口也被拆成元数据和执行面。BaseTool.Info 返回 name、description 和参数 JSON schema; 真正执行时才需要 InvokableToolStreamableTool,或能返回 multimodal result 的 enhanced variants。 这个拆分让模型可见的 tool schema 与 runtime 真实副作用保持分层。 源码在 components/tool/interface.go

schema.Message

role 是 system / user / assistant / tool;assistant message 可以带 ToolCalls, tool message 通过 ToolCallID 回填结果。它更像 chat completions 时代的 conversation history。

schema.AgenticMessage

内容是 ContentBlock 列表,块类型包含 reasoning、function tool、server tool、MCP call、 MCP approval request / response 等。它更适合 provider 原生 agentic 事件。

这不是文档层命名差异。schema.Message 明确定义 role、tool call、multimodal part、response meta 和 reasoning content; schema.AgenticMessage 则把 reasoning、assistant generated media、server tool call、MCP tool call 和 approval request 都建模成 content blocks。证据在 Message role and tool callchat message partsMessage structAgenticMessage blocks

五、Runnable 是 compose runtime 的最小执行契约

Eino 的关键抽象不是“graph 有一个 run 方法”,而是 Runnable[I, O] 同时承诺四种数据流: Invoke 是普通输入到普通输出,Stream 是普通输入到流式输出, Collect 是流式输入到普通输出,Transform 是流式输入到流式输出。 源码注释直接说 graph 和 chain 都会编译成 Runnable,并且 Eino 会对四种 data flow patterns 做 downgrade compatibility。 证据在 Runnable interface

数据流 输入 输出 适合什么组件
Invoke 普通值 普通值 一次性分类、格式化、同步工具调用。
Stream 普通值 LLM token streaming 或流式搜索结果。
Collect 普通值 把上游多个 chunk 汇总成最终结构。
Transform 边接收边改写,例如流式脱敏、过滤或转换。

这个设计解决一个实际问题:不同组件天然支持的流形态不同,框架不能要求每个组件都手写四套实现。 所以 runnablePacker 会把用户给的 invoke / stream / collect / transform 包成 composableRunnable, 做输入、输出、option 的类型检查;默认转换函数再用 stream concat 或 single-element stream 在四种形态之间转换。 证据在 runnable packingdefault flow conversion

Graph[I, O] 是把这些 Runnable 组织起来的 typed graph。NewGraph 可以带 local state generator; AddEdge 明确要求上一节点输出类型匹配下一节点输入类型;Compile 把 graph 编译成 Runnable, 于是编译后的 graph 同样支持 Invoke / Stream / Collect / Transform。 源码在 NewGraph and stateAddEdge and Compile

graph 内部同时记录 control edges、data edges、branches、start nodes、end nodes、state type、expected input / output type、 handlers 和 compiled 标记。graphRunType 里有 Pregel 与 DAG 两种运行模式:Pregel 支持环和 AnyPredecessor, DAG 面向有向无环图并配合 AllPredecessor。 源码在 graph fields and run types

六、执行循环把 channel、task、checkpoint 和 interrupt 串起来

Graph 编译完之后,runner 接管的不只是“节点之间怎么连”。它还要管四件事:数据从哪个 channel 来、下一个 task 怎么算、 checkpoint 怎么恢复、interrupt 发生时怎样把状态交还给调用方。这样一个 Go graph 才能既支持流式输出,也支持中断和恢复。 源码在 runner fields

runner 接管的层 它解决什么
channel / predecessors / successors 数据流和控制流不是临时 callback,而是由 graph runtime 调度。
task manager 每一轮该跑哪些节点、哪些已经完成,由 runner 统一计算。
checkpoint pointer 恢复时能找回 channel、state 和待执行节点。
interruptBefore / interruptAfter 人机确认或外部等待可以成为 graph 的正式暂停点。

runner.run 的前半段先确定是 Invoke 还是 Transform,初始化 channel manager 和 task manager, 解析 max steps、node options、checkpoint id 和 subgraph path。然后它要么从 context / store 恢复 checkpoint, 要么从 START 节点计算第一批 tasks。如果第一批 tasks 命中 interrupt-before 节点,会立即构造 interrupt。 源码在 runner.run setup

主循环则是固定三步:submit next tasks,wait completed tasks,calculate next tasks。它会处理 context cancellation、 max steps、subgraph interrupt、rerun nodes、interrupt-before 与 interrupt-after,再在到达 END 时返回 result。 DAG 模式不能设置 max run steps,非 DAG 模式必须有正数 max steps。 源码在 main execution loop

恢复的关键是 checkpoint 里保存的不只是“下一步节点”。restoreCheckPointState 会恢复 channels, 应用 state modifier,把 checkpoint state 挂回 context;handleInterrupt 会复制 state、记录待执行 inputs、 建立 interrupt id 到 address / state 的持久化映射,必要时写 checkpoint store。 源码在 restore checkpoint statehandle interrupt

公开 interrupt API 也对应这条执行链。编译时可以声明 WithInterruptBeforeNodesWithInterruptAfterNodes;普通组件用 Interrupt,需要保存组件内部状态时用 StatefulInterrupt,复合节点例如 ToolsNode 则用 CompositeInterrupt 把多个子中断压成一个可恢复信号。 源码在 interrupt compile optionsInterrupt and StatefulInterruptCompositeInterrupt

恢复边界。 checkpoint 保存可重放的 channels、state 和 pending inputs;interrupt 标记暂停地址,并把当前等待条件交给调用方; 调用方之后只提供 resume data,不需要自己重建 graph 内部状态。反过来,单独一个 interrupt token 也不等于完整恢复状态。

应用提供了什么能恢复到哪里进程重启后的边界
只有 interrupt 信息调用方知道等待条件,但没有可供 Resume 读取的完整执行状态。不能恢复。
进程内 CheckPointStore + 稳定 ID同一进程中可找回 channels、state 与 pending inputs。store 随进程消失时,checkpoint 也消失。
持久化 CheckPointStore + 稳定 ID新 Runner 可按 ID 读取 checkpoint,再恢复 graph 状态。能否跨进程取决于应用实现的 store durability 与共享可见性。

这份 checkpoint 也不包住外部风控或退款写入的事务。恢复后的 task calculation 可能再次选择某个节点; 读操作要接受重复查询,写操作则必须使用幂等键、去重或补偿。Eino 能恢复图状态,不等于它自动承诺每个业务副作用 exactly-once。

七、callback 是 aspect,不是随便插日志

Eino 的 callback API 把观测也当成 runtime 边界。RunInfo 描述触发 callback 的实体, 包括用户可读 name、实现 type 和 component category;注释提醒 handler 应该根据 RunInfo 过滤,而不是假定不同 handlers 有固定执行顺序。 源码在 RunInfo

Handler 统一了 OnStartOnEndOnError、stream input start 和 stream output end。 同一个 handler 的 context 可以沿 timing 传递,但不同 handlers 之间没有顺序保证;stream handler 拿到的是复制出来的 StreamReader,必须 close,否则原 stream 无法释放;同时注释明确禁止 mutate input / output,因为下游节点和 handlers 共享同一指针,会引发并发图里的 data race。 源码在 Handler contract

八、ADK Runner 是 agent 层入口,但仍然走 flowAgent 边界

README 的 Quick Start 很短:创建 ChatModel,再创建 adk.NewChatModelAgent,交给 adk.NewRunner,最后通过 runner.Query 得到 event iterator。加 tools 时,tools 放进 ToolsConfig 里的 compose.ToolsNodeConfig;README 说明 agent 会内部处理 ReAct loop。 证据在 ChatModelAgent quick start

源码上,TypedRunner 的注释说它是执行 Agent 的 primary entry point,负责 start、resume 和 checkpoint; 更重要的是,execution always goes through the flowAgent pipeline,用来处理 multi-agent orchestration、callbacks、 agent naming、run paths 和 cancellation。RunQuery 返回 AsyncIteratorResumeResumeWithParams 则在 checkpoint store 可用时继续执行,并允许按 address 提供 resume data。 源码在 TypedRunner

Runner 的实现也保留了类型分层:如果消息类型是 *schema.Message,会把 agent 转成 legacy-compatible 的 flowAgent; 否则走 typed flowAgent。恢复时,checkpoint 里保存的 streaming mode 是 source of truth,而不是新建 Runner 时传入的值。 事件处理循环遇到 interrupted action 时,会把内部 interrupt signal 转成公开 interrupt contexts,并在有 checkpoint id 时保存 checkpoint。 源码在 runner run implementationrunner resume implementationrunner event handling

九、ChatModelAgent 是 graph 上的 agent 循环

ChatModelAgent 不是跳出 graph runtime 自己写一套 agent loop。它仍然把 tools 放进 ToolsNode, 把模型调用、工具调用、checkpoint 和 interrupt 放回 compose 边界里。ToolsConfig 继承 compose.ToolsNodeConfig,再增加 ReturnDirectly 和 EmitInternalEvents。 注释强调:agent tool 的内部事件可以转发给父 agent 的 AsyncGenerator,但不会记录进父 agent state 或 checkpoint; Interrupted 例外,它会通过 CompositeInterrupt 传播,以便跨 agent 边界恢复。 源码在 ToolsConfig

TypedChatModelAgentConfig 里的配置可以分成四组:身份说明(Name、Description、Instruction)、 执行核心(Model、ToolsConfig、GenModelInput)、停止与输出(Exit、OutputKey、MaxIterations),以及运行切面 (Middlewares、Handlers、retry、failover)。其中 handler 注释很长,因为模型调用、工具调用、事件发送、 状态重写、动态工具列表和 prompt cache 都会被 handler 顺序影响。 源码在 TypedChatModelAgentConfig

这里最值得和 AgentScope 的 OpenAI Responses API 分析放在一起看。Eino 不是在 adapter 里简单判断一个 endpoint, 而是在类型层把两类消息分开:*schema.Message 使用完整 ReAct loop,即 model 到 tool calls 再到 model; *schema.AgenticMessage 使用 single-shot chain,因为 agentic models 自己处理工具调用。源码注释直接写在 TypedChatModelAgent 上;真正构建 run function 时,buildReActRunFunc 对两种消息类型做 type switch。 源码在 TypedChatModelAgent mode splitbuildReActRunFunc

普通 Message 分支会创建 newReact graph,再用 compose.NewChain 先把 agent input 转成 react input,然后 AppendGraph(g, WithNodeName("ReAct")),最后用 graph name、checkpoint store、serializer 和 max steps 编译成 runnable。运行时根据 EnableStreaming 调 runnable.Streamrunnable.Invoke。 源码在 message ReAct run function

AgenticMessage 分支也会构建 graph 和 chain,但源头是 newAgenticReact,输入是 *schema.AgenticMessage,模型类型是 model.AgenticModel。这让 provider 原生 agentic content blocks 和传统 chat history 在 Go 类型层、graph 编译层、事件层都保持分离。 源码在 agentic run function

Agent 真正运行时还会先 buildRunFunc 冻结默认执行函数;如果有 handler 在 BeforeAgent 阶段修改 tools 或 instruction, getRunFunc 可以按 runtime exec context 重新构图。Run 会创建 AsyncIterator / AsyncGenerator, 追加 bridge checkpoint id,把 tool infos 通过 model.WithTools 传进 compose options,再在 goroutine 里执行 run。 源码在 ChatModelAgent.Run

十、AgentTool 把多 agent 协作收束成工具边界

README 里 DeepAgent 被描述成能拆分复杂任务、委派给 sub-agents、跟踪进度,并可配置 shell、Python、web search 等工具。 同一 README 的 Composition 例子还展示了 graph 可以包装成 tool,再交给 agent 判断什么时候使用。 这两段说明 Eino 对 multi-agent 和 deterministic workflow 的连接方式不是另起一个 bus,而是继续回到 tool 与 graph。 证据在 DeepAgent and Composition

NewAgentTool 的注释把边界讲得很直接:被包装的 agent 必须有非空 Name 和 Description,因为它们会成为 tool name 和 description;内部事件可以转发给父 agent 的用户侧事件流,但不会记录到父 state 或 checkpoint。Exit、TransferToAgent、 BreakLoop 这些 action 都被限制在 agent tool 内部;只有 Interrupted 会通过 CompositeInterrupt 跨边界传播。 源码在 NewAgentTool

运行时,InvokableRun 会判断当前 tool 是否从 interrupt state 恢复。如果不是恢复,它会根据参数或 full chat history 构造 agent input,创建 runner,并带上 bridge checkpoint id 运行;如果是恢复,则创建 resume bridge store 并调用 runner.Resume。事件循环会关闭上一条 stream,必要时转发内部事件;最终如果最后一个 event 是 Interrupted, 就从 bridge store 取 checkpoint data,再返回 tool.CompositeInterrupt。 源码在 agent tool setup and resumeagent tool event loop

10.1 图结束只是产出,不是业务采用

对退款任务,到达 END 或 iterator 结束,只说明得到一个 DecisionDraft。 应用还要验证订单数据是否仍新鲜、必要规则是否齐全、是否存在未解决 interrupt,以及高风险动作是否获得批准。 最后由退款系统按幂等键接受写入并返回回执,才算 adopted。把 produced、validated、adopted 分开,才能避免“graph 跑完了,所以钱一定退了”的错误结论。

十一、和前几篇放在一起看

框架 先读的 owner 最容易误读的点
AgentScope agent turn ledger 与 OpenAI API 适配。 Responses API 不是 Chat Completions 的字段别名,而是事件化、item 化的输出模型。
ADK Python code-first Agent + Workflow + Runner。 不是只有 graph DSL,自主 agent 和确定性 workflow 共用运行边界。
Agno AgentOS platform control plane。 不是更大的 agent dataclass,而是 API、storage、approval、RBAC 和 interface 平台层。
AutoGen / MAF message runtime 到 production orchestration。 AutoGen 进入 maintenance mode 后,新项目应该从 MAF 的 Agent、Workflow、Hosting 读起。
CrewAI Agent / Task / Crew / Flow。 不要先找底层 bus;它先把团队自治和生产控制建模给应用开发者。
Eino Component / Runnable / compose Graph / ADK Runner。 不要把它只看成 Go 版 ReAct;ReAct agent 是 typed runnable graph 上的一层。
tRPC-Agent-Go Runner / session summary / memory / recall tools。 不要把 memory 看成聊天摘要;summary 压当前 session,memory 存长期事实。

到这里路线更清楚了:如果你要看 provider item 与 turn ledger,读 AgentScope;要看最小 coding harness、 session tree 与 compaction,读 Pi;要看 code-first agent / workflow,读 ADK;要看平台化 control plane,读 Agno; 要看消息 runtime 到生产 orchestrator 的迁移,读 AutoGen 和 MAF; 要看团队流程,读 CrewAI;要看 Go 生态里怎样把组件、流、中断和 agent 循环收束成可组合图,读 Eino; 要看 Go agent 怎么进一步变成服务端 runtime,并在其中处理 summary、memory 和隐藏历史召回,读下一篇 tRPC-Agent-Go。

参考源码与文档