ADK 入门:不写图也能搭 Agent
ADK作为Eino框架中compose包之上的高级封装,通过配置即可构建智能体,无需手动构图。这种设计大幅简化了开发流程,让开发者能快速搭建功能强大的Agent。
读完这篇你会知道
一、ADK 是什么
在Eino底层,compose包负责构建计算图,开发者需手动添加节点、连接边、配置分支,再经历编译和运行。这种方式虽然灵活,但代码量较大。

ADK建立在compose包之上,是一种高层封装,其设计目标是通过配置即可创建智能体,无需关心底层图结构。该功能的包路径为github.com/eino/adk。ADK与compose的关系可以类比为Web框架与HTTP库:两者共享底层机制,上层则省去了重复的建图、编译和运行步骤。
二、ChatModelAgent:最常用的类型
ChatModelAgent是ADK中使用频率最高的类型。其核心逻辑是:配置一个模型,可选配工具,其余部分由ADK自动处理。
无工具模式
复制代码// 示例: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,仅调用一次模型,不会循环。
复制代码用户输入 → [system prompt + messages] → ChatModel → 输出
加工具:自动变成 ReAct 循环
在ToolsConfig中传入工具,ChatModelAgent会自动升级为ReAct Agent。其内部结构会从简单chain变为包含循环逻辑的复杂流程:
复制代码// 示例: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()},
},
},
})
内部结构会变成ReAct循环:
复制代码用户输入
↓
ChatModel → 要用工具?
├── 是 → ToolsNode(执行工具)→ 回 ChatModel
└── 否 → 输出结果
(最多循环 MaxIterations 次,默认 20)
两种模式的切换是自动进行的:有工具时采用ReAct,无工具时直通。开发者无需修改代码结构。
三、Runner:怎么跑起来
Agent本身只负责执行逻辑,而Runner是实际的执行入口,负责管理生命周期,包括启动、流式输出、断点持久化以及恢复等操作。
复制代码// 示例: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):
复制代码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
}
完整流程包括以下步骤:
- Agent调用工具
- 工具返回compose.NewInterruptAndRerunErr,例如"你想看什么类型的书?"
- Runner检测到中断,将状态存入CheckPointStore,结束事件流
- 用户看到问题并输入答案
- 调用runner.Resume()注入用户答案,从断点继续执行
恢复代码示例如下:
复制代码// 示例: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接口:
复制代码// 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):
复制代码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完全一致:
复制代码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即可。
七、完整代码:从零到可运行
将前面拆开的部分组装成最小完整示例:
复制代码package mainimport (
"context"
"fmt"
"log"
"os" "github.com/eino/adk"
"github.com/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提供从无工具链式到自动ReAct循环的灵活切换,配合统一的Runner执行入口和内置中断恢复机制,覆盖大多数智能体应用场景。日常开发中,ChatModelAgent可处理大部分需求,需要完全控制时则实现Agent接口。下篇将继续深入探讨。
代码来源:eino/eino-examples