ReAct Agent 精读
第 3 周收官。把 Day 11-14 的知识全部串起来,逐段读 ReAct Agent 的真实源码,看一个完整 Agent 从建到跑的全貌。
ReAct 范式回顾
ReAct = Reasoning + Acting,一个循环:"想 → 做 → 看结果 → 再想 …… → 答"。它是当今工具型 Agent 最主流的范式。
flow/agent/react/react.go 就是那张"装配图"——它用最少的代码把所有零件拼成一个完整 Agent,是"用图引擎搭 Agent"的最佳范本。读透它,前 14 天的知识就真正合龙了。NewAgent,flow/agent/react/react.go:284)就干一件事:建一个 ChatModel 节点、一个 Tool 节点,用"回头边 + 流式分支"连成循环,再 Compile。这就像生活中的乐高说明书:零件(组件)你早有了,这张图告诉你"先拼哪块、边怎么扣",最后搭出一辆能跑的车。对外暴露 Generate(react.go:480)/Stream(react.go:485)/ExportGraph(react.go:490)三个口子。一次真实运行轨迹
问:"北京今天比上海热多少度?" Agent 的内部轨迹:
"我需要北京和上海的气温,先查北京。"→ 输出 tool_call: get_weather(北京)
执行 get_weather(北京)
"北京 32℃" → 回头边送回模型
"还差上海。"→ 输出 tool_call: get_weather(上海)
执行 get_weather(上海)
"上海 27℃" → 回头边送回模型
"北京 32℃,上海 27℃,北京比上海热 5 度。"→ 分支走 END
把同一个输入按单步走查表跟到底,看关键变量怎么变(messages 是喂给模型的完整历史,越滚越长):
| 步 | 此刻发生什么 | messages(喂模型的历史)长度 | 分支判断 |
|---|---|---|---|
| 0 | 入口:用户问题进 START | 1 条(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 ✅ |
react 包在哪
除了 ADK 的 ChatModelAgent,Eino 还有一个更"原味"的 ReAct 实现在 flow/agent/react(早期 API,很多示例用它)。两者思想一致,都用 compose 的 Graph 搭循环。
flow/agent/react 是较早的、直接面向"ReAct"这一具体模式的封装;adk 是更新更通用的智能体套件(支持多智能体、中断等)。学习时读 flow/agent/react/react.go 最能看清"用 Graph 搭 ReAct"的骨架,因为它更聚焦、代码更短。建图逐段读
flow/agent/react/react.go 的 NewAgent 核心骨架(概念还原):
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))
WithMaxRunSteps(Day 09)设循环上限防死循环。flow/agent/react/react.go,多出来的每一块都在解决一个具体问题:
| 真实版多出的东西 | 在哪 | 解决什么问题 |
|---|---|---|
StreamToolCallChecker | react.go:179 | 不同模型把 tool_call 放在流的不同位置,需要可配置的"怎么判断这段流里有没有工具调用" |
firstChunkStreamToolCallChecker | react.go:218 | 默认实现:只看流的第一个 chunk 就判断,够快(Day 12 流式分支) |
modelPostBranchCondition | react.go:369 | 模型输出后那个"有工具→tools / 无→END"的真实分支函数 |
| options / callbacks | option.go / callback.go | 让调用方能塞入选项、埋点观测(Day 16)而不改核心 |
消息如何累积
ReAct 每轮都要把"之前所有消息 + 新产生的消息"一起喂给模型,否则模型失忆。这个累积靠 State(Day 09)或节点前置处理完成:工具结果(Tool 消息)通过回头边追加进消息列表,模型节点每次拿到的是完整历史。
👶 小白:模型不是刚查过北京气温吗?为什么下一轮还要把"北京 32℃"连同前面所有消息再喂它一遍,它自己不记得?
👨🏫 老师:它真不记得。每次 Generate/Stream 调用对模型来说都是全新的一次,模型本身没有记忆,只能看到你这次 input 里的消息。所谓"多轮对话"的记忆,其实是框架在外面替它记的——就是这节讲的:把历史累积在 State 里,每轮拼成完整 messages 再递进去。
👨🏫 所以"回头边"不是把模型的记忆接回去,而是把工具结果追加进那本病历,让下一轮的完整历史里多一条。理解这点你就懂了两件事:① 为什么长对话越聊越慢越贵(病历本越翻越厚);② 为什么要做"上下文裁剪/摘要"——本子太厚了得定期精简,否则迟早超出模型能读的上限。
结束条件
循环什么时候停?两个出口:
- 正常结束:某轮模型输出不含 tool_calls(它认为能直接回答了)→ 流式分支走 END → 返回最终答案。
- 强制结束:循环轮数达到
MaxRunSteps/MaxIterations→ 报错ErrExceedMaxSteps,防死循环。
MaxRunSteps 是餐厅贴的"每桌最多 2 小时"告示(强制结束 ErrExceedMaxSteps),只在你没完没了、赖着不走时才启用。健康的 Agent 应该"自己吃饱主动走",天天靠告示轰出去说明它陷在死循环里——那多半是工具老是不给模型想要的结果、它只好反复重试。全部知识串联(一图流)
一次 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 续跑
第 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