Eino ADK 中的 ChatModelAgent 详解与实战
深入解析 Eino ADK 框架中的 ChatModelAgent,阐述其基于 ReAct 循环的核心执行模式,对比 Tool、Transfer 与 AgentAsTool 的使用场景,介绍 Middleware/Handler 的工程化扩展能力,并通过 Go 语言实战演示故障分诊助手的搭建流程。

深入解析 Eino ADK 框架中的 ChatModelAgent,阐述其基于 ReAct 循环的核心执行模式,对比 Tool、Transfer 与 AgentAsTool 的使用场景,介绍 Middleware/Handler 的工程化扩展能力,并通过 Go 语言实战演示故障分诊助手的搭建流程。

ChatModelAgent 是一个以 LLM 为决策核心、默认采用 ReAct 式循环来推进任务的 Agent。
很多人第一次看 ChatModelAgent,可能会下意识的认为:
不就是
ChatModel + Instruction + Tools吗?
这句话不算错,但不全。
ChatModelAgent 在 ADK 里,承担的是'默认思考型 Agent'的角色。它不是单纯帮你调一次模型,而是把模型决策、工具调用、协作跳转、事件输出和扩展钩子,统一规范到一个可运行的 Agent 骨架里。
本篇不在重复 Runner / Console 多轮 这种入门动作,而是从以下 6 个更关键的问题入手:
ChatModelAgent 在 ADK 里到底是什么ReAct 循环,而不是一次模型调用ReturnDirectly / Exit / MaxIterations / OutputKey 这些字段到底解决什么问题Tool、Transfer、AgentAsTool 到底怎么选Middleware / Handler 为什么才是工程化分水岭ChatModelAgent 想简单很多人一上来就把注意力放在这几个字段上:
InstructionModelTools然后得出一个很自然的结论:
这就是一个'会调模型、也会调工具'的配置对象。
但他的重点其实是另一层:
ChatModelAgent是 ADK 里最核心、最常用的预构建 Agent 之一,它把'思考 + 决策 + 调工具 + 协作 + 输出事件'规范成了一个统一实现。
也就是说,它不是 ChatModel 的语法糖。
它解决的是:当一个 Agent 需要靠 LLM 自己判断下一步该答、该调工具、该转给别人、还是该退出时,系统应该怎么组织这段运行过程。
这也是为什么你会发现:
ReAct 循环TransferHandlerAgentEvent如果只是'模型外面包一层',根本没必要长出这一整套能力。
ChatModelAgent 在 ADK 里到底是什么官方定义很直接:
ChatModelAgent是 Eino ADK 中的一个核心预构建 Agent,它封装了与大语言模型交互、并支持使用工具来完成任务的复杂逻辑。
这句话里最重要的词,不是'模型',而是'复杂逻辑'。
你可以把 ADK 里的几类 Agent 先粗分一下:
| 类型 | 主要职责 | 决策方式 |
|---|---|---|
ChatModelAgent | 负责思考、推理、工具调用、动态决策 | 由 LLM 决定 |
Workflow Agents | 负责顺序、循环、并行等固定流程 | 由预设流程决定 |
Supervisor / Plan-Execute | 负责多 Agent 协作范式封装 | 仍以内置 ChatModelAgent 为核心 |
Custom Agent | 负责高度定制的执行协议 | 由你自己实现 |
所以 ChatModelAgent 的位置,其实非常像默认的'脑子'。
当你的 Agent 需要:
那它通常就会成为第一个候选。
把这套关系放到运行时视角里,看起来会更清楚:

