news 2026/10/20 11:57:44

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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编写,使用模拟模型(不访问网络),已实机运行,正文事件流为真实输出。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 11:55:24

堆排序原地建堆原理与实战避坑指南

1. 堆不是“堆”出来的,是“建”出来的:先破除三个常见误解很多人第一次看到“建堆”这个词,下意识觉得就是把一堆数随便摞起来——就像把书本胡乱堆在桌上,高矮不一、重心不稳,风一吹就塌。但计算机里的“堆”&#x…

作者头像 李华
网站建设 2026/10/20 11:55:17

剪枝、量化、知识蒸馏:模型压缩三板斧实战指南

A同学上周跟我吐槽,他训练好的图像分类模型塞进手机直接内存爆掉,一张图要跑八百多毫秒,App根本带不动。我告诉他,要解决这种问题,绕不开三个词:剪枝、量化、知识蒸馏。注意,剪枝是“剪”不是“…

作者头像 李华
网站建设 2026/10/20 11:53:31

AI顶会闭门交流会全记录:多模态、端侧大模型与具身智能的人才风向

BA学术顶会上的技术交流,这两年早就不是单纯的论文宣讲和茶歇寒暄了。尤其像人工智能方向,一个规模靠前的学术会议,几乎相当于各家大厂把研发底牌、团队气质、招聘倾向一次性摆到台面上。这次我专门挤出时间参加了某家国内头部硬件企业在某顶…

作者头像 李华
网站建设 2026/10/20 11:53:17

HDFS SafeMode故障排查指南:原理、触发机制与运维实践

HDFS的SafeMode是每个做Hadoop集群运维的人都绕不开的一个状态。深夜告警群里跳出“NameNode is in SafeMode”的那一刻,值班的人心里都会咯噔一下——所有写操作被切断,客户端开始报错,跑批任务批量失败。这篇文章就从原理、触发机制和运维实…

作者头像 李华
网站建设 2026/10/20 11:53:05

麒麟系统软件商店#0002错误排查与修复全指南

1. 这个#0002错误到底在说什么,遇到先别慌先说说我自己的经历。前阵子帮一个单位处理办公电脑,系统装的是麒麟桌面操作系统,用户反馈软件商店里点"更新全部"没多久就弹出"#0002错误,更新失败"的提示。重新试了…

作者头像 李华