16|从流程到 Agent:理解 ChatModelAgent 与 ReAct - eino教程

16|从流程到 Agent:理解 ChatModelAgent 与 ReAct

《Eino 实战:用 Go 构建 AI 应用与智能体》· 第 16 篇
关键词:ADK、ChatModelAgent、ReAct、Runner、AgentEvent、工具反馈
前置条件:已完成 11|Tool 与 ToolsNode 与 07|Graph 实战。

第 11 篇提到过一个麻烦:要让模型“先查工具、看结果、再决定下一步”,用 Graph 手写这个循环很啰嗦——需要模型节点、工具节点、分支回退,还得处理边界情况。

这类“模型反复决策”的场景,正是 ADK(Agent Development Kit)要解决的。它把 ReAct 循环封装起来,你只需要描述用什么模型、给什么工具、说什么规则。


一、Agent 与 Graph 的区别

先回到第一篇的区分,现在可以讲得更具体了:

GraphAgent
谁决定下一步开发者(分支函数、边)模型
路径编译期可枚举运行时不可预测
交付物最终结果全过程事件
适合固定流程开放式任务

一个系统有聊天框,不代表它是 Agent。判据是:下一步行动由谁决定。如果是你在代码里写死的,那就是 Graph。


二、ChatModelAgent 的四个要素

官方文档给出的定义很精炼:

ChatModelAgent = 以 ChatModel 作为决策器、以 Tools 作为行动空间、以工具反馈和历史记录作为下一轮决策上下文的 ReAct Agent。

拆开看四个要素,缺一不可:

要素作用缺失的后果
ChatModel(决策器)每轮判断下一步做什么无法推理
Tools(行动空间)定义能采取的行动只能空谈,无法执行
反馈(工具结果)把执行结果送回上下文无法根据真实结果修正
历史(累积上下文)保留推理轨迹与工具结果每轮都从零开始

关键特征是累积上下文驱动的渐进式决策:每一轮模型看到的不是孤立问题,而是从开始到现在的完整求解过程。

注意:ChatModelAgent 不是一次模型调用,而是一次可持续推进的求解过程。这句话决定了它的成本模型——一次Run可能触发多次模型调用。


三、最小可用配置

agent,err:=adk.NewChatModelAgent(ctx,&adk.ChatModelAgentConfig{Name:"ticket-assistant",Description:"查询工单状态的助手",Instruction:"你是工单助手,需要时调用工具查询。",Model:chatModel,ToolsConfig:adk.ToolsConfig{ToolsNodeConfig:compose.ToolsNodeConfig{Tools:[]tool.BaseTool{ticketTool},},},})

字段含义:

字段说明
NameAgent 名称。作为子 Agent 被调用时必填
Description能力描述,帮助其他 Agent 判断是否该转交任务
Instruction系统提示词。支持用会话变量填充占位符
Model决策模型。使用工具时,模型必须支持model.WithTools
ToolsConfig工具配置,复用compose.ToolsNodeConfig

MaxIterations默认20。超过上限时 Agent 会以 error 结束,而不是无限循环下去——这是保护机制,也是排查“为什么突然报错”时该先看的地方。


四、Runner:执行入口

构建好 Agent 后,通过 Runner 运行:

