ChatModelAgent评测:Eino ADK体系下AI大模型落地解析
摘要
ChatModelAgent作为默认思考型Agent,内部基于ReAct循环实现模型决策、工具调用、协作跳转及事
本文核心内容来自官方文档对 Eino ADK 关键模块的剖析,涵盖 ChatModelAgent 的核心概念与 Agent 协作机制。
不少开发者初次接触 ChatModelAgent 时,容易低估它的复杂度。直觉上,他们或许认为:这无非是在模型外面包了一层壳。
这个说法并非完全错误,但远未触及本质。ChatModelAgent 在 ADK 中承担的是“默认思考型代理”角色。它做的远不止一次模型调用——它将模型决策、工具调用、协作跳转、事件输出和扩展钩子全部整合进一个可运行的 Agent 骨架内。
本文跳过基础的 Runner 或 Console 多轮操作,直接聚焦以下六个更关键的问题:
ChatModelAgent在 ADK 中的准确定位是什么?- 为什么它的内部是一个
ReAct循环,而非简单的模型调用? ReturnDirectly / Exit / MaxIterations / OutputKey这些字段各自解决哪些痛点?Tool、Transfer、AgentAsTool三者的选择策略是什么?- 为何
Middleware / Handler是工程水平的分水岭? - 如何搭建一个贴近后端生产环境的在线演示?
1. 为什么很多人会把 ChatModelAgent 想简单
许多开发者一开始只关注 Instruction、Model 和 Tools 这几个字段,从而得出一个看似合理的结论:这不过是“给模型加了个工具调用功能”。
但真正的重点在于:它内置了决策能力。
换句话说,它并非 ChatModel 的语法糖。它要解决的核心问题是:当 Agent 需要依赖 LLM 自行判断下一步是直接回答、调用工具、转交任务还是退出时,系统该如何组织并管控这段运行过程。
这也是为什么它具备以下特性:
- 内置
ReAct循环 - 支持
Transfer机制 - 可将另一个 Agent 当作 Tool 来调用
- 提供专门的
Handler处理工程逻辑 - 能将整个运行过程输出为
AgentEvent
如果它真的只是一个“模型外套”,根本不需要如此完整的能力体系。
2. ChatModelAgent 在 ADK 里到底是什么
官方定义清晰直接:ChatModelAgent 是一个由底层聊天模型驱动的 Agent,用于处理复杂逻辑。
这句话中,关键词并非“模型”,而是“复杂逻辑”。
我们先对 ADK 中的几类 Agent 做个简要分类:
| 类型 | 主要职责 | 决策方式 |
|---|---|---|
ChatModelAgent | 负责思考、推理、工具调用、动态决策 | 由 LLM 决定 |
Workflow Agents | 负责顺序、循环、并行等固定流程 | 由预设流程决定 |
Supervisor / Plan-Execute | 负责多 Agent 协作范式封装 | 仍以内置 ChatModelAgent 为核心 |
Custom Agent | 负责高度定制的执行协议 | 由你自己实现 |
因此 ChatModelAgent 在体系中的位置,非常像默认的“大脑”。
当你的 Agent 需要:
- 根据上下文自行判断下一步动作
- 在回答与工具调用之间灵活切换
- 在多个 Agent 之间转交任务
- 在运行过程中插入工程逻辑
那么它通常就是你的首选方案。
将这套关系放到运行时视角下,会更清晰:

