Eino 架构与 Agent Engine 开发课
面向:几乎没写过 Go,但需要给 Code Agent 下任务、做架构选择和代码验收的 Agent Engine 负责人
版本基线:Einov0.9.15,Go1.21+
目标:建立 Eino 整体知识,能够设计、开发和评审独立的 Go/Eino Agent Engine
读法
第一次按顺序读第 0~7 章,先建立框架地图。第 8~16 章讲运行治理,适合带着具体设计问题读。附录提供 Code Agent 任务合同、架构评审清单和术语表。
每章最后有一道“闭卷检索题”。先不翻答案,写两三句话。能从记忆中说出边界,比看懂代码更重要。
本课程中的代码分两种:标为“骨架”的片段表达接口与依赖关系;标为“可编译方向”的片段接近真实 API,但仍需要 Code Agent 按锁定版本和具体 Provider 补全配置。不要复制一段代码就直接上线。
目录
- 第 0 章:先画出 Eino 地图
- 第 1 章:Schema、消息与模型
- 第 2 章:Agent、ChatModelAgent、Runner 与事件
- 第 3 章:对话历史、Memory、Session 与 Store
- 第 4 章:Tool、JSON Schema 与 ToolsNode
- 第 5 章:DeepAgent、Backend 与工作区执行
- 第 6 章:Compose、Chain、Graph、Workflow 与流式处理
- 第 7 章:GraphTool,把确定性流程放进 Agent
- 第 8 章:Handlers、中间件、重试与故障切换
- 第 9 章:Callback、Trace、指标与资源生命周期
- 第 10 章:运行时 Skill 与 ToolSearch
- 第 11 章:Interrupt、Resume、Checkpoint 与人工确认
- 第 12 章:TurnLoop、排队、抢占、取消与停止
- 第 13 章:A2UI、SSE 与产品事件边界
- 第 14 章:AgentTool、DeepAgent 与多 Agent 选择
- 第 15 章:生产安全、预算、幂等、测试与评测
- 附录 A:给 Code Agent 的总任务合同
- 附录 B:架构评审速查表
- 附录 C:术语表
第 0 章:先画出 Eino 地图
0.1 Eino 是什么
Eino 是 Go 语言的 LLM 应用开发框架。它提供四层能力:
| 层 | 主要包 | 你可以把它理解成 | 典型对象 |
|---|---|---|---|
| 数据合同 | schema |
框架里的共同语言 | Message、AgenticMessage、ToolInfo、Document |
| 原子能力 | components |
可替换的零件接口 | Model、Tool、Retriever、Embedding、Indexer |
| 编排 | compose |
有类型的执行图 | Runnable、Chain、Graph、Workflow、Checkpoint |
| Agent Runtime | adk |
Agent 循环和运行控制 | ChatModelAgent、Runner、AgentEvent、DeepAgent |
这四层的依赖方向应当从上往下:ADK 使用 Compose 和 Component,Component 使用 Schema。业务层可以调用它们,但 Eino 不知道你的 tenant_id、聊天表、审批规则或业务 artifact。
flowchart TB
Product[产品协议:HTTP / SSE / A2UI] --> App[业务应用层:Run / Session / Policy / Artifact]
App --> ADK[Eino ADK:Agent / Runner / TurnLoop]
ADK --> Compose[Eino Compose:Chain / Graph / Workflow]
ADK --> Components[Eino Components:Model / Tool / Retriever]
Compose --> Components
Components --> Schema[Eino Schema]
App --> Infra[DB / Queue / Object Store / Workspace Executor]0.2 Eino 不是什么
Eino 不是完整的 Agent 平台。以下责任仍在应用层:
- 对外 API、认证、租户隔离和限流
run_id、session_id、消息持久化和业务状态机- Tool 的授权、审计、幂等和副作用边界
- 模型与工具预算、计费、SLO 和告警
- SSE 事件合同、A2UI 映射和前端兼容
- artifact、evidence、评测集和回放语义
- Workspace Executor 或其他工作区执行器的生命周期
一句判断法:Eino 管“这次 Agent 怎么跑”;应用管“为什么允许跑、跑的是谁的任务、跑完留下什么可追责事实”。
0.3 三组容易混淆的仓库与 Skill
| 名称 | 用途 |
|---|---|
cloudwego/eino |
公共核心框架 |
cloudwego/eino-ext |
公共 Provider/存储等组件实现 |
cloudwego/eino-examples |
示例,不是生产模板 |
Code Agent 使用的 eino-guide、eino-component、eino-compose、eino-agent 是写代码时的知识插件。ADK 的 Skill Middleware 是 Agent 运行时按需加载指令和资源的能力。两者同名为 Skill,运行位置完全不同。
0.4 版本纪律
课程按 v0.9.15。如果项目锁在 v0.9.14,先读 release diff,再决定是否升级。不要让 Code Agent默认取 main:Eino 的 ADK 仍在快速变化,main 上的接口和稳定 tag 可能不一致。
v0.9.15 相比 v0.9.14 的核心变化很小,主要修正 ADK filesystem 的越界读取处理。但新服务仍应精确 pin 版本,因为“差异很小”不是允许浮动依赖的理由。
0.5 给 Code Agent 的任务合同
mode: investigate
repo: <新 Go 服务仓库>
branch: <当前分支,只读>
scope: Eino 依赖与架构入口
allowed: 读取 go.mod、源码、Eino v0.9.15 release/source
forbidden: 修改文件、升级依赖、访问真实凭据、调用生产模型
output:
1. 当前 Eino/eino-ext 精确版本
2. schema/components/compose/adk 在本项目中的入口文件
3. 应用层自有的 Run/Session/Policy/Event/Artifact 模块
4. 过时接口或版本漂移风险,附源码证据0.6 评审清单
go.mod是否精确锁定 Eino 与扩展版本- Provider、存储与回调能力是否来自受维护的扩展,而非复制一份私有实现
- Eino 对象有没有直接依赖 HTTP DTO、数据库实体或前端事件
- 应用层是否明确拥有 Run、Session、Policy、Event 和 Artifact
- 示例代码是否经过锁定版本的编译测试
0.7 闭卷检索题
如果模型成功返回文本,但 SSE 连接断了,应该由 Eino ADK 还是业务应用层决定重连和事件补发?为什么?
0.8 一手资料
第 1 章:Schema、消息与模型
1.1 Schema 是运行合同,不是普通结构体集合
模型、工具、图节点和回调需要共享数据语义。schema 提供这些类型。先掌握四个:
schema.Message:经典聊天消息,角色包括 system、user、assistant、toolschema.AgenticMessage:v0.9 的 Agentic 消息,以 ContentBlock 表达文本、推理、工具调用和工具结果schema.ToolInfo:模型看到的工具名称、描述与输入 Schemaschema.StreamReader[T]:单消费者的流式读取器
1.2 两条消息轨
经典轨:schema.Message
适合已有 Chat Completions Provider、Eino 内置经典 ReAct、需要明确在客户端执行 Tool 的场景。一个 assistant 消息可以带 Tool Calls,随后必须有对应 ToolCallID 的 tool 消息。
messages := []*schema.Message{
schema.SystemMessage("You are a governed analysis agent."),
schema.UserMessage("分析 campaign 123"),
}
reply, err := chatModel.Generate(ctx, messages)Agentic 轨:schema.AgenticMessage
适合原生 Agentic Model。内容由 block 组成,可以保留 Provider 原生的工具、推理和多模态语义。
messages := []*schema.AgenticMessage{
schema.SystemAgenticMessage("You are a governed analysis agent."),
schema.UserAgenticMessage("分析 campaign 123"),
}
reply, err := agenticModel.Generate(ctx, messages)model.AgenticModel 本质上是 BaseModel[*schema.AgenticMessage]。它只有 Generate 和 Stream 两类核心调用。工具通常通过每次请求的 option 传递,不应靠修改共享模型实例来绑定。
1.3 Greenfield 怎么选
新服务优先评估 Agentic 轨,但不能仅凭“新”字决定。要问:
- 目标模型平台的 Agentic 接口是否已稳定?
- 需要的 Tool Calling、流式事件、取消和 retry 在该轨是否已接线?
- 产品是否要保留 Provider 原生 ContentBlock?
- 现有下游是否只接受经典 Message?
v0.9 源码明确指出:经典 Message 的 ADK 功能更完整;AgenticMessage 单 Agent 可用,但部分流式取消和 retry 能力仍有限。因此业务应用可以把 Agentic 设为目标合同,同时为已验证的经典 ReAct Adapter 保留兼容实现。不要在一个 run 中随意来回转换。
1.4 Message 不是聊天记录表
schema.Message 表达一次模型上下文中的消息。数据库消息还需要业务字段:tenant、session、run、版本、可见性、脱敏级别、创建者和证据引用。直接把 Eino Message JSON 当数据库模型,会让框架升级变成数据迁移。
建议使用显式投影:
DomainMessage --project--> ModelContextMessage
AgentEvent --adapt----> ProductEvent历史 run 通常只投影用户问题、最终回答、摘要和必要证据。不要把所有旧 Tool Call/Tool Result 原样塞回下一次模型上下文。当前 run 内的 Tool Call 与 Tool Result 则必须成对,不能删除一半。
1.5 流的三个规则
StreamReader必须关闭,最好在拿到后立即defer reader.Close()- 流通常只能消费一次;回调、日志和业务若都要读,需要在正确位置复制或做事件分发
- 读到
io.EOF是正常结束,其他错误才是失败
Eino 的 model stream 是 token/content block 流。产品 SSE 是带 event_id、run 状态、重连语义的外部协议。两者不能直接等同。
1.6 给 Code Agent 的任务合同
实现一个只做消息边界验证的 spike:
- 版本:github.com/cloudwego/eino v0.9.15
- 同时定义 DomainMessage 与到 schema.AgenticMessage 的单向投影
- 不把 Eino 类型写入数据库实体或 HTTP DTO
- 为 text、tool call、tool result、非法未配对结果写表驱动测试
- 流式示例必须关闭 reader,并正确区分 EOF 与错误
- 输出一份 classic Message 与 AgenticMessage 能力差异说明
不要接真实 Provider,不要引入重试。1.7 评审清单
- 是否明确选定消息轨,还是在代码中隐式混用
- Tool Call ID 与 Tool Result 是否严格关联
- 是否把 reasoning 或敏感 Provider 元数据直接写入日志
- 是否关闭所有 stream;错误路径是否也能关闭
- 业务存储是否通过投影隔离 Eino 类型
- 是否把共享模型实例做了可变的 per-request Tool 绑定
1.8 闭卷检索题
为什么历史消息可以只保留“结论和证据投影”,而当前 run 的 Tool Call 与 Tool Result 不能只留一边?
1.9 一手资料
第 2 章:Agent、ChatModelAgent、Runner 与事件
2.1 从模型调用到 Agent 运行
ChatModel 只回答“一次调用”。Agent 需要决定是否调用工具、把工具结果交回模型、何时结束,以及如何发出过程事件。
经典 ReAct 的最小循环是:
flowchart LR
I[输入消息] --> M[模型调用]
M -->|普通回答| F[最终输出]
M -->|Tool Calls| T[执行工具]
T --> R[追加 Tool Results]
R --> MChatModelAgent 提供这个循环。Runner 是标准运行入口,负责启动、恢复、checkpoint 接线和事件迭代。业务服务应调用 Runner,而不是到处直接调用 Agent 的 Run。
2.2 经典 ChatModelAgent 骨架
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "governed_analysis",
Description: "Runs governed analysis tasks",
Instruction: systemPrompt,
Model: chatModel,
ToolsConfig: toolsConfig,
MaxIterations: 8,
Handlers: handlers,
})
if err != nil {
return err
}
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent: agent,
EnableStreaming: true,
CheckPointStore: checkpointStore,
})
iter := runner.Query(ctx, userQuery)
for {
event, ok := iter.Next()
if !ok {
break
}
if event.Err != nil {
return event.Err
}
// 交给应用事件适配器,不要直接写 SSE
}Agentic 轨使用泛型构造:NewTypedChatModelAgent[*schema.AgenticMessage] 与 NewTypedRunner。它不是把类型名替换一下就结束;你必须核对目标能力在 Agentic 路线是否已实现。
2.3 AgentEvent 应该怎么看
事件可能包含:
Output:完整消息或消息流Action:中断、退出等运行动作Err:这次事件携带的错误AgentName:事件来源RunPath:嵌套路径;对 AgentTool/DeepAgent 通常很简单
不要假设每个事件都是最终回答,也不要通过自然语言猜终态。应用适配器应按结构化字段和自己的状态机映射。
流式输出还带一个所有权问题:如果事件中的 MessageStream 没被消费,也要关闭。框架建议对自定义 Agent 发出的流设置自动关闭;业务消费者仍应做到显式生命周期管理。
2.4 最大迭代数不是预算系统
MaxIterations 防止 Agent 无限循环,但它不等于:
- 最大模型调用成本
- 最大工具副作用次数
- wall time
- Provider retry 次数
- 并发 run 配额
业务应用层应分别定义 max_model_turns、max_tool_calls、wall_timeout、每工具 deadline、token/cost budget。然后把能下推的限制交给 Eino,其他限制由 Supervisor/Broker 执行。
2.5 给 Code Agent 的任务合同
实现一个 Fake Model 驱动的 ChatModelAgent 最小运行测试:
- 使用真实 Eino Agent loop,不自己写 for 循环模拟 ReAct
- 第一 turn 返回一个 tool call,第二 turn 返回最终文本
- Runner 统一启动,收集所有 AgentEvent
- 断言模型调用次数、工具调用次数、ToolCallID 配对和最终输出
- max iterations 设为显式小值
- 覆盖模型错误、工具错误、context cancel、流未消费的关闭
- 不接 HTTP、数据库、真实模型网关2.6 评审清单
- 是否误写了一套自己的 ReAct 循环
- 是否所有运行都经 Runner,便于恢复和事件治理
- 是否区分过程事件、输出、动作和错误
- 是否用自然语言内容判断
answered、cancelled等终态 - 是否把
MaxIterations当成全部预算 - Fake Model 是否确定性验证真实 Agent loop
2.7 闭卷检索题
Runner 和 ChatModelAgent 的职责有什么区别?为什么 HTTP handler 不应直接遍历一堆模型与工具调用?
2.8 一手资料
第 3 章:对话历史、Memory、Session 与 Store
3.1 四个概念分开
| 概念 | 含义 | 默认所有者 |
|---|---|---|
| Model context | 某次模型调用实际收到的消息 | Agent + 应用投影器 |
| Conversation history | 用户可见的多轮对话事实 | 应用数据库 |
| Session values | 一次会话或运行中的键值上下文 | 应用定义,ADK 可在运行中携带 |
| Checkpoint | 中断后恢复执行所需的框架状态 | Eino Store + 应用保管策略 |
Memory、Session 和 Store 在业务语义上不是 Eino core 替你完成的产品能力。Eino 有 Session value、run-local value 和 CheckPointStore 接口,但“该存什么、保留多久、谁能读”仍由业务应用决定。
3.2 推荐的数据流
flowchart LR
DB[(Domain messages)] --> P[Context projector]
P --> C[Model context]
C --> R[Eino Runner]
R --> E[Agent events]
E --> A[Event adapter]
A --> DB
R <--> CP[(Checkpoint store)]Context projector 应处理:
- 窗口截断与 token 预算
- 历史摘要
- evidence 引用
- Tool Call/Result 成对校验
- 不同角色和内容块的兼容
- tenant 与敏感数据过滤
3.3 三种状态的生存期
Run-local state 只在一次执行及其 resume 中存在,适合本轮计数、临时选择和中间游标。需要 checkpoint 的自定义类型必须可序列化并注册。
Session state 跨多轮,例如用户偏好、选中的数据域、已确认的工作区。它必须有业务 Schema、版本和存储策略。
Business facts 是报告状态、审批结论、证据 hash 等真相。它们不能只存在 checkpoint。Checkpoint 是恢复 payload,不是业务事实源。
3.4 Prompt 长度不是用删消息解决的
简单删掉最老消息会破坏 Tool 配对,也可能删掉用户已确认的约束。生产策略通常是:
- 保留当前用户请求和本轮完整执行链。
- 保留系统约束和不可丢的业务确认。
- 把更早历史压成有版本的摘要。
- 证据保留引用和摘要,需要时通过工具再取。
- 达到硬预算时明确失败或要求新会话,不静默改变语义。
3.5 给 Code Agent 的任务合同
设计并实现 ContextProjector:
- 输入是 DomainConversation,不接收数据库 entity 指针
- 输出是选定消息轨的模型消息
- 当前 run 的 tool call/result 必须成对且顺序合法
- 历史 run 只保留用户输入、最终回答、摘要与 evidence refs
- 预算按 token estimator 或可测字符上限执行
- 明确定义不可裁剪消息和超预算错误
- 表驱动测试覆盖空历史、断裂工具对、长历史、敏感消息和摘要版本
- Checkpoint 内容不得被当作业务事实回写3.6 评审清单
- 数据库是否保存领域合同,而非直接保存 Eino 对象
- 是否把 Run-local、Session 和 Business facts 混在一个 map
- 历史裁剪是否可能留下孤立 Tool Result
- 摘要是否有来源 run、版本和可追溯 evidence
- Checkpoint 是否设置 TTL、tenant 隔离和加密要求
- resume 后是否会重复提交已有业务副作用
3.7 闭卷检索题
用户已批准一个写操作,Agent 在工具执行前中断。批准结果、Eino checkpoint 和最终业务写入分别应该存在哪里?
3.8 一手资料
第 4 章:Tool、JSON Schema 与 ToolsNode
4.1 Tool 有两份合同
Tool 的第一份合同给模型看:名称、描述、参数 JSON Schema。第二份合同给执行系统:身份、权限、限额、幂等、审计、deadline 和错误分类。
模型生成了合法 JSON,只说明它通过了语法门槛,不说明用户有权执行。
flowchart LR
M[Model Tool Call] --> S[Schema validation]
S --> P[Policy / permission]
P --> B[Broker preflight]
B --> X[Tool execution]
X --> A[Audit + evidence]
A --> R[Canonical result]4.2 工具实现形态
Eino Tool 主要按是否流式、是否需要增强输入区分:
- Invokable Tool:JSON 输入,完整字符串输出
- Streamable Tool:JSON 输入,流式输出
- Enhanced Tool:除参数外还能拿到 ToolCall 上下文或更丰富输入
ToolsNode:读取 assistant 的 Tool Calls,查找工具并执行,产出 Tool Results
工具的 Info() 决定模型看到什么;InvokableRun 或流式方法执行真正逻辑。Schema 要尽量平坦、明确、闭合,尤其要考虑 Provider 对 $defs、allOf 等特性的兼容。
4.3 Tool Schema 的写法
type ExecuteOperationInput struct {
Operation string `json:"operation"`
Input map[string]any `json:"input"`
}
// 骨架:实际项目应使用 Eino 提供的 Schema helper 或明确构造 ToolInfo。
func (t *ExecuteOperationTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "execute_operation",
Desc: "Execute one approved 业务操作 in the current run",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"operation": {
Type: schema.String, Required: true,
Desc: "One operation from the approved closed set",
},
"input": {
Type: schema.Object, Required: true,
Desc: "Operation-specific input validated again by the broker",
},
}),
}, nil
}描述必须写行为边界,不能写“万能分析工具”。枚举、必填字段和互斥关系应尽量进 Schema,但执行前仍要用 Go 领域合同二次验证。
4.4 批量 Tool Calls 的语义
一个 assistant response 可以一次返回多个 Tool Call。先决定是否并行:
- 纯读、互不依赖、限流允许时可并行。
- 共享工作区、存在顺序依赖、写操作或要求稳定 evidence 顺序时应串行。
ToolsNodeConfig.ExecuteSequentially = true 可以要求按顺序执行。生产系统可以先对整批调用做 preflight,再按 FIFO 串行执行,避免第一项已经产生副作用、第二项才发现无权限。
4.5 错误要分两层
Tool 可恢复错误可以作为稳定 JSON Tool Result 返回给模型,让它改参数或换方案。此时 Go error 通常为 nil,否则 Agent loop 会直接终止。
系统错误包括事件持久化不确定、凭据异常、合同破坏和内部 panic。它们应终止 run,不能包装成普通工具输出让模型继续。
稳定错误示例:
{
"schema_version": 1,
"ok": false,
"error": {
"category": "runtime",
"code": "broker_not_ready",
"message": "The business executable is not configured.",
"retriable": false
}
}不要把内部堆栈、路径、凭据或原始 Provider body 返回模型。
4.6 给 Code Agent 的任务合同
实现 read_skill 与 execute_operation 两个 Eino Tool wrapper:
- ToolInfo 的名称、描述、顺序和 Schema 来自已验证的 RunRequest
- wrapper 不实现业务选择,只调用 run-scoped Broker
- 整批 Tool Calls 在任何执行前完成 preflight
- ExecuteSequentially=true,严格按 ToolCallID 和响应顺序消费
- typed business failure 返回 canonical JSON 且 Go error=nil
- 审计/合同/提交状态不确定等系统错误返回 Go error
- 不记录参数正文、凭据或原始错误 body
- 测试批量拒绝无部分副作用、FIFO、取消、deadline 和非法 ToolCallID4.7 评审清单
- Schema 是否与 Go 执行合同一致
- Tool 名称是否稳定且处于闭集
- 是否把模型参数合法等同于已授权
- 批量调用是否可能部分通过 preflight 后产生副作用
- 串并行选择是否写进合同和测试
- 错误是否区分可恢复业务错误与系统错误
- Tool Result 是否泄露路径、凭据、SQL 或内部堆栈
4.8 闭卷检索题
为什么“第二个 Tool Call 没权限”最好在第一个 Tool Call 执行前就发现?这要求 Broker 提供什么接口?
4.9 一手资料
第 5 章:DeepAgent、Backend 与工作区执行
5.1 DeepAgent 解决什么问题
普通 ChatModelAgent 适合有限工具、较短步骤的任务。DeepAgent 在它之上组合了长任务常用能力:任务拆分、文件系统工具、较大工具结果卸载、上下文压缩、可选 Shell,以及通过 task 工具调用子 Agent。
DeepAgent 仍然是 Agent Runtime。它不会自动提供安全沙箱、代码仓库权限和进程审计。
agent, err := deep.New(ctx, &deep.Config{
Name: "governed_deep_agent",
Description: "Coordinates governed long-running analysis",
ChatModel: chatModel,
Instruction: prompt,
ToolsConfig: toolsConfig,
MaxIteration: 12,
Backend: backend,
Shell: shell,
Handlers: handlers,
})配置 Backend 后可以注册 read/write/edit/glob/grep 等文件工具。配置 Shell 或 StreamingShell 后可以执行命令;两者互斥。
5.2 Backend 是能力接口,也是安全边界
Backend 抽象文件读取、写入、编辑、glob 和 grep。实现可以是内存、受限目录或远程工作区。你要额外定义:
- 所有路径如何规范化,是否允许符号链接
- 根目录在哪里,能否越界读取
- 单文件、单次响应和 run 总量上限
- 二进制、大文件和敏感文件如何处理
- 多 run 是否共享工作区
- 写入和删除如何审计
不要把宿主机根目录直接暴露给 DeepAgent。即便提示词写了“不要访问”,模型输出也不是权限控制。
5.3 Shell 不是普通 Tool
Shell 可以启动任意子进程,风险高于查询型工具。至少需要:
- 命令 allow/deny policy
- 明确工作目录和环境变量白名单
- 无用户 shell profile 的非交互执行
- wall timeout、输出字节上限和进程组清理
- 网络、文件系统、CPU、内存的隔离
- stdout/stderr 脱敏与 artifact 策略
如果业务已经有独立的 Workspace Executor,就不要再给 Eino DeepAgent 一个同等权限的本地 Shell。两个执行面会产生权限漂移、重复重试和不可比较证据。
5.4 外部工作区执行器的推荐边界
当工作区操作由独立执行器负责时,建议保持下面的调用边界:
flowchart LR
E[Eino Agent] --> T[execute_workspace Tool]
T --> B[Policy Broker]
B --> O[Workspace Executor]
O --> W[Run-scoped workspace]
O --> A[Commands / artifacts / evidence]
A --> B
B --> EEino 决定何时需要工作区任务;Broker 验证操作;Workspace Executor 负责真实文件和命令执行。Eino 可以有只读、虚拟或 artifact-oriented Backend,但不能绕过 Broker 直接写宿主机。
5.5 什么时候选 DeepAgent
适合:需要文件工作记忆、长上下文压缩、任务清单和受控子 Agent 的复杂任务。
不适合:单个确定性流程、一次工具调用、强事务写操作、只需短 ReAct 的查询。使用 DeepAgent 会带来更多提示词、工具和状态,需要用评测证明收益。
5.6 给 Code Agent 的任务合同
做一个 DeepAgent filesystem 安全 spike:
- 使用内存或临时 run-scoped Backend,不接宿主机任意路径
- 只开放 read/write/glob/grep 的最小集合
- 拒绝绝对路径、路径穿越、符号链接越界和超限文件
- 不配置本地 Shell;工作区命令只能走 Fake Workspace Broker
- 记录命令合同与 evidence hash,不保存敏感正文
- 覆盖两个并发 run 的目录隔离与取消清理
- 比较普通 ChatModelAgent 与 DeepAgent 的工具数、prompt token 和完成率5.7 评审清单
- DeepAgent 是否因真实需求引入,而非因为模板方便
- Backend 是否 run-scoped、路径规范化且 fail closed
- Shell 是否绕过了 Workspace Executor 和 Broker
- 大工具结果是否有卸载与引用方案
- 工作区命令、artifact 与 AgentEvent 是否能用 run_id 关联
- 取消后子进程和临时目录是否有界清理
5.8 闭卷检索题
为什么已经有 Workspace Executor 时,不应再给 Eino 一个等价的本地 Shell?至少说出两个故障后果。
5.9 一手资料
第 6 章:Compose、Chain、Graph、Workflow 与流式处理
6.1 Compose 的基本单位是 Runnable
Runnable[I, O] 表达一个有类型的计算:输入 I,输出 O。它通常支持四种调用形态:
| 输入 | 输出 | 常见方法 |
|---|---|---|
| 单值 | 单值 | Invoke |
| 单值 | 流 | Stream |
| 流 | 单值 | Collect |
| 流 | 流 | Transform |
这四种形态是内部数据流语义,不是四种 HTTP 接口。
6.2 Chain、Graph、Workflow 怎么选
Chain:线性为主,前一个输出给后一个,代码最短。适合 prompt → model → parser。
Graph:显式节点、边、分支和循环。适合 ReAct、自定义路由、可恢复执行,以及需要看到拓扑的流程。
Workflow:围绕字段映射组织 DAG。适合多个节点读取输入不同字段,再把结果装配成结构化输出。
选择最小表达力:Chain 能清楚表达就不建 Graph;需要回边或条件路由再用 Graph;重点是字段级依赖时用 Workflow。
6.3 Graph 骨架
type Input struct {
Query string
}
type Output struct {
Answer string
}
graph := compose.NewGraph[*Input, *Output](
compose.WithGenLocalState(func(ctx context.Context) *runState {
return &runState{}
}),
)
_ = graph.AddLambdaNode("validate", validateLambda)
_ = graph.AddChatModelNode("generate", chatModel)
_ = graph.AddLambdaNode("format", formatLambda)
_ = graph.AddEdge(compose.START, "validate")
_ = graph.AddEdge("validate", "generate")
_ = graph.AddEdge("generate", "format")
_ = graph.AddEdge("format", compose.END)
runnable, err := graph.Compile(ctx)
result, err := runnable.Invoke(ctx, input, compose.WithCallbacks(handler))v0.9 共享本次运行状态应从 NewGraph(...WithGenLocalState(...)) 创建。不要跟随旧文章使用已经被替代的 StateGraph/StateChain 构造方式。
6.4 Local state 不是全局缓存
Local state 应由每个 run 单独生成。常见错误是把一个指针捕获在 graph 外,所有请求共用,结果产生数据竞争和租户串线。
节点读写 state 要经过框架提供的 state 处理函数,并保持最小字段。不要把 DB client、HTTP response writer 或不可序列化的大对象塞进去。需要 checkpoint 时,自定义状态还要满足序列化要求。
6.5 流式节点的现实成本
流经过分支时,分支条件可能消费 stream。多个下游同时读取时需要复制;任何一个分支不读完或不关闭都可能卡住上游。流式图评审要画“谁创建、谁消费、谁关闭”。
如果一个中间节点必须看到完整内容才能判断,不要伪装成逐 token 流。明确在该节点 Collect,然后输出新流或单值。
6.6 Compile 是架构检查点
Graph 定义是 builder;Compile 后得到 Runnable。生产服务通常在启动期完成装配和 Compile,而不是每次请求重建图。启动期失败应进入 readiness,不要等首个用户请求才暴露。
但 per-run 的工具列表、tenant policy 和 session 数据不能通过修改共享 compiled graph 注入,应使用 option、context、run-local state 或请求级不可变依赖。
6.7 给 Code Agent 的任务合同
实现一个 typed Graph:validate -> fetch -> analyze -> format
- 所有节点输入输出使用明确结构体,不用 map[string]any 穿透全图
- run-local state 通过 NewGraph 的 state generator 每次创建
- 图在服务启动时 Compile,失败影响 readiness
- fetch 和 analyze 的错误分别分类,不用字符串匹配
- 同时测试 Invoke 与 Stream;记录每个 stream 的创建、消费和关闭方
- callbacks 通过 compose call option 注入
- 用 Fake nodes 做分支、取消和并发 run 隔离测试6.8 评审清单
- 选 Chain/Graph/Workflow 是否有具体理由
- 节点间是否有明确类型,还是全部使用
any - local state 是否每个 run 新建
- compile 是否在启动期,错误是否进入 readiness
- 流的单消费者和关闭责任是否清楚
- 是否在请求中修改共享 Runnable 或模型对象
6.9 闭卷检索题
一个节点需要读取完整模型输出才能决定路由。它还能声称端到端逐 token streaming 吗?应该如何表达真实语义?
6.10 一手资料
第 7 章:GraphTool,把确定性流程放进 Agent
7.1 为什么需要 GraphTool
有些工作要由 Agent 决定“是否做”,但一旦开始,内部步骤必须确定。例如:验证报告 ID → 查询数据 → 计算指标 → 校验 claim → 返回 evidence pack。把每一步都暴露为独立 Tool,会让模型自由改顺序、漏步骤或重复调用。
GraphTool 的思路是把整个 typed Graph 包成一个 Tool:外层 Agent 自主选择,内层流程按图执行。
flowchart LR
A[Agent] -->|决定调用| GT[GraphTool: analyze_report]
subgraph Deterministic graph
V[Validate] --> F[Fetch]
F --> C[Compute]
C --> Q[Claim validation]
Q --> P[Evidence pack]
end
GT --> V
P --> GT
GT --> A7.2 先澄清一个包边界
Eino README 展示了 graphtool.NewInvokableGraphTool,对应实现与示例目前在 eino-examples/adk/common/tool/graphtool。它表达的是推荐模式,不应不加判断地从 examples 复制进生产。
生产有两个选择:
- 若内部或稳定扩展已有受维护的 GraphTool,锁定它的模块与版本。
- 若没有,自己写一个很薄的 Tool adapter:
Info()暴露输入 Schema,InvokableRun()反序列化输入并调用已编译 Runnable。adapter 不应重新实现 Graph。
7.3 一个薄 adapter 应该长什么样
type AnalysisGraphTool struct {
runnable compose.Runnable[*AnalysisInput, *AnalysisOutput]
info *schema.ToolInfo
}
func (t *AnalysisGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return t.info, nil
}
func (t *AnalysisGraphTool) InvokableRun(
ctx context.Context,
arguments string,
opts ...tool.Option,
) (string, error) {
in, err := decodeAndValidate(arguments)
if err != nil {
return canonicalInputError(err), nil
}
out, err := t.runnable.Invoke(ctx, in)
if err != nil {
return "", err
}
return encodeOutput(out)
}这是结构骨架。真实 Tool 接口签名要按所选 classic/enhanced 形态和 v0.9.15 源码确认。
7.4 GraphTool 和子 Agent 的区别
GraphTool 内部路由由代码控制,适合必须按序、可独立单测的业务流程。子 Agent 内部仍由模型决策,适合任务描述开放、步骤难以预先枚举的工作。
如果一个“子 Agent”只能走固定的五步,它更像 GraphTool。如果一个 Graph 中大部分节点只是让模型决定下一步,它可能更适合普通 Agent。
7.5 业务应用中的候选 GraphTool
build_evidence_pack:读数据、规范化、hash、验证完整性validate_claims:逐 claim 检查来源与计算口径prepare_workspace_task:验证任务合同、生成只读 snapshot、提交 Workspace Executorfinalize_run_artifacts:闭合事件、生成 final 与 validation
最后一项通常更适合 Supervisor 固定执行,而不是让模型决定是否调用。这也是边界判断:涉及 run 终态完整性的步骤不能依赖模型自觉。
7.6 给 Code Agent 的任务合同
把 build_evidence_pack 实现为 Graph + Tool adapter:
- 外层 Agent 只看到一个工具
- 图内节点为 validate/fetch/normalize/hash/verify
- 输入输出是版本化结构体
- 图启动时 Compile;Tool adapter 只做 decode、invoke、encode
- 所有节点无隐式 retry,无直接 SSE 写入
- 验证失败不生成“成功” evidence pack
- Fake repository 下做确定性单测,并验证任一步失败不执行后续副作用
- 说明 GraphTool 实现来源与版本,禁止直接复制未维护示例7.7 评审清单
- 该流程是否真需要模型决定内部步骤
- GraphTool adapter 是否薄,业务逻辑是否仍在 Graph 节点
- 输入输出是否可版本化、可独立验证
- Graph compile 是否复用,run state 是否隔离
- 失败后是否错误地产生完整 artifact
- 必做的 run 终态步骤是否被放进可选 Tool
7.8 闭卷检索题
“生成 final.json 并关闭 run”为什么不应设计成模型可选的 GraphTool?
7.9 一手资料
第 8 章:Handlers、中间件、重试与故障切换
8.1 v0.9 的推荐扩展点
ChatModelAgent 支持基于接口的 Handlers []adk.ChatModelAgentMiddleware。自定义 handler 通常嵌入 *adk.BaseChatModelAgentMiddleware,只覆盖需要的方法。
type AuditHandler struct {
*adk.BaseChatModelAgentMiddleware
sink AuditSink
}
func (h *AuditHandler) WrapInvokableToolCall(
ctx context.Context,
next adk.InvokableToolCallEndpoint,
tc *adk.ToolContext,
) (adk.InvokableToolCallEndpoint, error) {
return func(ctx context.Context, args string, opts ...tool.Option) (string, error) {
// before: 只记录安全元数据
result, err := next(ctx, args, opts...)
// after: 按提交语义记录终态
return result, err
}, nil
}旧的 struct/closure 扩展方式已经标记为 deprecated。新服务应实现 interface-based handler,不应为了少写几个方法继续建立旧接口依赖。
8.2 Handler 的四类工作
BeforeAgent/AfterAgent:运行级装配和收尾BeforeModelRewriteState/AfterModelRewriteState:修改可持久的模型输入状态、动态 ToolInfoWrapModel:模型调用周围的计时、事件和响应处理Wrap...ToolCall:工具调用周围的授权、审计和结果处理
动态工具选择应改 state 中的 ToolInfo/DeferredToolInfo,或在 BeforeAgent 改可执行工具集合。不要在 WrapModel 临时修改工具绑定:变化不会成为 Agent state 的事实,还会破坏 prompt cache。
8.3 包装顺序会改变语义
Handlers 按注册顺序包装,第一项在最外层。若配置 [A, B, C],实际是 A(B(C(target)))。因此:
- before 顺序通常是 A → B → C
- after 顺序通常是 C → B → A
- 事件发送器放在哪里,决定它看到原始输出还是清洗后的输出
评审不能只看“注册了审计 handler”,还要看它在脱敏、retry、failover 和事件发送器的哪一层。
8.4 Retry 必须按操作语义设计
模型读调用在满足条件时可以 retry。写工具不能因为网络超时就盲目 retry,因为第一次可能已经提交。判断矩阵:
| 操作 | 自动 retry 条件 |
|---|---|
| 模型 Generate | Provider 明确可重试、预算允许、没有流式部分提交 |
| 只读 Tool | 幂等、deadline 有余量、错误明确为调用前失败 |
| 写 Tool | 有 idempotency key,且下游可查询提交状态 |
| 事件写入 | 只有 writer 明确证明未提交时才可重试同一事件 |
max_retries 是上限,不是“必须至少重试这么多次”。建议默认关闭隐式模型和工具 retry;只有合同、预算与幂等条件明确后才单独启用。
8.5 Failover 不是 retry 的别名
Retry 对同一模型重试;failover 选择另一个模型。换模型可能改变 Tool Calling、JSON Schema 支持、token 口径和内容安全行为。必须定义:哪些错误允许换、备选模型是否合同等价、usage 如何累计、事件如何记录。
8.6 给 Code Agent 的任务合同
实现 interface-based Agent handlers:
- registration order 固定为 policy -> audit -> metrics -> sanitizer
- 用测试证明 before/after 的真实顺序
- 动态工具过滤修改持久 state,不在 model wrapper 临时绑定
- retry policy 按 model/read-tool/write-tool/event-writer 分开
- 默认 retry=0;启用必须写预算和幂等依据
- failover 只对闭集错误开放,并在事件中记录实际 provider/model
- 日志和事件不得包含原始工具参数与 reasoning8.7 评审清单
- 新代码是否使用 interface-based Handlers
- wrapper 顺序是否有测试,而非靠注释猜
- 动态工具列表是否同时影响“模型可见”和“实际可执行”
- retry 是否可能重复副作用
- failover 模型是否真的满足相同 Tool Schema 合同
- retry/failover 次数是否计入 turn、token、wall-time 预算
8.8 闭卷检索题
一个写工具返回超时,但不能证明下游未提交。为什么不能自动 retry?下一步应该是什么?
8.9 一手资料
第 9 章:Callback、Trace、指标与资源生命周期
9.1 Callback 和 Handler 不重复
Handler 改变 Agent 的运行行为,知道 Agent state 和 Tool context。Callback 观察 Component 生命周期,适合统一 trace、日志和指标。
判断方法:如果逻辑要决定“允不允许工具执行”,用 Handler/Broker;如果只记录“模型调用用了多久”,用 Callback。不要把权限判断藏在一个全局观测 callback 里。
9.2 观测需要三层 ID
trace_id:跨服务调用链run_id:一次 Agent 运行的业务身份model_call_id/tool_call_id:一次具体 attempt
只有 trace_id 无法回答“这个 run 的第二个工具是否闭合”。只有 run_id 又无法定位跨服务延迟。三层要在事件和安全日志中关联。
9.3 Callback 接入
全局 callback 用追加式注册函数接入;单次 Compose 调用用 compose.WithCallbacks。不要调用旧的全局覆盖式初始化函数,否则可能把其他库已注册的 handler 清掉。
callbacks.AppendGlobalHandlers(globalTraceHandler)
out, err := runnable.Invoke(
ctx,
input,
compose.WithCallbacks(runHandler),
)全局 handler 必须线程安全,也不能保存某个租户或某个 run 的可变状态。
9.4 指标先回答运营问题
建议最少有:
| 类别 | 指标 |
|---|---|
| Run | started/completed/failed/cancelled/timed_out、duration |
| Model | calls、first output latency、duration、tokens、provider errors |
| Tool | requested/started/succeeded/failed、duration、denied |
| Queue | wait duration、active runs、rejected |
| Artifact | validation failure、evidence missing、writer uncertainty |
标签必须是低基数闭集。不要把 run_id、query、error message 或文件路径放进 metrics tag。
9.5 流式首包不能只测模型
用户感知首包是:请求进入 → 排队 → context 构造 → 首次模型调用 → 适配产品事件 → SSE flush。应分别测量模型首次输出与端到端 TTFT,并用 run/model call ID 对齐。
9.6 不可变输入与 stream close
观测代码最容易引入两个隐蔽 bug:
- 为加标签修改输入 message 或 option,导致其他消费者看到变化。
- 为记录流内容先读一次,业务消费者随后读到空流。
Callback 应把输入视为不可变。需要记录流式统计时,用框架支持的复制/tee 机制,并确保每个 reader 在正常、错误、取消路径上都关闭。
9.7 给 Code Agent 的任务合同
实现 Eino observability adapter:
- 全局 callback 用 append 语义注册,不能覆盖已有 handlers
- per-run callback 通过 compose option 注入
- trace_id/run_id/model_call_id/tool_call_id 可关联
- metrics tags 只允许闭集字段
- 不记录 prompt、reasoning、tool args、provider body 和凭据
- callback 不修改输入,不抢先消费单消费者 stream
- 测试正常、EOF、模型错误、工具错误和取消下所有 reader 关闭
- 分别测 Eino output-stream-start 与 HTTP SSE first-flush9.8 评审清单
- Callback 是否只观测,还是暗中改变授权和重试
- 全局注册是否覆盖别人的 handler
- metrics 是否出现高基数或敏感标签
- trace、run 与 attempt 是否可关联
- 是否把 model stream 首块误当端到端首包
- callback 是否修改输入或消费唯一 stream
- 错误路径是否也闭合 span、事件和 reader
9.9 闭卷检索题
模型在 300ms 产出首块,但用户 3s 才看到 SSE。只看 Eino 模型指标能定位问题吗?还需要哪些时间点?
9.10 一手资料
第 10 章:运行时 Skill 与 ToolSearch
10.1 两种 Skill 再分一次
| Skill | 谁使用 | 何时运行 | 目的 |
|---|---|---|---|
| Code Agent Eino Skill | 写代码的 Agent | 开发期 | 查 Eino 用法、生成或审查代码 |
| ADK Skill Middleware | 你的业务 Agent | 用户 run 期间 | 按需加载任务说明或派生子 Agent |
开发期 Skill 不会随业务服务部署。运行时 Skill 也不应教 Code Agent 怎么改 Go 代码。
10.2 Skill Middleware 的模型
每个运行时 Skill 是一个目录中的 SKILL.md,frontmatter 包含 name、description、context、agent、model 等字段。Middleware 先列出元数据,Agent 决定加载哪个 Skill,再按模式处理正文:
- inline:把 Skill 内容作为工具结果回到当前 Agent
- fork:启动一个不带父历史的子 Agent
- fork_with_context:子 Agent 带父历史
skillHandler, err := skill.NewMiddleware(ctx, &skill.Config{
Backend: skillBackend,
AgentHub: agentHub,
ModelGateway: modelGateway,
})
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Model: chatModel,
Handlers: []adk.ChatModelAgentMiddleware{skillHandler},
})Skill Backend 决定从哪里列出和读取 Skill。filesystem backend 只扫描 base dir 的第一层子目录。生产中还要做版本、签名、tenant 可见性和内容安全校验。
10.3 Skill 不是权限
Skill 文本可以说“只读数据库”,但真正的工具集合与 Broker policy 必须落实只读。Prompt 约束只能影响模型选择,不能阻止恶意或错误的 Tool Call。
加载 Skill 还会改变 prompt,因此需要记录:skill name、内容版本/hash、加载模式、实际 Agent/model。不要记录可能含敏感信息的全文。
10.4 ToolSearch 解决工具规模问题
一次把数百个 Tool Schema 塞进模型会增加 token、降低选择质量,也可能超过 Provider 限制。ToolSearch/Deferred Tools 允许先暴露搜索工具和延迟加载的 ToolInfo,模型找到候选后再调用。
但 ToolSearch 只解决“模型看到什么”。执行系统仍必须用当前 run 的批准工具闭集校验,防止模型搜索到租户无权使用的工具。
工具检索应按名称、描述、domain、权限标签和版本做确定性过滤。检索结果也要受数量上限控制。
10.5 业务应用可以怎样用
- Skill 表达某类分析方法、证据要求和输出合同
- ToolSearch 从大规模 业务操作目录 中找少量候选
- Broker 根据 run contract、tenant 和数据域做最终授权
- Evidence 记录 Skill 与 Tool 定义版本,支持回放解释
Skill 内容和 Tool Schema 都应该版本化。仅记录名称不足以重放,因为同名内容可能已经更新。
10.6 给 Code Agent 的任务合同
实现运行时 Skill catalog spike:
- 只读取批准目录第一层的 SKILL.md
- frontmatter 做闭合 Schema 校验,name 唯一
- List 只返回安全元数据,Get 返回内容并计算版本 hash
- inline/fork/fork_with_context 分别做隔离测试
- ToolSearch 先按 tenant policy 过滤,再返回最多 N 个候选
- 搜索结果不能扩大 Broker 的 executable tool set
- run artifact 记录 skill/tool definition hash,不记录敏感全文
- 文档中明确区分开发期 Eino Skills 与运行时 Skills10.7 评审清单
- Skill 来源是否可信、版本是否可追溯
- fork 是否意外继承父历史或敏感 context
- Skill 是否被误当权限控制
- ToolSearch 是否先做 tenant/policy 过滤
- 返回候选数和 Schema token 是否有上限
- 运行 artifact 能否说明实际加载了哪版 Skill
10.8 闭卷检索题
一个 Skill 写着“允许调用 write_report”。为什么 Agent 仍不能直接得到该写工具?
10.9 一手资料
第 11 章:Interrupt、Resume、Checkpoint 与人工确认
11.1 中断不是失败
Interrupt 表示 Agent 需要外部输入才能继续,例如用户批准写操作、补充参数、选择候选数据源。Runner 保存恢复所需状态并产出结构化动作。应用把它映射为 waiting_for_input 或 waiting_for_approval,而不是 failed。
sequenceDiagram
participant U as User
participant A as Application service
participant R as Eino Runner
participant S as Checkpoint store
R->>S: save checkpoint(checkpoint_id)
R-->>A: interrupted action + safe info
A-->>U: approval requested
U->>A: decision + idempotency key
A->>R: ResumeWithParams(checkpoint_id, targets)
R->>S: load checkpoint
R-->>A: continued events11.2 两种恢复
Runner.Resume(checkpointID) 是隐式恢复全部,适合单一确认点。ResumeWithParams 可以按 interrupt address 提供数据,适合多个或嵌套中断点。
iter, err := runner.ResumeWithParams(ctx, checkpointID, &adk.ResumeParams{
Targets: map[string]any{
interruptAddress: ApprovalDecision{
Approved: true,
ActorID: actorID,
},
},
})创建首次运行时,应给每个业务 run 分配不可猜测、租户隔离的 checkpoint ID。不要直接使用用户可控字符串。
11.3 Checkpoint 的正确地位
Checkpoint 包含框架恢复状态,可能有消息、local state 和嵌套 Agent 数据。它必须有:
- tenant/run ownership 校验
- TTL 与删除策略
- 静态加密和访问审计
- Schema/版本兼容策略
- 最大体积与序列化失败处理
它不能替代业务审批表。审批事实要单独持久化,包含 actor、decision、scope、request hash 和时间。恢复时先读取并验证业务事实,再把受控 resume data 交给 Eino。
11.4 Resume 最危险的是重复副作用
恢复可能从 Tool 边界重入。所有写操作都应带业务幂等键,例如 run_id + tool_call_id + operation_version。下游要能回答:未执行、已成功、已失败、提交状态未知。
如果上次提交状态未知,不能靠 resume 再执行一次。先查询下游状态,仍未知就进入人工处理或失败终态。
11.5 取消和中断不同
中断期待未来 resume;取消表示调用方不再需要当前执行。不要为取消自动生成一个可恢复审批。产品可以允许“重新开始”,那是新 run,不是旧 run 的 resume。
11.6 给 Code Agent 的任务合同
实现 HITL approval 流:
- 首次 run 在写工具前产出结构化 interrupt
- checkpoint_id 由服务生成并绑定 tenant/run
- approval 独立存储 actor/scope/request_hash/decision/version
- resume 前验证审批未过期且与原请求 hash 相同
- ResumeWithParams 只传最小 decision,不传 HTTP DTO
- 写工具用 run_id+tool_call_id+operation_version 做幂等键
- 测试拒绝、过期、重复 resume、跨租户、篡改 request 和提交状态未知
- checkpoint 设置 TTL,终态后按策略删除11.7 评审清单
- 中断是否有结构化原因与最小安全展示信息
- checkpoint ID 是否可枚举或跨租户读取
- 审批事实是否只存在 checkpoint
- resume data 是否未经验证直接进入工具
- 写操作是否能安全处理重复 resume
- run 终态是否清理或过期 checkpoint
11.8 闭卷检索题
为什么批准结果必须单独存业务表,不能只依赖 Eino checkpoint?
11.9 一手资料
第 12 章:TurnLoop、排队、抢占、取消与停止
12.1 为什么单次 Runner 不够
聊天产品可能在一个 Agent turn 尚未完成时收到新消息。你需要决定:排到后面、合并进当前计划、抢占当前 turn,还是拒绝。TurnLoop[T, M] 管理 item buffer,并为每个 turn 准备 Agent 和输入。
核心回调:
GenInput:从 buffered items 中决定本轮消费哪些、保留哪些PrepareAgent:按本轮 items 创建或取得 AgentOnAgentEvents:消费本轮事件GenResume:存在 checkpoint 时决定如何恢复
loop := adk.NewTurnLoop(adk.TurnLoopConfig[InboundItem, *schema.Message]{
GenInput: genInput,
GenResume: genResume,
PrepareAgent: prepareAgent,
OnAgentEvents: consumeEvents,
Store: checkpointStore,
CheckpointID: checkpointID,
})
loop.Run(ctx)
ok, ack := loop.Push(item, adk.WithPreempt[InboundItem, *schema.Message](adk.AnySafePoint))
loop.Stop(adk.WithGraceful())
exit := loop.Wait()这是结构示例;option 的精确泛型推导应由锁定版本编译验证。
12.2 四种产品行为
Queue:当前 turn 完成后处理新消息。最可预测。
Merge:GenInput 把多条 item 合成一个 turn。要保留 item IDs 和顺序。
Preempt:新消息入队,同时请求取消当前目标 turn。新 item 不会凭空进入当前模型上下文,而是在下一 turn 处理。
Stop:关闭 loop。无 option 时通常等当前 turn 结束;immediate/graceful/timeout 影响取消方式。若 Agent 不支持相应取消 option,行为可能退化为当前 turn 结束后退出。
12.3 Push 成功不等于抢占已生效
Push 返回 bool 表示是否进入 buffer。带 preempt 时还返回 ack channel,表示抢占请求已被解析,不代表旧 turn 已完成清理。产品状态必须区分:item accepted、preempt requested、old turn terminal、new turn started。
12.4 TurnLoop 不是全局任务队列
它适合单 session/单 Agent 的内存运行循环。服务级分布式排队仍需要应用层 queue、shard ownership、lease 和故障恢复。进程重启时,只有配置 Store 与 CheckpointID 并正确实现 item 序列化,才能恢复 TurnLoop bookkeeping。
12.5 取消的闭合原则
一旦发出 tool_requested,终态前就要闭合为 succeeded/failed/cancelled。取消不能直接跳到 run_cancelled,留下半截 Tool lifecycle。清理应使用有界 detached context,避免父 context 已取消后事件无法落盘;这个 context 只能做收尾,不能开始新业务副作用。
12.6 给 Code Agent 的任务合同
实现单 session TurnLoop spike:
- item 含 message_id/session_id/received_at/payload
- 默认 queue;只有显式 urgent 类型允许 preempt
- 记录 accepted/preempt_requested/old_terminal/new_started
- Stop 区分 drain、graceful、immediate,并写状态转换测试
- Store 开启时证明 item 可序列化并能进程重建恢复
- 已 requested 的工具在 run terminal 前全部闭合
- cleanup 共用有界预算,不在 detached context 启动新工具
- 并发 Push、Stop-before-Run、late items 和重复 Wait 都有测试12.7 评审清单
- 新消息的 queue/merge/preempt/reject 语义是否由产品冻结
- 是否把 Push accepted 当成旧 turn 已取消
- TurnLoop 是否被误当分布式队列
- item 和 checkpoint 是否支持版本化序列化
- 取消后是否留下未闭合 Tool Call
- Stop 退化行为是否被测试和暴露
12.8 闭卷检索题
urgent 消息 Push 返回成功且 ack 已关闭,此时能否立刻把旧 run 标记 cancelled?为什么?
12.9 一手资料
第 13 章:A2UI、SSE 与产品事件边界
13.1 三类输出不要混
| 输出 | 面向谁 | 语义 |
|---|---|---|
| Model stream | Agent/Compose | token 或 content block 增量 |
| AgentEvent | Runtime 消费者 | 模型、工具、动作、错误等内部运行事件 |
| Product event | Web/App 客户端 | 版本化、可重连、可授权的产品协议 |
A2UI 是 Agent 输出 UI 描述的一种方式。SSE 是传输协议。两者都属于产品 adapter,不是 Eino 自动替你冻结的业务合同。
13.2 事件适配层
flowchart LR
AE[AgentEvent] --> N[Normalizer]
N --> L[Run lifecycle reducer]
L --> P[Persisted product event]
P --> S[SSE publisher]
P --> R[Replay API]
N --> U[A2UI validator]
U --> PNormalizer 读取 Eino 事件,但输出业务应用自有类型。Reducer 验证状态转换。先持久化,再发布;只有这样客户端重连才能从 last_event_id 补发。
13.3 推荐事件信封
{
"schema_version": 1,
"event_id": 42,
"run_id": "run_...",
"session_id": "session_...",
"type": "tool_started",
"occurred_at": "2026-08-24T12:00:00Z",
"payload": {
"tool_call_id": "call_...",
"tool_name": "execute_operation"
}
}event_id 应在 run 内单调递增。writer 只有确认事件提交后才能推进 sequence。若返回错误但提交状态不明,不能随便重写一个新 event_id;artifact 应标记不可验证。
13.4 A2UI 必须验证
模型输出的 UI tree 仍是不可信输入。需要:
- 组件 allowlist 和 Schema version
- 文本、URL、动作参数大小限制
- 禁止任意脚本、HTML 和未知协议
- action 必须映射到后端批准的 command,不由客户端直接执行 Tool
- 旧客户端遇到未知组件有降级表示
A2UI 失败不应破坏文本回答。可以发出结构化 rendering failure,再退化为文本或固定卡片。
13.5 Eino streaming 不等于 SSE
直接把每个 token 写 SSE 会让内部 Provider 行为泄漏到产品协议,也难以做重连。建议聚合为稳定 delta 或语义事件,并限制频率。SSE 断开时要取消订阅还是取消 run,是产品决定;后台长任务可能继续运行。
13.6 给 Code Agent 的任务合同
实现 AgentEvent -> ProductEvent adapter:
- 对外类型不能暴露 Eino struct
- event_id 在 run 内单调,先持久化后 publish
- lifecycle reducer 拒绝非法状态转换和未闭合工具
- SSE 支持 Last-Event-ID 重放和慢消费者处理
- 客户端断开是否取消 run 由显式 policy 决定
- A2UI 只允许版本化组件闭集,禁止 script/raw HTML
- A2UI 校验失败降级为安全文本事件
- 测试断连重连、重复 publish、writer 提交未知和未知组件13.7 评审清单
- 外部 API 是否直接序列化 AgentEvent
- event_id 是否在持久化成功前推进
- SSE 断开是否无条件取消后台 run
- 是否有 replay、慢消费者和背压策略
- A2UI action 是否绕过后端授权
- 未知组件和 Schema 版本是否能安全降级
13.8 闭卷检索题
为什么客户端收到 tool_started 后断线,重连不能只靠重新订阅 Eino stream?
13.9 一手资料
第 14 章:AgentTool、DeepAgent 与多 Agent 选择
14.1 首选工具式委派
v0.9 源码对 Agent transfer 和旧 Workflow/Supervisor Agent 给出明确的“不推荐”提示。多数场景应选:
- 单 ChatModelAgent + 普通 Tool
- 单 ChatModelAgent +
adk.NewAgentTool包装的专业 Agent - DeepAgent 的 task/subagent 模式
工具式委派的优势是边界清楚:父 Agent 发出一个 Tool Call,子 Agent 返回一个结果。子 Agent 的 exit/transfer/break 等动作不会随意终止父 Agent;interrupt 可以跨边界传播。
14.2 AgentTool 骨架
specialist, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "metric_specialist",
Description: "Explains one metric using approved evidence tools",
Model: specialistModel,
ToolsConfig: specialistTools,
})
specialistTool := adk.NewAgentTool(ctx, specialist)
parent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "governed_orchestrator",
Model: parentModel,
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{specialistTool},
},
},
})子 Agent 必须有非空、稳定的 Name 和 Description。默认 Tool 输入是 {"request":"..."};可以自定义输入 Schema,也可以选择传完整聊天历史。完整历史会增加隐私和 token 风险,不应默认开启。
14.3 三种方案对照
| 问题 | 普通 Tool | AgentTool | DeepAgent |
|---|---|---|---|
| 内部步骤 | 代码确定 | 子模型自主 | 主/子模型长期自主 |
| 上下文 | 结构化参数 | request 或受控历史 | 文件、任务、压缩上下文 |
| 成本 | 最低 | 额外模型调用 | 通常最高 |
| 适合 | API、计算、固定流程 | 专业判断 | 长任务与工作区任务 |
先问能否用普通 Tool/GraphTool完成。只有子任务真的需要独立模型推理时再用 AgentTool。只有长任务能力确实能提高成功率时再用 DeepAgent。
14.4 多 Agent 的隐藏成本
- 每个 Agent 有自己的 prompt 和 Tool Schema token
- 父子上下文投影可能丢失或泄露信息
- 事件与 usage 要归因到嵌套调用
- 取消、checkpoint 和审批要跨边界传播
- 测试空间随路由和失败组合扩大
多 Agent 架构必须用 eval 证明完成率或成本收益,不能把组织结构照搬成 Agent 结构。团队里有“数据组”和“报告组”,不代表 Runtime 里必须有两个 Agent。
14.5 业务应用的建议
初版保持一个主 Agent。业务操作目录、Workspace Executor、Evidence Validator 都先作为 Tool 或 GraphTool。若评测显示某类任务需要独立长上下文,再把该能力包装为 AgentTool。DeepAgent 可作为后续实验轨,不作为所有 run 的默认入口。
14.6 给 Code Agent 的任务合同
对比三种实现同一任务:GraphTool、AgentTool、DeepAgent
- 使用同一 Fake evidence source 和同一输出合同
- 记录模型 turns、tool calls、token、duration、成功/失败原因
- AgentTool 默认只传结构化 request,不传完整历史
- 验证子 Agent interrupt 可传播,exit 不终止父 Agent
- 取消后父子事件全部闭合
- 给出选择结论;没有 eval 数据不得推荐多 Agent 为默认
- 禁止使用 Agent transfer 或旧 Supervisor/Workflow Agent 方案14.7 评审清单
- 子任务是否真的需要模型推理
- Name/Description 是否稳定且能准确路由
- 是否默认传递了完整父历史
- 父子 usage、事件和 artifact 是否可归因
- interrupt/cancel 是否跨边界正确传播
- 是否用 eval 证明多 Agent 的收益
14.8 闭卷检索题
一个“验证 evidence hash”的步骤该做普通 Tool、GraphTool 还是 AgentTool?说明理由。
14.9 一手资料
第 15 章:生产安全、预算、幂等、测试与评测
15.1 生产 Agent 是受监督的状态机
Prompt 不能替代状态机。一个 run 至少应有闭集终态:completed、failed、cancelled、timed_out,以及非终态 waiting_for_input。每个 model/tool lifecycle 也要从 started 闭合到唯一终态。
状态转换由代码验证。模型文本不能决定 run 是成功还是等待澄清。
15.2 权限模型
每次 Tool Call 的有效权限是这些集合的交集:
service deployment allowlist
∩ tenant entitlements
∩ user authorization
∩ run contract tools
∩ current approval scope
∩ runtime safety policy任一信息缺失都应拒绝。权限计算结果应不可变地绑定到 run snapshot,避免运行中配置变化使前后两次 Tool Call 使用不同边界。
15.3 预算分开计
建议冻结以下硬限制:
- max model turns
- max total tool calls 与每工具上限
- max input/output/total tokens 或 cost
- run wall timeout
- per-model-call 和 per-tool deadlines
- max streamed bytes、artifact bytes、workspace bytes
- max parallel tools、active runs 和 queue depth
每个 retry 都消耗预算。清理预算不能随工具数不断重置,否则取消后可能无限收尾。
15.4 幂等和提交状态
所有副作用使用幂等键。事件 writer、Tool Broker、artifact finalizer 都要区分:
- 确认未提交:可以用同一 identity 精确重试
- 确认已提交:返回已有结果
- 提交状态未知:停止自动行为,标记不可验证或进入 reconciliation
“出现 error”并不等于“没有提交”。这是分布式系统中最该坚持的规则。
15.5 安全日志与 artifact
默认不持久化:完整 prompt、reasoning、Tool 参数正文、Provider body、环境变量、授权 header、工作区未筛选文件。
建议持久化:版本化 RunRequest、安全事件流、命令元数据、模型/工具计数、usage、evidence hashes、final output、validation result。需要原始材料时建立单独的安全等级、保留期和访问审计。
15.6 测试金字塔
- 纯函数:Schema、状态转换、预算、错误映射。
- Fake Model/Tool:真实 Eino loop,离线且确定性。
- filesystem E2E:生成完整 artifact 并重新读取验证。
- 本地 HTTP wire test:验证 Provider serializer 和 transport。
- dev canary:只用批准凭据和只读任务。
- paired evaluation:在冻结合同下比较 Runtime/Prompt 方案。
不要让普通单测依赖真实模型网关。也不要用 Fake readiness 冒充真实环境 ready。
15.7 Eval 测什么
至少分开评估:
- 任务完成与输出合同
- Tool 选择、参数和调用顺序
- evidence 完整性与 claim 支持
- 拒绝、澄清和安全行为
- 成本、延迟、turn/tool 次数
- 取消、超时、Provider 错误下的恢复
比较两个 Runtime 时要冻结输入、模型、temperature、工具 Schema、retry 和预算。否则分数差异不能归因到 Runtime。
15.8 版本升级流程
升级 Eino 或 Provider 扩展时:阅读 release diff;编译;跑兼容测试;跑 Fake Agent E2E;跑 wire test;检查 checkpoint 兼容;做 dev canary;再跑冻结 eval。出现行为差异时更新合同或回退,不要只改测试期望。
15.9 给 Code Agent 的任务合同
为 Agent Runtime 建生产合同测试套件:
- 定义 run/model/tool 的合法状态转换
- 所有硬预算使用 checked arithmetic,禁止溢出
- 写操作按幂等键与提交状态处理
- 敏感字段做 allowlist 持久化,不靠事后正则删除
- Fake Model 驱动真实 Eino loop;测试取消、超时、批量工具和流关闭
- filesystem E2E 生成 run/events/commands/final/validation 并重新验证
- Provider wire test 不访问公网
- eval fixture 冻结模型参数、Tool Schema、retry 与预算
- 输出 coverage gap,不用降低断言掩盖未实现能力15.10 评审清单
- 状态转换是否有唯一终态和闭合验证
- 权限是否取交集并绑定 run snapshot
- 预算是否覆盖 retry、流字节和 cleanup
- error 后是否盲目假设未提交
- artifact 是否含敏感或不可解释数据
- Fake 测试是否真的经过 Eino loop
- paired eval 是否冻结了公平条件
- 版本升级是否验证 wire 与 checkpoint
15.11 闭卷检索题
事件 writer 返回超时,无法判断事件是否落盘。为什么既不能用新 event_id 重发,也不能继续写 run terminal?
15.12 一手资料
附录 A:给 Code Agent 的总任务合同
以后每张任务都尽量写全这些字段:
mode: investigate | design | implementation | review
repo: 精确仓库根目录
branch: 当前或目标分支
scope: 允许阅读/修改的包和文件
allowed: 可做的操作
forbidden: 不能做的操作,特别是生产网络、凭据、其他仓库
baseline:
- Go version
- Eino/eino-ext/internal ext exact versions
- selected message track
contracts:
- input/output/error/event schemas
- ownership and lifecycle
- retry/idempotency/cancel semantics
requirements:
- observable behaviors, not vague implementation wishes
verification:
- compile/unit/race/offline E2E/wire/eval commands
output:
- changed files
- decisions and evidence
- test results
- residual risks and explicitly unimplemented scope差的任务:“用 Eino 写个 Agent,支持工具和记忆。”
好的任务:“用锁定 v0.9.15 的内置 ChatModelAgent 实现 Fake Model 离线 ReAct;只注册两个 Tool wrapper;整批 preflight 后串行;应用合同不 import Eino;覆盖取消后 Tool lifecycle 闭合;不接真实 Provider。”
附录 B:架构评审速查表
框架选择
- 简单模型调用:Model Component
- 线性管道:Chain
- 条件、回边、checkpoint:Graph
- 字段依赖 DAG:Workflow
- 自主 Tool 循环:ChatModelAgent
- 确定性流程由 Agent 选择:GraphTool
- 开放专业子任务:AgentTool
- 长任务、文件工作记忆:经评测后的 DeepAgent
状态
- Eino Message 不是数据库消息实体
- Run-local、Session、Business facts 分开
- 当前 Tool Call/Result 必须配对
- Checkpoint 只用于恢复
- 历史上下文由 projector 构建
工具与安全
- 模型 Schema 和执行 Schema 可以分层,执行层不能放宽
- 权限由 Broker 计算交集
- 批量 Tool Calls 先整批 preflight
- 写操作有幂等键和提交状态查询
- Prompt 和 Skill 不是权限控制
- Shell/Filesystem 必须隔离
运行与事件
- 所有 lifecycle 闭合到唯一终态
- Model stream、AgentEvent、ProductEvent 分开
- event 先提交再递增 sequence 和发布
- SSE 支持重放;断连策略显式
- cancel 后只做有界收尾,不启动新副作用
观测与测试
- trace/run/call ID 可关联
- metrics 只用低基数标签
- 不保存 prompt、reasoning、参数正文和凭据
- Fake Model 经过真实 Eino loop
- stream 正常、错误、取消都关闭
- Provider 升级跑 wire test
- 架构变化用冻结 eval 证明收益
附录 C:术语表
| 术语 | 本课程中的固定含义 |
|---|---|
| Agent | 接收消息并产生异步 AgentEvent 的运行单元 |
| AgenticMessage | v0.9 的 ContentBlock 型消息 |
| AgentTool | 把一个 Agent 包成父 Agent 可调用的 Tool |
| Broker | Tool 执行前后的授权、验证、幂等与审计边界 |
| Callback | Component 级观测扩展点 |
| Checkpoint | 中断或取消后恢复框架执行所需的序列化状态 |
| Component | Model、Tool、Retriever 等原子接口 |
| Compose | 把 Component 和函数组成 typed Runnable 的编排层 |
| Context projector | 从领域历史构造本次模型上下文的应用模块 |
| DeepAgent | 带长任务、文件和子任务能力的预构建 Agent |
| Evidence | 支持结论的可校验来源及其 hash/引用 |
| GraphTool | 把确定性 Graph 适配成 Agent Tool 的模式 |
| Handler | 改变 ChatModelAgent 模型/工具调用行为的接口扩展点 |
| ProductEvent | 业务应用对客户端提供的版本化、可持久化事件 |
| Runner | 启动、恢复 Agent 并输出事件的标准入口 |
| RuntimeAdapter | 隔离领域合同与具体 Agent Runtime 的应用接口 |
| Session | 跨 run 的业务会话及其状态 |
| Skill Middleware | 运行时按需加载 SKILL.md 的 ADK handler |
| Supervisor | 拥有 run admission、writers、终态、验证与资源关闭的应用模块 |
| ToolSearch | 延迟发现 ToolInfo,减少一次发送给模型的工具数量 |
| TurnLoop | 管理一个连续交互中的 item buffer、turn 和抢占 |
课程结束后的学习顺序
第一轮只做闭卷回答:第 0、2、3、4、11、13 章。第二轮让 Code Agent 完成第 2 章的 Fake ReAct,并按源码、合同、测试的顺序阅读一个真实 Eino 项目。第三轮为自己的 Agent Engine 写运行、事件、工具和恢复合同,再开始服务实现。
有任何一章读起来像“知道每个词,但说不出边界”,就把该章的检索题单独发给我。后续教学应围绕你的答案纠偏,而不是再堆一遍资料。