runner:=adk.NewRunner(ctx,adk.RunnerConfig{Agent:agent,EnableStreaming:false,})iter:=runner.Query(ctx,"工单 T-1001 现在是什么状态?")for{event,ok:=iter.Next()if!ok{break}// 处理 event}

只有经过 Runner,才能获得事件流、检查点与中断恢复等运行期能力。直接调用 Agent 的Run拿不到这些(更准确地说,Run也返回AsyncIterator,但EnableStreaming、CheckPointStore等运行配置由 Runner 统一管理)。

Query是Run的便捷方法:把单个字符串包装成用户消息。Run接受完整的消息列表。


五、完整示例:看清 ReAct 循环

下面这段程序用一个模拟模型跑通完整的“模型 → 工具 → 模型”循环,不访问网络:

packagemainimport("context""fmt""log""github.com/cloudwego/eino/adk""github.com/cloudwego/eino/components/model""github.com/cloudwego/eino/components/tool""github.com/cloudwego/eino/components/tool/utils""github.com/cloudwego/eino/compose""github.com/cloudwego/eino/schema")typeTicketReqstruct{TicketIDstring`json:"ticket_id" jsonschema:"description=工单编号"`}// mockModel 模拟两轮决策:先请求调用工具,看到工具结果后给出最终回答。typemockModelstruct{}func(m*mockModel)Generate(_context.Context,in[]*schema.Message,_...model.Option)(*schema.Message,error){for_,msg:=rangein{ifmsg.Role==schema.Tool{returnschema.AssistantMessage("工单 T-1001 已分派给网络支持组。",nil),nil}}returnschema.AssistantMessage("",[]schema.ToolCall{{ID:"call_1",Type:"function",Function:schema.FunctionCall{Name:"get_ticket",Arguments:`{"ticket_id":"T-1001"}`,},}}),nil}func(m*mockModel)Stream(ctx context.Context,in[]*schema.Message,opts...model.Option)(*schema.StreamReader[*schema.Message],error){msg,err:=m.Generate(ctx,in,opts...)iferr!=nil{returnnil,err}sr,sw:=schema.Pipe[*schema.Message](1)gofunc(){defersw.Close()sw.Send(msg,nil)}()returnsr,nil}func(m*mockModel)WithTools([]*schema.ToolInfo)(model.ToolCallingChatModel,error){returnm,nil}funcmain(){iferr:=run();err!=nil{log.Fatal(err)}}funcrun()error{ctx:=context.Background()ticketTool,err:=utils.InferTool[*TicketReq,string]("get_ticket","查询工单状态",func(_context.Context,req*TicketReq)(string,error){return"工单 "+req.TicketID+" 状态:已分派,处理人:网络支持组",nil})iferr!=nil{returnerr}agent,err:=adk.NewChatModelAgent(ctx,&adk.ChatModelAgentConfig{Name:"ticket-assistant",Description:"查询工单状态的助手",Instruction:"你是工单助手,需要时调用工具查询。",Model:&mockModel{},ToolsConfig:adk.ToolsConfig{ToolsNodeConfig:compose.ToolsNodeConfig{Tools:[]tool.BaseTool{ticketTool},},},})iferr!=nil{returnfmt.Errorf("创建 Agent 失败: %w",err)}runner:=adk.NewRunner(ctx,adk.RunnerConfig{Agent:agent,EnableStreaming:false})iter:=runner.Query(ctx,"工单 T-1001 现在是什么状态?")n:=0for{event,ok:=iter.Next()if!ok{break}n++ifevent.Err!=nil{fmt.Printf("event %d: ERR=%v\n",n,event.Err)continue}ifmv:=event.Output.MessageOutput;mv!=nil&&mv.Message!=nil{// mv.Role 表示这条消息来自模型还是工具;mv.ToolName 在工具结果时非空。fmt.Printf("event %d: role=%-9s tool=%-10s content=%q tool_calls=%d\n",n,mv.Role,mv.ToolName,mv.Message.Content,len(mv.Message.ToolCalls))}ifevent.Action!=nil&&event.Action.Exit{fmt.Printf("event %d: 动作=退出\n",n)}}fmt.Println("事件总数:",n)returnnil}

实机输出:

event 1: role=assistant tool= content="" tool_calls=1 event 2: role=tool tool=get_ticket content="工单 T-1001 状态:已分派,处理人:网络支持组" tool_calls=0 event 3: role=assistant tool= content="工单 T-1001 已分派给网络支持组。" tool_calls=0 事件总数: 3

这三行就是 ReAct 循环的完整形状:

  1. 事件 1:模型输出为空、tool_calls=1——它没有直接回答,而是请求调用get_ticket。
  2. 事件 2:角色是tool,tool_name是get_ticket——工具执行结果回到流程。
  3. 事件 3:模型基于工具结果给出最终回答,没有新的工具调用,循环结束。

对照第 11 篇手写 Graph 的版本可以看出,这套循环现在由框架管理:模型决策、工具执行、结果回填、再次决策,都不需要你写分支逻辑。你要做的只是定义工具和规则。


六、AgentEvent 里有什么

每次iter.Next()拿到一个AgentEvent:

字段含义
AgentName产生事件的 Agent 名称
Output.MessageOutput消息输出(模型输出或工具结果)
Action动作,如退出、中断、转交
Err错误
RunPath从根 Agent 到当前事件的执行路径

MessageOutput上有几个字段很关键:

字段用法
Message非流式时的完整消息
MessageStream流式时的读取器
Role这条消息来自模型(assistant)还是工具(tool)
ToolName工具结果时非空,流式下这是识别来源的唯一方式

务必处理event.Err。事件流可能在中途携带错误,忽略它会让失败看起来像“正常结束但没输出”。


七、关于事件流的两个常见误解

误解一:事件流等于模型的完整思维链。

不是。事件反映的是执行步骤——模型输出、工具调用、工具结果、动作。模型内部如何得出这个结果(内部的推理 token),事件流并不展示。支持深度思考的模型会把推理内容放在消息的ReasoningContent字段,那是另一回事。

误解二:事件流越长说明 Agent 越聪明。

恰恰相反。事件多意味着多轮往返,直接对应更多的模型调用、更长的耗时和更高的成本。事件数量是成本指标,不是能力指标。


八、什么时候该用 Agent

场景建议
步骤固定、可提前枚举Graph / Chain
步骤依赖运行时结果,无法提前写死ChatModelAgent
需要多轮试错、根据反馈修正ChatModelAgent
单次生成、无需外部调用直接用 ChatModel

判断标准很简单:如果你能用一段流程图把步骤画完,就不需要 Agent。只有当“下一步做什么”真的取决于运行结果时,自主决策才带来价值。

反过来说,用了 Agent 并不意味着放弃控制:

  • 关键业务操作仍应通过工具内部鉴权(第 11 篇)。
  • 不可逆操作可以要求人工确认(第 20 篇)。
  • 迭代次数有上限(MaxIterations),不会无限跑下去。

九、常见问题

Agent 不调用工具,直接回答
检查Instruction是否说明了工具的使用时机,以及模型是否支持WithTools。工具描述含糊时(第 11 篇),模型难以判断何时使用。

报错说迭代次数超限
默认上限 20。可能是模型陷入循环,也可能是工具持续返回让模型“还想再查”的结果。先用事件流看清发生了什么,再决定是放宽上限还是修正工具。

event.Output为空指针
不同事件类型携带的内容不同。访问前先判空,特别是Output、MessageOutput和Message三层。

流式下拿不到内容
流式事件的内容在MessageStream里,需要Recv读取。另外要先SetAutomaticClose或自行Close,避免泄漏。

同样的问题,每次工具调用次数不同
这是自主决策的正常表现。用事件流观察典型路径,并关注成本上限。

想让 Agent 记住上一次对话
Agent 自身不维护跨请求记忆。需要把历史消息传给Run,或使用会话机制(第 18 篇)。


十、小结

  • Agent 与 Graph 的分界在于下一步由谁决定:模型还是开发者。
  • ChatModelAgent四要素:决策器、行动空间、反馈、累积上下文,缺一不可。
  • 它是一次可持续推进的求解过程,一次运行可能触发多次模型调用。
  • Runner是执行入口,只有经过它才能获得事件流与运行期能力。
  • 事件流展示的是执行步骤,不等于模型的完整思维链。
  • MaxIterations默认 20,是防失控的保护,不是可忽略的配置。

下一篇《Runner 与事件流:看见 Agent 的执行过程》,我们系统地消费事件,为交互界面和诊断提供依据。


参考资料与版本说明

资料核对日期为2026 年 10 月 9 日:

  1. 五分钟上手 Eino ADK
  2. Eino ADK 概述
  3. Agent 还是 Graph?AI 应用路线辨析
  4. Eino 主仓库

本文示例按Eino v0.9.21编写,使用模拟模型(不访问网络),已实机运行,正文事件流为真实输出。

相关新闻

15|RAG 优化:为什么检索到了,回答仍然不对? - eino教程

15|RAG 优化:为什么检索到了,回答仍然不对? - eino教程

15|RAG 优化:为什么检索到了,回答仍然不对? 《Eino 实战:用 Go 构建 AI 应用与智能体》 第 15 篇 关键词:召回质量、MultiQuery、检索路由、重排序、评测 前置条件:已完成 13|知识入…

2026/10/20 11:57:55 阅读更多 →
ReportMachine7.0报表引擎实战:从模板设计到打印导出

ReportMachine7.0报表引擎实战:从模板设计到打印导出

简介:ReportMachine 7.0是一套面向Delphi 7开发环境的专业报表控件资源,适合需要为桌面业务系统添加报表设计、打印与导出能力的开发人员。资源包内含完整的控件源码与设计文件,共811个文件,压缩包大小约9.22MB。其中pas源码与dcu…

2026/10/20 11:57:55 阅读更多 →
BGP选路与属性详解:从权重到MED的完整指南

BGP选路与属性详解:从权重到MED的完整指南

BGP 的选路和属性,这个东西我琢磨了挺久才算是彻底捋顺。一开始看各路文档,什么权重、本地优先级、MED、AS Path,名词堆了一堆,感觉每个都认识,但真到了排障或者调优的时候,根本不知道从哪儿下手。后来在真…

2026/10/20 11:56:54 阅读更多 →

最新新闻

基于Win32 API与GDI+的捕鱼达人游戏开发实战:C++课程设计完整方案

基于Win32 API与GDI+的捕鱼达人游戏开发实战:C++课程设计完整方案

简介:这是一份面向高校C课程设计与Windows游戏开发初学者的实践项目源码,主题为经典小游戏“捕鱼达人”。项目以C语言为基础,结合Windows API完成窗口创建、消息循环与事件响应,并借助GDI绘制游戏背景、鱼群、炮台与子弹等元素&am…

2026/10/20 13:34:41 阅读更多 →
AI辅助教材编写全流程:从大纲到降重的实战指南

AI辅助教材编写全流程:从大纲到降重的实战指南

开头220字(我需要认真思考标题和场景,构思一篇结构清晰、完整、专业的博文。先理解用户需求。)开头(220字)写教材在很多人眼里是“专家才配干”的活儿——体例要规范、内容要严谨、查重要过关、还要有可教学性。但这两…

2026/10/20 13:34:41 阅读更多 →
内容创作的“rea”工作流:复盘、重建与行动,打造可持续内容体系

内容创作的“rea”工作流:复盘、重建与行动,打造可持续内容体系

“rea”这个标题,看起来很简短,但如果把它当成一个内容项目或者品牌栏目的起点,它其实是很多想法的容器。我自己的理解是,它更像一套“重新运转”的方法论——react,重新观察、重新拆解、重新行动。这篇博文不写宏大理…

2026/10/20 13:34:41 阅读更多 →
C++ Win32捕鱼达人课设:从零搭建与避坑指南

C++ Win32捕鱼达人课设:从零搭建与避坑指南

简介:这是一份面向高校C课程设计与Windows游戏开发初学者的完整实践项目,主题为“捕鱼达人”,适合正在学习面向对象编程、希望把语法知识落地为可运行图形程序的学生参考。项目围绕Windows API展开,涵盖窗口创建、消息循环与事件响…

2026/10/20 13:34:41 阅读更多 →
跨平台实时协作框架的架构设计与离线同步实践

跨平台实时协作框架的架构设计与离线同步实践

1. rea 这个代号,到底代表什么如果你在一个技术团队里待得够久,就会发现每个项目在正式命名之前,都会有一个特别随意的内部代号:可能是某个人随手敲的文件夹名,可能是会议室名字的缩写,也可能是三个字母的随…

2026/10/20 13:34:40 阅读更多 →
Qt命令行参数解析:从argc/argv到QCommandLineParser实战指南

Qt命令行参数解析:从argc/argv到QCommandLineParser实战指南

写这篇文章的缘由很简单:之前接手一个老项目的维护工作时,发现主程序里塞了满满一屏的手写参数解析代码,各种strcmp加if/else嵌套,看着头大不说,新加一个开关还得顺着调用链一路摸下去,搞得人战战兢兢。后来…

2026/10/20 13:33:32 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →