Day 15 / 共 20 天 · 第 3 周 ADK 智能体开发(收官)

ReAct Agent 精读

第 3 周收官。把 Day 11-14 的知识全部串起来,逐段读 ReAct Agent 的真实源码,看一个完整 Agent 从建到跑的全貌。

📍 你在整门课的位置 · 第 3 周 Agent 套件(共 4 周 · 20 天)
D11 ADK 抽象 D12 ChatModelAgent D13 多智能体 D14 中断恢复 D15 ReAct 精读
L01

ReAct 范式回顾

ReAct = Reasoning + Acting,一个循环:"想 → 做 → 看结果 → 再想 …… → 答"。它是当今工具型 Agent 最主流的范式。

🤔 痛点:为什么要专门"精读"这一个文件? 前面 14 天学的 schema、components、compose、ADK 像一堆零件,各自都懂了,但"它们到底怎么咬合成一台能自己干活的机器"还是模糊的。flow/agent/react/react.go 就是那张"装配图"——它用最少的代码把所有零件拼成一个完整 Agent,是"用图引擎搭 Agent"的最佳范本。读透它,前 14 天的知识就真正合龙了。
💡 本质:把组件用 compose 图搭成一个"模型↔工具"循环的最佳范本 react.go 的核心(NewAgentflow/agent/react/react.go:284)就干一件事:建一个 ChatModel 节点、一个 Tool 节点,用"回头边 + 流式分支"连成循环,再 Compile。这就像生活中的乐高说明书:零件(组件)你早有了,这张图告诉你"先拼哪块、边怎么扣",最后搭出一辆能跑的车。对外暴露 Generatereact.go:480)/Streamreact.go:485)/ExportGraphreact.go:490)三个口子。
论文原意:让模型交替产出"思考(Thought)"和"行动(Action,即调工具)",用工具返回的"观察(Observation)"指导下一步思考。Eino 的 ChatModelAgent(Day 12)就是 ReAct 的实现——模型输出 tool_calls(Action)→ 工具执行返回结果(Observation)→ 回到模型继续(Reasoning)。Day 12 讲的那张循环图,就是 ReAct 的骨架。
L02

一次真实运行轨迹

问:"北京今天比上海热多少度?" Agent 的内部轨迹:

🤔 Reason(模型)

"我需要北京和上海的气温,先查北京。"→ 输出 tool_call: get_weather(北京)

🔧 Act(ToolsNode)

执行 get_weather(北京)

👀 Observe(Tool 消息)

"北京 32℃" → 回头边送回模型

🤔 Reason(模型)

"还差上海。"→ 输出 tool_call: get_weather(上海)

🔧 Act

执行 get_weather(上海)

👀 Observe

"上海 27℃" → 回头边送回模型

✅ Answer(模型,无 tool_call)

"北京 32℃,上海 27℃,北京比上海热 5 度。"→ 分支走 END

读法:模型转了两圈工具(每圈一个城市),第三次不再要工具、直接给答案,分支走 END。每圈都经过"模型→分支→工具→回头边"(Day 12 的图)。这就是 ReAct 循环的真实运转。

把同一个输入按单步走查表跟到底,看关键变量怎么变(messages 是喂给模型的完整历史,越滚越长):

此刻发生什么messages(喂模型的历史)长度分支判断
0入口:用户问题进 START1 条(user 问题)
1模型节点:输出 tool_call get_weather(北京)2 条(+assistant 带 tool_call)有 tool_call → 去 tools
2工具节点:执行,得"北京 32℃"3 条(+tool 消息)回头边 → 回模型
3模型节点:还差上海,输出 tool_call get_weather(上海)4 条(+assistant)有 tool_call → 去 tools
4工具节点:执行,得"上海 27℃"5 条(+tool 消息)回头边 → 回模型
5模型节点:信息够了,直接答"北京比上海热 5 度"6 条(+assistant,无 tool_call无 tool_call → 走 END ✅
读法:盯住第 3 列——每转一圈 messages 就 +2(一条 assistant + 一条 tool),模型每次看到的历史越来越全。第 5 步模型输出里没有 tool_call,分支立刻走 END。数字具象:这道简单的两城对比,就要"2 圈工具 = 3 次模型调用",喂给模型的消息从 1 条涨到 6 条,token 成倍增长——这正是长对话越聊越贵的根源。
L03

react 包在哪

除了 ADK 的 ChatModelAgent,Eino 还有一个更"原味"的 ReAct 实现在 flow/agent/react(早期 API,很多示例用它)。两者思想一致,都用 compose 的 Graph 搭循环。

flow vs adkflow/agent/react 是较早的、直接面向"ReAct"这一具体模式的封装;adk 是更新更通用的智能体套件(支持多智能体、中断等)。学习时读 flow/agent/react/react.go 最能看清"用 Graph 搭 ReAct"的骨架,因为它更聚焦、代码更短。
START ChatModel 节点WithTools 后的模型 流式分支 ToolsNode并发执行工具 有 tool_call ★回头边:工具结果→模型 END最终答案 无 tool_call
react.go 内部图结构:ChatModel 节点 ↔ ToolsNode 靠"回头边"形成循环,流式分支决定"再转一圈"还是"出 END"。这就是整个 Agent 的真身。
L04

建图逐段读

flow/agent/react/react.goNewAgent 核心骨架(概念还原):

g := compose.NewGraph[[]*schema.Message, *schema.Message]()

// ① 模型节点(先绑定工具)
chatModel, _ := config.ToolCallingModel.WithTools(toolInfos)
g.AddChatModelNode(nodeModel, chatModel)

// ② 工具节点
toolsNode, _ := compose.NewToolNode(ctx, &config.ToolsConfig)
g.AddToolsNode(nodeTools, toolsNode)

// ③ 连边 + 分支(ReAct 循环骨架)
g.AddEdge(compose.START, nodeModel)                 // 入口→模型
g.AddBranch(nodeModel, 流式分支{有tool_call→nodeTools, 无→END})  // 模型后判断
g.AddEdge(nodeTools, nodeModel)                     // ★工具→模型:循环回头边

r, _ := g.Compile(ctx, compose.WithMaxRunSteps(config.MaxStep))
读法:整个 ReAct Agent 就是"两个节点 + 三条连接":模型节点、工具节点,START→模型、模型→分支、工具→模型(回头边)。你在 Day 08/12 学的全部在这里兑现。WithMaxRunSteps(Day 09)设循环上限防死循环。
看,没有黑魔法(一句话复述) 一个能自主用工具、多轮推理的 Agent,核心代码就这么几行——因为复杂度都被 compose 引擎(第 2 周)和 schema(Day 03)吸收了。一句话:ReAct Agent = 两个节点 + 一条回头边 + 一个分支,其余全是引擎替你扛。
🔬 简化版 vs 真实版:上面的骨架,真实 react.go 多了啥? 上面 9 行是"如果让你写你大概会这么写"的极简版。对照真实 flow/agent/react/react.go,多出来的每一块都在解决一个具体问题:
真实版多出的东西在哪解决什么问题
StreamToolCallCheckerreact.go:179不同模型把 tool_call 放在流的不同位置,需要可配置的"怎么判断这段流里有没有工具调用"
firstChunkStreamToolCallCheckerreact.go:218默认实现:只看流的第一个 chunk 就判断,够快(Day 12 流式分支)
modelPostBranchConditionreact.go:369模型输出后那个"有工具→tools / 无→END"的真实分支函数
options / callbacksoption.go / callback.go让调用方能塞入选项、埋点观测(Day 16)而不改核心
骨架不变,真实版只是把"各种模型都能用、看得见、可配置"这些工程需求补齐了。
⚠️ 小白常误以为:react.go 里那一大堆代码都是"ReAct 算法本身"。其实:算法骨架就上面 9 行,剩下大部分是适配不同模型 + 流式判断 + 可观测 + 选项这些工程外围。看源码时先抓住那条"模型→分支→工具→回头"的主干,别被外围淹没。
L05

消息如何累积

ReAct 每轮都要把"之前所有消息 + 新产生的消息"一起喂给模型,否则模型失忆。这个累积靠 State(Day 09)或节点前置处理完成:工具结果(Tool 消息)通过回头边追加进消息列表,模型节点每次拿到的是完整历史

具体实现里,模型节点前有个"整理输入"的步骤,把 State 里累积的消息 + system 提示拼成本轮的模型输入。工具节点执行后把 Tool 消息 append 进 State。所以"回头边"传的不只是这一步的结果,而是让消息历史不断增长——模型每轮看到的上下文越来越全。这也是为什么长对话会越来越贵(token 累积,呼应 Day 01 成本经济学)。
💡 生活类比:messages 就像看病的病历本 大模型是"金鱼记忆"——每次调用它只看你这次递进去的材料,上一轮说过什么它自己一点不记得。所以 ReAct 每轮都得把"到目前为止的全部对话"重新递一遍,就像看病的病历本:每查一次就往本子里夹一张新化验单(assistant/tool 消息),医生(模型)每次都要翻整本才能判断下一步。本子越夹越厚(L02 里从 1 条涨到 6 条),翻一次也越来越费劲(token 越来越贵)——这就是"回头边让历史增长"在生活里的样子。

👶 小白:模型不是刚查过北京气温吗?为什么下一轮还要把"北京 32℃"连同前面所有消息再喂它一遍,它自己不记得?

👨‍🏫 老师:它真不记得。每次 Generate/Stream 调用对模型来说都是全新的一次,模型本身没有记忆,只能看到你这次 input 里的消息。所谓"多轮对话"的记忆,其实是框架在外面替它记的——就是这节讲的:把历史累积在 State 里,每轮拼成完整 messages 再递进去。

👨‍🏫 所以"回头边"不是把模型的记忆接回去,而是把工具结果追加进那本病历,让下一轮的完整历史里多一条。理解这点你就懂了两件事:① 为什么长对话越聊越慢越贵(病历本越翻越厚);② 为什么要做"上下文裁剪/摘要"——本子太厚了得定期精简,否则迟早超出模型能读的上限。

L06

结束条件

循环什么时候停?两个出口:

  • 正常结束:某轮模型输出不含 tool_calls(它认为能直接回答了)→ 流式分支走 END → 返回最终答案。
  • 强制结束:循环轮数达到 MaxRunSteps/MaxIterations → 报错 ErrExceedMaxSteps,防死循环。
谁决定"够了"? 主要是模型自己——当它觉得收集的信息足够回答时,就不再产出 tool_call,自然走向 END。MaxSteps 只是安全网,正常情况用不到。所以模型的判断力(和提示词质量)直接决定 Agent 好不好用。
💡 生活类比:两个出口 = 自助餐怎么收场 循环停不停,就像自助餐:正常情况是你自己吃饱了主动放下筷子(模型觉得信息够了、不再要工具,走正常 END);而 MaxRunSteps 是餐厅贴的"每桌最多 2 小时"告示(强制结束 ErrExceedMaxSteps),只在你没完没了、赖着不走时才启用。健康的 Agent 应该"自己吃饱主动走",天天靠告示轰出去说明它陷在死循环里——那多半是工具老是不给模型想要的结果、它只好反复重试。
L07

全部知识串联(一图流)

一次 ReAct 运行,用到了你学的所有东西:

  • Day 03 schema:Message(含 ToolCalls)、StreamReader 承载模型流
  • Day 04 components:ChatModel.WithTools、Tool 执行
  • Day 06 Runnable:每个节点统一成 Runnable,流式分支靠 4 范式适配
  • Day 08 Graph:回头边 + 流式分支 = 循环
  • Day 09 引擎:Pregel 一轮轮跑、channel 传消息、State 累积历史、MaxSteps 兜底
  • Day 11-12 ADK:Run 把图执行翻译成 AgentEvent 事件流
  • Day 14:某轮工具可中断等人工,之后 Resume 续跑
这就是通读源码的意义:单看某一天觉得是孤立知识点,串起来才发现它们精密咬合成一台完整的机器。你现在能从一句"北京比上海热几度"追踪到底层每个字节怎么流动的——这是真正读懂了一个框架。
L08

第 3 周收官 + 动手

🎓 第 3 周(Day 11-15)你已掌握 ADK

  • Day 11 Agent 抽象:接口、事件流、Runner、Session
  • Day 12 ChatModelAgent:内部循环图、流式分支、ToolsNode
  • Day 13 多智能体:Agent-as-Tool(推荐)vs Transfer、Supervisor
  • Day 14 中断恢复:Interrupt/CheckPoint/Store/Resume
  • Day 15 ReAct 精读:把一切串成一台机器

下周(Day 16-20)进入横切与生态:回调埋点、流式深水区、Go 泛型类型安全、eino-ext 生态、构建测试收官。

✋ 动手

# 逐段读 ReAct 建图(本日核心)
sed -n '1,120p' flow/agent/react/react.go | head -80
grep -n 'AddChatModelNode\|AddToolsNode\|AddEdge\|AddBranch\|Compile' flow/agent/react/react.go
明天预告 · Day 16callbacks 回调系统——不改业务代码就能给任意节点埋点(日志、监控、计时、追踪)。5 个触发时机、AOP 思想、handler 怎么挂。
← Day 14 中断恢复 Day 16 · callbacks →