这张图中最核心的两点是:
ChatModelAgent不等于“模型输出一段话”。- 它真正对外暴露的,是一整段可运行的决策过程。
3. 其内部本质是一个 ReAct 循环
ChatModelAgent 的核心执行模式非常朴素:它的内部走的是 ReAct。
其内部是一个循环:
- 调用模型,让模型先做判断。
- 如果模型直接给出答案,则结束。
- 如果模型发起 Tool Call,则执行工具。
- 把工具结果回灌给模型。
- 再让模型决定下一步。
- 直到模型不再需要工具,或者 Agent 被强制结束。
这套循环中的四个关键词直接对应:
Reason:模型思考Action:模型决定调用什么Act:系统真正执行动作Observation:把动作结果喂回去
所以 ChatModelAgent 的关键,不在于“它能调工具”,而在于它把“思考-行动-观察-再思考”这个闭环变成了一个天然的循环。
这也是它和我们直接手写一段 ChatModel.Generate(...) 的根本区别。
没有 Tool 时会怎样
坦率地说,如果没有 Tool,ChatModelAgent 就退化成了一次模型调用。
这意味着:
- 并非所有
ChatModelAgent都会进入循环。 - 只有当提供了工具、协作能力,或模型真的产生了 Tool Call,它才会进入完整的
ReAct运行形态。
为什么还需要 MaxIterations
ReAct 的好处是灵活,但风险在于不加控制容易陷入无限循环。因此 MaxIterations 本质上是一根保险丝。
默认值为 20。一旦超过这个次数仍未结束,Agent 会报错退出。这在真实业务场景中非常必要,否则你很可能会遇到两种常见问题:
- 模型在几个工具之间反复试探,迟迟无法做出决定。
- Prompt 写得模糊,模型不清楚是该直接回答还是继续调用工具。
很多线上“为什么 Agent 一直在调用工具”的疑问,本质上不是框架的 bug,而是因为循环上限和结束策略没有设计清楚。
4. 哪几组配置真正决定了行为
Name / Description
这两个字段常被初学者忽视,实际重要性远超预期。
Name是 Agent 的身份标识。Description决定了其他 Agent 是否会把任务转交给它。
尤其在 Transfer 场景中,Description 并非装饰品,而是模型判断“谁更适合接手这件事”的依据。
Instruction / Model
这两个字段最为直观:
Instruction:Agent 的系统约束。Model:底层使用的ChatModel。
注意:Instruction 决定行为风格,Model 决定能力底座。
ToolsConfig
这组配置是 ChatModelAgent 与普通模型调用真正拉开差距的地方。其中有两个关键的扩展字段:
ReturnDirectlyEmitInternalEvents
ReturnDirectly
该字段的含义:工具执行完后,直接将结果作为最终输出,不再让模型处理一遍。
这个能力特别适合两类场景:
- 工具结果本身就是最终答案。
- 工具结果本身就是“交接单”“审批单”或“跳转结果”,再返回给模型处理反而可能污染结果。
比如后面 Demo 中的 handoff_to_human,就很适合设置 ReturnDirectly。
EmitInternalEvents
这个配置仅在 AgentAsTool 场景中有效。默认情况下,当你把一个 Agent 封装成 Tool 后,外层只会拿到最终的 ToolResult,看不到内层 Agent 的事件流。而设置 EmitInternalEvents=true 后,内层 Agent 产生的事件会继续向外透出,调用方就能实时看到它的运行细节。
这个能力特别适合:
- 你把一个复杂 Agent 当 Tool 用。
- 同时又希望前端或调用方还能看到它的实时输出。
OutputKey
这是一个非常实用的字段:OutputKey 的作用是将本次运行的结果存到运行时上下文的某个固定 key 里。
如果后续的 Agent、Workflow 或外层业务逻辑还需要继续消费这次结果,用它比手动到处传字符串要干净得多。
Exit
你可以把它理解为一个特殊的 Tool。模型调用这个 Tool 并成功执行后,ChatModelAgent 会直接退出。效果与 ReturnDirectly 类似,但语义更明确:
ReturnDirectly更像“某个工具调用后直接收口”。Exit更像“模型自己宣布:到这里结束,把这个最终结果拿出去”。
ModelRetryConfig
这是一个典型的工程字段,它解决的不是“让回答更聪明”,而是“模型调用失败时,系统要不要重试以及如何重试”。需要特别注意:这个重试策略在流式输出和非流式输出场景下的表现不同。
所以在真实系统中做流式输出时,不能只考虑 happy path。一旦流中途断掉,你需要知道是彻底失败了,还是下一轮很快会补回来。
5. Tool、Transfer、AgentAsTool 到底怎么选
这部分最值得深入探讨。很多开发者第一次看到这三种能力时,会觉得它们都像是“把事情交给别人做”,但它们的差异其实很大。

