一、先看手工串服务为什么很快失控
假设公司的订单、风控和搜索服务都用 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 放到后文作为另一种入口对照。
- 用户输入订单号,消息先进入执行图的起点。
- “读取订单”组件返回订单数据,类型边界保证下一节点拿到的是约定结构,而不是随意拼出的文本。
- 模型节点判断还需要风险信息,于是进入工具节点调用风控服务。
- 若资料齐全,图走向“生成建议”;Runnable 把模型的流式片段持续交给界面。
- 若缺少退款原因,Runner 在补问节点中断,并把当前位置和已有数据保存为 checkpoint。
- 用户补充原因后,Runner 恢复 channel、state 与 pending input,再由 task calculation 决定下一批节点。
- 图到达终点并返回建议;业务校验和外部退款系统再决定是否采用它。
第 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.go
和
compose/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,并暴露 Generate
和 Stream 两种交互。旧的 ChatModel.BindTools 被标为 deprecated,
因为它会原地修改实例并在并发时引入 race;新的 ToolCallingChatModel.WithTools 返回不可变 variant。
这就是 Go 风格:并发安全和类型边界先行。
源码在
components/model/interface.go。
工具接口也被拆成元数据和执行面。BaseTool.Info 返回 name、description 和参数 JSON schema;
真正执行时才需要 InvokableTool、StreamableTool,或能返回 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 call、
chat message parts、
Message struct
和
AgenticMessage 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 packing
和
default flow conversion。
Graph[I, O] 是把这些 Runnable 组织起来的 typed graph。NewGraph 可以带 local state generator;
AddEdge 明确要求上一节点输出类型匹配下一节点输入类型;Compile 把 graph 编译成 Runnable,
于是编译后的 graph 同样支持 Invoke / Stream / Collect / Transform。
源码在
NewGraph and state、
AddEdge 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 state
和
handle interrupt。
公开 interrupt API 也对应这条执行链。编译时可以声明 WithInterruptBeforeNodes 或
WithInterruptAfterNodes;普通组件用 Interrupt,需要保存组件内部状态时用
StatefulInterrupt,复合节点例如 ToolsNode 则用 CompositeInterrupt 把多个子中断压成一个可恢复信号。
源码在
interrupt compile options、
Interrupt and StatefulInterrupt
和
CompositeInterrupt。
恢复边界。 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 统一了 OnStart、OnEnd、OnError、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。Run 和 Query 返回 AsyncIterator;
Resume 与 ResumeWithParams 则在 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 implementation、
runner resume implementation
和
runner 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 split
和
buildReActRunFunc。
普通 Message 分支会创建 newReact graph,再用 compose.NewChain 先把 agent input 转成
react input,然后 AppendGraph(g, WithNodeName("ReAct")),最后用 graph name、checkpoint store、serializer 和 max steps
编译成 runnable。运行时根据 EnableStreaming 调 runnable.Stream 或 runnable.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 resume
和
agent 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。
参考源码与文档
- Eino README
- Eino root package docs
- Eino compose package docs
- Eino model interfaces
- Eino tool interfaces
- Eino
schema.Message - Eino
schema.AgenticMessage - Eino
Runnable - Eino generic graph
- Eino graph internals
- Eino graph runner
- Eino interrupt API
- Eino callbacks
- Eino ADK Runner
- Eino ChatModelAgent
- Eino AgentTool