AI大模型落地系列|Eino ADK体系篇:ChatModelAgent你了解吗?

作者:技术拾光

时间:2026-07-29

浏览:285

ChatModelAgent作为默认思考型Agent,内部基于ReAct循环实现模型决策、工具调用、协作跳转及事件输出。其配置字段如ReturnDirectly、Exit、MaxIterations等控制行为。Tool、Transfer与AgentAsTool分别适用于函数调用、控制权限移交和复杂工具封装。Middleware与Handler提供工程化扩展点

文章目录

本文内容主要参考了官方文档中关于 Eino ADK 的几个核心部分:ChatModelAgent 概述、Agent 协作机制等。

很多人第一次接触 ChatModelAgent 时,容易把它想简单了。他们可能会下意识地认为:这不就是给模型套了个壳吗?

这个说法不算错,但远不够全面。ChatModelAgent 在 ADK 里扮演的是“默认思考型 Agent”的角色。它不仅仅是帮你调用一次模型,而是把模型决策、工具调用、协作跳转、事件输出和扩展钩子,全部规范到一个可运行的 Agent 骨架里。

这篇文章不会重复那些入门级的 Runner 或 Console 多轮操作,而是从以下六个更关键的问题入手:

  1. ChatModelAgent 在 ADK 里到底是什么?
  2. 它的内部为什么是一个 ReAct 循环,而不是一次简单的模型调用?
  3. ReturnDirectly / Exit / MaxIterations / OutputKey 这些字段到底是为了解决什么问题?
  4. Tool、Transfer、AgentAsTool 这三者到底该怎么选?
  5. 为什么说 Middleware / Handler 才是区分工程水平的分水岭?
  6. 如何搭建一个更贴近后端真实场景的 Demo?

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 之间转交任务
  • 在运行过程中插入工程逻辑

那它通常会是你的首选方案。

把这套关系放到运行时视角里,看起来会更清楚:

AI大模型落地系列|Eino ADK体系篇:ChatModelAgent你了解吗?

这张图里最需要记住两点:

  1. ChatModelAgent 不等于“模型输出一段话”。
  2. 它真正对外暴露的,是一整段可运行的决策过程。

3. 其内部本质是一个 ReAct 循环

ChatModelAgent 的核心执行模式其实很朴素:它内部走的是 ReAct。

其内部是一个循环:

  1. 调用模型,让模型先做判断。
  2. 如果模型直接给出答案,那就结束。
  3. 如果模型发起 Tool Call,就执行工具。
  4. 把工具结果回灌给模型。
  5. 再让模型决定下一步。
  6. 直到模型不再需要工具,或者 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 和普通模型调用真正拉开差距的地方。其中有两个很关键的扩展字段:

  • ReturnDirectly
  • EmitInternalEvents

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 到底怎么选

这段最值得展开讲讲。很多人第一次看到这三种能力时,会觉得它们都像是“把事情交给别人做”,但它们之间的区别其实很大。

AI大模型落地系列|Eino ADK体系篇:ChatModelAgent你了解吗?

普通 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 在真实系统里怎么管”。

官方文档给出的扩展点一共有几层:

  • BeforeAgent
  • BeforeModelRewriteState
  • AfterModelRewriteState
  • WrapModel
  • WrapInvokableToolCall / WrapStreamableToolCall

把它们放到一张执行图里,会比只看接口名字更容易理解:

AI大模型落地系列|Eino ADK体系篇:ChatModelAgent你了解吗?

BeforeAgent

这是最适合做“运行前改配置”的地方。它能改的不是消息历史,而是本次运行的 Instruction、Tools 和 ReturnDirectly。所以它很适合做这些事情:

  • 动态追加系统约束。
  • 按租户或环境动态添加工具。
  • 把某个工具临时标记为 ReturnDirectly。

BeforeModelRewriteState / AfterModelRewriteState

这两个钩子关注的是 Messages。适合做:

  • 历史消息裁剪。
  • 敏感信息脱敏。
  • 在模型调用前后检查消息状态。

如果你只是想管“发给模型的消息长什么样”,优先看这组钩子。

WrapModel

这个钩子适合拦截模型调用本身。典型用途包括:

  • 统一日志。
  • 指标采集。
  • 审计。
  • 对模型输入输出做包装。

它的价值在于:你不必修改业务代码,就能把“模型调用前后”的工程逻辑拦截下来。

WrapInvokableToolCall / WrapStreamableToolCall

这两个钩子关注的是工具层。特别适合:

  • 打工具调用日志。
  • 统计耗时。
  • 做参数审计。
  • 对工具结果进行二次包装。

为什么新代码更推荐 Handlers

官方和本地源码都已经把方向说得很明确了:老的 AgentMiddleware 是 struct 风格,适合简单静态扩展;而新的 ChatModelAgentMiddleware 是 interface 风格,更适合动态行为和上下文改写。如果你是现在开始写新的 ChatModelAgent 扩展,优先使用 Handlers 会更稳妥。

7. 实战:用 ChatModelAgent 搭一个故障分诊助手

这个例子的目的不是为了做一个真正的运维平台,而是想展示如何把 ChatModelAgent 这一页最重要的几个点跑通:

  1. ChatModelAgent + Tool。
  2. ReturnDirectly。
  3. 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"

完整代码

这段代码的目标是演示一个故障分诊助手,它能够:

  1. 调用 runbook 工具查询预案。
  2. 调用 handoff 工具转人工。
  3. 通过 Handler 实现运行前约束和工具日志。

// 代码示例略,全文较长但已在原文中给出

这个 Demo 到底对应了什么

  1. search_runbook 是普通 Tool,模型先查事实,再组织答案。
  2. handoff_to_human 被配置成 ReturnDirectly,一旦调用就直接退出。
  3. OpsGuardHandler 通过 BeforeAgent 和 WrapInvokableToolCall 把运行约束和工具日志插了进来。

如果你在本地运行,并传入一个“高风险但信息不足”的查询,比如:

go run . "payment 服务持续报错,但我只有一句日志:DB_TIMEOUT,请直接给我下一步动作。"

常见的表现会是两种:

  • 模型先调用 search_runbook,再组织答案返回。
  • 模型判断信息不足或风险过高,直接调用 handoff_to_human,然后因为设置了 ReturnDirectly 而立即结束。

这正是 ChatModelAgent 和普通模型调用之间的本质差别:它不仅仅会说话,它还会决定下一步该怎么干。

8. 总结

这篇文章最想帮你建立的,不是对某个 API 的记忆,而是一个判断:ChatModelAgent 是一条可运行的思考管线,而不是一次模型调用。

它真正解决的是:

  • 让模型在回答、调用工具、转交任务之间做动态决策。
  • 让这些动作按照 ReAct 方式循环运行。
  • 让运行过程以 AgentEvent 的形式输出。
  • 让你能通过 Handler 把日志、审计、消息裁剪、动态工具这些工程能力无缝地插进去。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›