这张图里最该记住的是两点:
ChatModelAgent 不等于'模型输出一段话'ReAct 循环ChatModelAgent 的核心执行模式其实很清楚:它内部走的是 ReAct。
其内部是一个循环:
这套循环里,用以下四个词能直接对应上:
Reason:模型思考Action:模型决定调用什么Act:系统真的去执行动作Observation:把动作结果喂回去所以 ChatModelAgent 的关键,不在于'它能调工具'。
而在于:
它允许模型把一次复杂任务拆成多轮判断,而不是一口气把答案硬生成出来。
这也是它和我们直接手写一段 ChatModel.Generate(...) 的根本区别。
可以这么说:
如果没有配置工具,
ChatModelAgent会退化为一次普通的 ChatModel 调用。
这意味着:
ChatModelAgent 都一定会循环ReAct 运行形态MaxIterationsReAct 的好处是灵活,风险是兜不住时会一直绕。
所以 MaxIterations 本质上是一个保险丝。
默认值是 20。超过这个次数还没结束,Agent 会直接报错退出。
这在真实业务里非常有必要。否则你很容易遇到两种问题:
很多线上'为什么 Agent 一直在调用工具'的问题,本质上都不是框架 bug,而是没有把循环上限和结束策略设计清楚。
Name / Description这两个字段经常被初学者轻视。
但实际上它们比你想象的重要。
Name 是 Agent 的身份标识Description 决定别的 Agent 会不会把任务转给它尤其在 Transfer 场景里,Description 不是装饰品,而是模型判断'谁更适合接手这件事'的依据。
Instruction / Model这两个字段是最直观的:
Instruction:Agent 的系统约束Model:底层使用哪个 ChatModel但有一点别搞混:
Instruction 决定行为风格,Model 决定能力底座。
ToolsConfig这组配置是 ChatModelAgent 和普通模型调用真正拉开差距的地方。
其中有两个很关键的扩展字段起到了作用:
ReturnDirectlyEmitInternalEventsReturnDirectly这个字段的意思是:
某些工具一旦被调用成功,就不要再把结果送回模型二次润色了,直接把结果带着返回。
这个能力特别适合两类场景:
比如这篇后面 demo 里的 handoff_to_human,就很适合 ReturnDirectly。
EmitInternalEvents这个配置只在 AgentAsTool 场景里有意义。
默认情况下,当你把一个 Agent 包成 Tool 后,外层只会拿到最终的 ToolResult,看不到内层 Agent 的事件流。
而 EmitInternalEvents=true 时,内层 Agent 产生的事件会继续往外透出,调用方就能实时看到里面到底在干什么。
这个能力特别适合:
OutputKey这个字段很实用:
把 Agent 最后一条输出消息,以某个 key 写进
SessionValues
如果你的后续 Agent、Workflow、或者外层业务逻辑还要继续消费这次结果,它比你手动到处传字符串干净得多。
Exit你可以把他当作一个特殊 Tool。
模型调用这个 Tool 并成功执行后,ChatModelAgent 会直接退出,效果和 ReturnDirectly 很像,但语义更明确:
ReturnDirectly 更像'某个工具调用后直接收口'Exit 更像'模型自己宣布:到这里结束,把这个最终结果拿出去'ModelRetryConfig这是一个典型的工程字段。
它解决的不是'让回答更聪明',而是'模型调用失败时,系统要不要重试,以及怎么重试'。
如果流式响应过程中发生错误,但策略允许重试,调用方读 stream 时会收到
WillRetryError。
所以在真实系统里做流式输出时,不能只管 happy path。否则一旦流中途断掉,你都不知道是彻底失败了,还是下一轮马上会补回来。
Tool、Transfer、AgentAsTool 到底怎么选这一段是最值得展开讲的地方。
很多人第一次看这三种能力时,会觉得它们都像'把事情交给别人做'。但它们不是一回事。

Tool适合那种边界特别清晰、输入输出很稳定的能力,比如:
它更像函数调用。
TransferTransfer 的意思不是'调用另一个能力',而是:
当前 Agent 判断,另一个 Agent 更适合接手这件事,于是把任务控制权转过去。
官方页对应的实现机制是:
ChatModelAgent 配置子 AgentTransfer ToolDescription 决定要不要跳转最小示意像这样:
// 创建一个上层 Agent,作为请求分发器使用。
// 它本身由聊天模型驱动,职责是根据用户问题决定该交给谁处理。
supervisor,_:= adk.NewChatModelAgent(ctx,&adk.ChatModelAgentConfig{
Name:"dispatcher",// Agent 名称:运行时用于标识当前 Agent
Description:"负责分发用户请求",// 描述:帮助上层协作逻辑理解它的职责
Model: cm,// 底层使用的聊天模型
})
// 创建一个子 Agent,专门处理数据库相关问题。
dbExpert,_:= adk.NewChatModelAgent(ctx,&adk.ChatModelAgentConfig{
Name:"db_expert",// 子 Agent 名称
Description:"擅长数据库故障排查",// 描述其擅长领域,便于被正确选择
Model: cm,// 同样使用聊天模型驱动
})
// 给 supervisor 挂载可协作的子 Agent。
// 这样 supervisor 在处理请求时,就可以把数据库类问题分发给 dbExpert。
dispatcher,_:= adk.SetSubAgents(ctx, supervisor,[]adk.Agent{dbExpert})
如果一个问题本来就该交给另一个 Agent 独立负责,那应该优先考虑 Transfer,而不是让当前 Agent 硬撑到底。
AgentAsTool它的语义又不同:
我不是把任务彻底交出去,我只是把另一个 Agent 当成一个'高级工具'来用。
什么时候适合这么做?
当被调用的 Agent:
我这里从官方源码 NewAgentTool(...) 截取片段举例:
reporterTool := adk.NewAgentTool(ctx, reporterAgent)
agent,_:= adk.NewChatModelAgent(ctx,&adk.ChatModelAgentConfig{
Name:"ops_assistant",
Description:"负责处理线上故障",
Model: cm,
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools:[]tool.BaseTool{reporterTool},
},
EmitInternalEvents:true,
},
})
一句话记忆这三者:
Tool:调用一个函数Transfer:把控制权交给另一个 AgentAgentAsTool:把另一个 Agent 当函数来调Middleware / Handler 才是工程化分水岭如果说 Tool 解决的是'Agent 能干什么',那 Handler 解决的就是'Agent 在真实系统里怎么管'。
官方页给出的扩展点一共有几层:
BeforeAgentBeforeModelRewriteStateAfterModelRewriteStateWrapModelWrapInvokableToolCall / WrapStreamableToolCall把它们放到一张执行图里,会比只看接口名字更容易懂:

BeforeAgent这是最适合做'运行前改配置'的地方。
它能改的不是消息历史,而是本次运行的:
InstructionToolsReturnDirectly所以它很适合做这些事:
ReturnDirectlyBeforeModelRewriteState / AfterModelRewriteState这两个钩子盯的是 Messages。
适合做:
如果你只是想管'发给模型的消息长什么样',优先看这组。
WrapModel这个钩子适合拦截模型调用本身。
典型用途是:
它的价值在于:你不用改业务代码,就能把'模型调用前后'的工程逻辑拦下来。
WrapInvokableToolCall / WrapStreamableToolCall这两个钩子盯的是工具层。
特别适合:
Handlers官方和本地源码都已经把这个方向说得很明确了:
AgentMiddleware 是 struct 风格,适合简单静态扩展ChatModelAgentMiddleware 是 interface 风格,更适合动态行为和上下文改写如果你是现在开始写新的 ChatModelAgent 扩展,优先用 Handlers 更稳。
ChatModelAgent 搭一个故障分诊助手目的:
做一个'故障分诊助手',能查 runbook、在高风险场景下直接升级给人工,并通过 handler 统一加上运行约束与工具日志。
本例子只演示三件事:
ChatModelAgent + ToolReturnDirectlyHandlergo get github.com/cloudwego/eino@latest
go get github.com/cloudwego/eino-ext/components/model/qwen@latest
环境变量至少准备两个:
$env:DASHSCOPE_API_KEY="你的百炼 API Key"
$env:QWEN_MODEL="qwen-plus"
这段代码的目标不是做一个真正的运维平台,而是把 ChatModelAgent 这一页最重要的几个点跑通。
package main
import(
"context"
"fmt"
"log"
"os"
"strings"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
"github.com/cloudwego/eino-ext/components/model/qwen")
// RunbookInput 是查询故障预案工具的输入。
type RunbookInput struct{ Service string`json:"service" jsonschema:"description=服务名,enum=user,enum=order,enum=payment,enum=search"` ErrorCode string`json:"error_code" jsonschema:"description=错误码,例如 DB_TIMEOUT、AUTH_EXPIRED、NO_STOCK"`}
// RunbookOutput 是故障预案工具的输出。
type RunbookOutput struct{ Level string`json:"level"` Suggestion string`json:"suggestion"` Owner string`json:"owner"`}
// HandoffInput 是转人工工具的输入。
type HandoffInput struct{ Reason string`json:"reason" jsonschema:"description=需要人工接手的原因"`}
// HandoffOutput 是转人工工具的输出。
type HandoffOutput struct{ Ticket string`json:"ticket"` Action string`json:"action"`}
// OpsGuardHandler 是一个自定义 middleware,
// 用来在 Agent 运行前补充约束,并在工具调用时统一打日志。
type OpsGuardHandler struct{*adk.BaseChatModelAgentMiddleware }
funcNewOpsGuardHandler()*OpsGuardHandler {
&OpsGuardHandler{
BaseChatModelAgentMiddleware:&adk.BaseChatModelAgentMiddleware{},
}}
BeforeAgent( ctx context.Context, runCtx *adk.ChatModelAgentContext,)(context.Context,*adk.ChatModelAgentContext,){
nRunCtx :=*runCtx
nRunCtx.Instruction +=
ctx,&nRunCtx,
}
WrapInvokableToolCall( ctx context.Context, endpoint adk.InvokableToolCallEndpoint, tCtx *adk.ToolContext,)(adk.InvokableToolCallEndpoint,){
returnfunc(ctx context.Context, argumentsInJSON , opts ...tool.Option)(,){
log.Printf(, tCtx.Name, argumentsInJSON)
returnendpoint(ctx, argumentsInJSON, opts...),
}}
funcnewRunbookTool() tool.BaseTool {
t, err := utils.InferTool(,,(*RunbookOutput,){
{
input.Service ==&& input.ErrorCode ==:
&RunbookOutput{
Level:,
Suggestion:,
Owner:,
},
input.Service ==&& input.ErrorCode ==:
&RunbookOutput{
Level:,
Suggestion:,
Owner:,
},
:&RunbookOutput{
Level:,
Suggestion:,
Owner:,
},
}})
err !={
log.Fatalf(, err)
}
t
}
funcnewHandoffTool() tool.BaseTool {
t, err := utils.InferTool(,,(*HandoffOutput,){
&HandoffOutput{
Ticket:,
Action:+ input.Reason,
},
})
err !={
log.Fatalf(, err)
}
t
}
funcnewModel(ctx context.Context)*qwen.ChatModel {
cm, err := qwen.NewChatModel(ctx,&qwen.ChatModelConfig{
BaseURL:,
APIKey:mustEnv(),
Model:envOrDefault(,),
})
err !={
log.Fatalf(, err)
}
cm
}
funcnewTriageAgent(ctx context.Context) adk.Agent {
agent, err := adk.NewChatModelAgent(ctx,&adk.ChatModelAgentConfig{
Name:,
Description:,
Instruction:,
Model:newModel(ctx),
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools:[]tool.BaseTool{newRunbookTool(),newHandoffTool(),},
},
ReturnDirectly:[]{:,},
},
MaxIterations:,
OutputKey:,
Handlers:[]adk.ChatModelAgentMiddleware{NewOpsGuardHandler(),},
})
err !={
log.Fatalf(, err)
}
agent
}
funcmain(){
ctx := context.Background()
query :=
iflen(os.Args)>{
query = strings.Join(os.Args[:],)
}
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent:newTriageAgent(ctx),
EnableStreaming:,
})
iter := runner.Query(ctx, query)
err :=printEvents(iter); err !={
log.Fatal(err)
}}
funcprintEvents(iter *adk.AsyncIterator[*adk.AgentEvent]){
{
event, ok := iter.Next()
!ok {returnnil}
event.Err !={ event.Err }
event.Output ==|| event.Output.MessageOutput =={}
mv := event.Output.MessageOutput
msg, err := mv.GetMessage()
err !={ err }
mv.Role {
schema.Tool: fmt.Printf(, mv.ToolName, msg.Content)
: fmt.Printf(, msg.Content)
}}
}
funcmustEnv(key ){
v := os.Getenv(key)
v =={
log.Fatalf(, key)
}
v
}
funcenvOrDefault(key, fallback ){ v := os.Getenv(key); v !={ v }
fallback
}
search_runbook 是普通 Tool,模型先查事实,再组织答案handoff_to_human 被配置成 ReturnDirectly,一旦调用就直接退出OpsGuardHandler 通过 BeforeAgent 和 WrapInvokableToolCall 把运行约束和工具日志插进来了如果你本地跑的时候传一个'高风险但信息不足'的问题,比如:
go run ."payment 服务持续报错,但我只有一句日志:DB_TIMEOUT,请直接给我下一步动作。"
常见表现会是两种:
search_runbook,再组织答案返回handoff_to_human,然后因为 ReturnDirectly 立即结束这正是 ChatModelAgent 和普通模型调用的差别:它不是只会说话,而是会决定下一步怎么干。
本篇最想帮你建立的,不是某个 API 记忆点,而是一个判断:
ChatModelAgent不是'模型调用升级版',而是 ADK 里默认的思考型 Agent 实现。
它真正解决的是:
ReAct 方式循环运行AgentEvent 形式输出Handler 把日志、审计、裁剪、动态工具这些工程能力插进去
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online