普通 Tool
它适合那些边界清晰、输入输出都很稳定的能力。比如:
- 查错误码
- 查 runbook
- 计算时间
- 调用外部 HTTP 接口
它更像一个函数调用。
Transfer
Transfer 的意思不是“调用另一个能力”,而是“把当前的控制权转交给另一个 Agent”。
官方实现的机制是:
- 给
ChatModelAgent配置子 Agent。 - 框架自动生成一个
Transfer Tool。 - 模型根据各个 Agent 的
Description决定是否跳转。 - Runner 收到 Transfer Event 后,切换到目标 Agent 继续执行。
最小示意如下:
// 创建一个上层 Agent,作为请求分发器使用。 // 它本身由聊天模型驱动,职责是根据用户问题决定该交给谁处理。 supervisor, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ Name: "dispatcher", Description: "负责分发用户请求", Model: cm, }) // 创建一个子 Agent,专门处理数据库相关问题。 dbExpert, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ Name: "db_expert", Description: "擅长数据库故障排查", Model: cm, }) // 给 supervisor 挂载可协作的子 Agent。 // 这样 supervisor 在处理请求时,就可以把数据库类问题分发给 dbExpert。 dispatcher, _ := adk.SetSubAgents(ctx, supervisor, []adk.Agent{dbExpert})
如果一个问题的确应该交给另一个 Agent 独立处理,那么优先考虑 Transfer,而不是让当前 Agent 硬撑到底。
AgentAsTool
它的语义又不同:将一个 Agent 整体当作一个 Tool 来调用。调用方式与普通 Tool 相同。
什么时候适合这么做?当被调用的 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:把控制权交给另一个 Agent。AgentAsTool:把另一个 Agent 当函数来调。
6. Middleware / Handler 才是工程化分水岭
如果说 Tool 解决的是“Agent 能干什么”,那么 Handler 解决的就是“Agent 在真实系统中如何管理”。
官方文档给出的扩展点共分为以下几层:
BeforeAgentBeforeModelRewriteStateAfterModelRewriteStateWrapModelWrapInvokableToolCall / WrapStreamableToolCall
把它们放到一张执行图中,会比只看接口名称更容易理解:

BeforeAgent
这是最适合做“运行前改配置”的地方。它能修改的不是消息历史,而是本次运行的 Instruction、Tools 和 ReturnDirectly。所以它非常适合做以下事情:
- 动态追加系统约束。
- 按租户或环境动态添加工具。
- 把某个工具临时标记为
ReturnDirectly。
BeforeModelRewriteState / AfterModelRewriteState
这两个钩子关注的是 Messages。适合做:
- 历史消息裁剪。
- 敏感信息脱敏。
- 在模型调用前后检查消息状态。
如果你只是想管“发给模型的消息长什么样”,优先看这组钩子。
WrapModel
这个钩子适合拦截模型调用本身。典型用途包括:
- 统一日志。
- 指标采集。
- 审计。
- 对模型输入输出做包装。
它的价值在于:你无需修改业务代码,就能把“模型调用前后”的工程逻辑拦截下来。
WrapInvokableToolCall / WrapStreamableToolCall
这两个钩子关注的是工具层。特别适合:
- 打工具调用日志。
- 统计耗时。
- 做参数审计。
- 对工具结果进行二次包装。
为什么新代码更推荐 Handlers
官方和本地源码已经明确了方向:老的 AgentMiddleware 是 struct 风格,适合简单静态扩展;而新的 ChatModelAgentMiddleware 是 interface 风格,更适合动态行为和上下文改写。如果你现在开始写新的 ChatModelAgent 扩展,优先使用 Handlers 会更稳妥。
7. 实战:用 ChatModelAgent 搭一个故障分诊助手
这个例子的目的不是为了搭建一个真正的运维平台,而是为了展示如何将 ChatModelAgent 最关键的几个点跑通:
ChatModelAgent + Tool。ReturnDirectly。Handler。
先装依赖
go 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"
完整代码
这段代码的目标是演示一个故障分诊助手,它能够:
- 调用 runbook 工具查询预案。
- 调用 handoff 工具转人工。
- 通过 Handler 实现运行前约束和工具日志。
// 代码示例略,全文较长但已在原文中给出
这个 Demo 到底对应了什么
search_runbook是普通 Tool,模型先查事实,再组织答案。handoff_to_human被配置成ReturnDirectly,一旦调用就直接退出。OpsGuardHandler通过BeforeAgent和WrapInvokableToolCall把运行约束和工具日志插了进来。
如果你在本地运行,并传入一个“高风险但信息不足”的查询,比如:
go run . "payment 服务持续报错,但我只有一句日志:DB_TIMEOUT,请直接给我下一步动作。"
常见的表现会是两种:
- 模型先调用
search_runbook,再组织答案返回。 - 模型判断信息不足或风险过高,直接调用
handoff_to_human,然后因为设置了ReturnDirectly而立即结束。
这正是 ChatModelAgent 和普通模型调用之间的本质差别:它不仅仅会说话,它还会决定下一步该怎么干。
8. 总结
这篇文章最想帮你建立的,不是对某个 API 的记忆,而是一个认知:ChatModelAgent 是一条可运行的思考管线,而不是一次模型调用。
它真正解决的是:
- 让模型在回答、调用工具、转交任务之间做动态决策。
- 让这些动作按照
ReAct方式循环运行。 - 让运行过程以
AgentEvent的形式输出。 - 让你能通过
Handler把日志、审计、消息裁剪、动态工具这些工程能力无缝地插进去。
来源:互联网
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。