系列「企业级 AI Agent 实现拆解」E18 篇。上一篇 E17 拆解了 deer-go 深度研究团队的完整实现------八节点图、共享状态、AnyPredecessor 循环。那条路的门槛不低:要手动建图、注册节点、配分支、编译运行。这篇介绍 Eino 的 ADK(Agent Development Kit)------一套专门为"不想手搓图"的场景准备的更高层封装。
读完这篇你会知道
- ADK 是什么:它和 compose/graph 的关系
- ChatModelAgent:最常用的 Agent,两种运行模式
- Runner:如何启动、流式输出、断点续跑
- Custom Agent:自定义 Agent 的最小实现
- Interrupt/Resume:一个工具怎么暂停整个 Agent 等人回复
- 三种类型怎么选
一、ADK 是什么
Eino 底层用 compose 包搭图------你手动加节点、连边、配分支、编译、运行。强大,但代码量不小。
ADK 是建在 compose 之上的高层封装 ,目标是:只需配置就能建 Agent,不用关心图的结构。包路径是 github.com/cloudwego/eino/adk。
ADK 和 compose 的关系,类似 Web 框架和 HTTP 库的关系:底层是同一套机制,上层帮你省掉重复的"建图→编译→运行"三步。
二、ChatModelAgent:最常用的类型
ChatModelAgent 是 ADK 里使用最频繁的类型。核心逻辑:配置一个模型,可选配工具,剩下的 ADK 自动处理。
无工具模式
go
// eino-examples/adk/helloworld/helloworld.go
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "hello_agent",
Description: "A friendly greeting assistant",
Instruction: "You are a friendly assistant. Please respond in a warm tone.",
Model: model,
})
无工具时,ChatModelAgent 内部是一条简单 chain:
css
用户输入 → [system prompt + messages] → ChatModel → 输出
只调用一次模型,不循环。
加工具:自动变成 ReAct 循环
给 ToolsConfig 传工具,ChatModelAgent 自动升级为 ReAct Agent:
go
// eino-examples/adk/intro/chatmodel/subagents/agent.go
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "BookRecommender",
Description: "An agent that can recommend books",
Instruction: `You are an expert book recommender. Use "search_book" to find books.`,
Model: model.NewChatModel(),
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{NewBookRecommender(), NewAskForClarificationTool()},
},
},
})
内部结构变成:
markdown
用户输入
↓
ChatModel → 要用工具?
├── 是 → ToolsNode(执行工具)→ 回 ChatModel
└── 否 → 输出结果
(最多循环 MaxIterations 次,默认 20)
两种模式的切换是自动的:有工具就 ReAct,没工具就直通。你不需要改代码结构。
三、Runner:怎么跑起来
Agent 本身只负责执行逻辑。Runner 是执行入口,负责生命周期管理:启动、流式输出、断点持久化、恢复。
go
// eino-examples/adk/intro/chatmodel/chatmodel.go
runner := adk.NewRunner(ctx, adk.RunnerConfig{
EnableStreaming: true,
Agent: a,
CheckPointStore: store.NewInMemoryStore(),
})
iter := runner.Query(ctx, "recommend a book to me", adk.WithCheckPointID("1"))
for {
event, ok := iter.Next()
if !ok { break }
if event.Err != nil { log.Fatal(event.Err) }
prints.Event(event)
}
每次 iter.Next() 返回一个 AgentEvent:
event.Output.MessageOutput--- 模型输出或工具结果event.Action--- 特殊动作(中断、退出)event.Err--- 错误
runner.Query() 接收字符串,自动包装成 UserMessage;runner.Run() 接收 []Message,适合多轮对话场景。
四、Interrupt/Resume:工具让 Agent 暂停等人
ADK 的中断机制允许一个工具在执行中途暂停整个 Agent,等待用户输入后继续。
书籍推荐 Agent 里的 ask_for_clarification 工具演示了这个机制(源码 subagents/ask_for_clarification.go):
go
func NewAskForClarificationTool() tool.InvokableTool {
t, _ := utils.InferOptionableTool(
"ask_for_clarification",
"Call this when the user's request is ambiguous.",
func(ctx context.Context, input *AskForClarificationInput, opts ...tool.Option) (string, error) {
o := tool.GetImplSpecificOptions[askForClarificationOptions](nil, opts...)
if o.NewInput == nil {
// 没有用户回复 → 触发中断,把问题文本作为 payload
return "", compose.NewInterruptAndRerunErr(input.Question)
}
// 有用户回复 → 继续执行,返回用户答案
return *o.NewInput, nil
})
return t
}
完整流程:
vbscript
① Agent 调用工具
② 工具返回 compose.NewInterruptAndRerunErr("你想看什么类型的书?")
③ Runner 检测到中断 → 状态存入 CheckPointStore → 事件流结束
④ 用户看到问题,输入答案
⑤ 调用 runner.Resume(),注入用户答案,从断点继续
恢复代码:
go
// eino-examples/adk/intro/chatmodel/chatmodel.go:61
iter, err := runner.Resume(ctx, "1",
adk.WithToolOptions([]tool.Option{subagents.WithNewInput(nInput)}))
中断恢复靠 CheckPointID (上面的 "1"):它绑定一次对话会话,Query 时创建,Resume 时读取。CheckPointStore 负责在两次调用之间持久化状态------示例里用内存 Map,生产环境换成 Redis 即可。
五、Custom Agent:从头实现 Agent 接口
当 ChatModelAgent 不够用------需要混合调用多个模型、自定义流控、注入外部状态------直接实现 Agent 接口:
go
// adk/interface.go
type Agent interface {
Name(ctx context.Context) string
Description(ctx context.Context) string
Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent]
}
最小实现(源码 adk/intro/custom/myagent.go):
go
type MyAgent struct{}
func (m *MyAgent) Name(_ context.Context) string { return "MyAgent" }
func (m *MyAgent) Description(_ context.Context) string { return "My custom agent" }
func (m *MyAgent) Run(ctx context.Context, input *adk.AgentInput, opts ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] {
iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
go func() {
defer func() {
if e := recover(); e != nil {
gen.Send(&adk.AgentEvent{Err: fmt.Errorf("panic: %v", e)})
}
gen.Close() // 必须 Close,否则 iter.Next() 永远阻塞
}()
// 你的任意逻辑:调模型、查 DB、调 API......
gen.Send(&adk.AgentEvent{
Output: &adk.AgentOutput{
MessageOutput: &adk.MessageVariant{
Message: &schema.Message{Role: schema.Assistant, Content: "hello world"},
Role: schema.Assistant,
},
},
})
}()
return iter
}
实现后传给 Runner,用法与 ChatModelAgent 完全一致:
go
runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: &MyAgent{}})
ADK 不关心 Agent 内部实现 ,只要遵守接口契约:通过 AsyncIterator 异步推送 AgentEvent,最后 gen.Close()。
六、三种类型怎么选
| ChatModelAgent(无工具) | ChatModelAgent(有工具) | Custom Agent | |
|---|---|---|---|
| 内部结构 | 简单 chain | 自动 ReAct 循环 | 完全自定义 |
| 代码量 | 最少,纯配置 | 少,多加一个 ToolsConfig | 多,实现接口 |
| 中断/恢复 | 内置支持 | 内置支持 | 需自行集成 |
| 适用场景 | 问答、摘要、翻译 | 需要工具的任务 | 混合多模型、自定义流控 |
还有一种 TypedChatModelAgent[*schema.AgenticMessage],专给支持原生工具调用的模型(如 Claude Computer Use 系列)------模型内部处理工具,Eino 只做单次调用,不跑外部 ReAct 循环。一般场景用默认 ChatModelAgent 即可。
七、完整代码:从零到可运行
把前面拆开的部分组装成最小完整示例:
go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino-ext/components/model/openai"
)
func main() {
ctx := context.Background()
// 1. 初始化模型
m, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{
APIKey: os.Getenv("OPENAI_API_KEY"),
Model: os.Getenv("OPENAI_MODEL"),
BaseURL: os.Getenv("OPENAI_BASE_URL"),
})
// 2. 创建 Agent(无工具 = 简单 chain)
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "demo_agent",
Description: "A demo agent",
Instruction: "You are a helpful assistant.",
Model: m,
})
// 3. 创建 Runner
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent: agent,
EnableStreaming: true,
})
// 4. 运行并消费事件
iter := runner.Query(ctx, "Hello, who are you?")
for {
event, ok := iter.Next()
if !ok {
break
}
if event.Err != nil {
log.Fatal(event.Err)
}
if msg, err := event.Output.MessageOutput.GetMessage(); err == nil {
fmt.Println(msg.Content)
}
}
}
五步:初始化模型 → 创建 Agent → 创建 Runner → 调用 Query → 消费事件流。不需要手工建图。
小结
ADK 是 Eino compose 层之上的高层封装。ChatModelAgent 处理日常大多数场景:无工具时是 chain,有工具时自动变 ReAct,中断恢复开箱即用。需要完全控制时实现 Agent 接口。Runner 统一管理执行入口,流式、断点、恢复都通过它来做。
下篇继续。