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.WithToolsToolsConfig工具配置,复用compose.ToolsNodeConfigMaxIterations默认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:模型输出为空、tool_calls=1——它没有直接回答,而是请求调用get_ticket。事件 2:角色是tool,tool_name是get_ticket——工具执行结果回到流程。事件 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 日:五分钟上手 Eino ADKEino ADK 概述Agent 还是 Graph?AI 应用路线辨析Eino 主仓库本文示例按Eino v0.9.21编写,使用模拟模型(不访问网络),已实机运行,正文事件流为真实输出。
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.WithToolsToolsConfig工具配置,复用compose.ToolsNodeConfigMaxIterations默认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:模型输出为空、tool_calls=1——它没有直接回答,而是请求调用get_ticket。事件 2:角色是tool,tool_name是get_ticket——工具执行结果回到流程。事件 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 日:五分钟上手 Eino ADKEino ADK 概述Agent 还是 Graph?AI 应用路线辨析Eino 主仓库本文示例按Eino v0.9.21编写,使用模拟模型(不访问网络),已实机运行,正文事件流为真实输出。