14ReAct 不是 for 循环,而是编译好的图
ReAct 图拓扑(默认 MaxIter=20)、cancel 建模为图节点安全点、工具映射成 AgentAction。
adk/react.goadk/chatmodel.go一个反直觉的起点
大多数人第一次实现 ReAct,写的是一个 for 循环:调模型 → 看有没有工具调用 → 有就执行工具、把结果塞回去、继续循环 → 没有就返回。这没错,但它不是 Eino 的做法。
在 Eino 里,ReAct 不是一段控制流代码,而是一张被编译好的图。理解这个转变,是理解 ADK 全部「高级能力」(取消、中断、续跑、可观测)的前提。先看它长什么样:
为什么要把循环变成图
for 循环把「下一步做什么」这个决策藏在了 Go 的控制流里。这意味着:
- 想在「执行工具之前」插一个取消检查点?得改循环体。
- 想在工具调用处中断并持久化?得手动保存一堆局部变量。
- 想让每一步都能被 callback 观测?得在循环里到处埋点。
而如果把每一步变成图中的一个节点,把「下一步」变成节点间的边,这些能力就都变成了图引擎的通用特性,而不是 ReAct 的特例。这正是 adk/react.go 所做的事。
/* * Copyright 2025 CloudWeGo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
package adk
import ( "bytes" "context" "encoding/gob" "errors" "io"
"github.com/cloudwego/eino/adk/internal" "github.com/cloudwego/eino/components/model" "github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/schema")
// ErrExceedMaxIterations indicates the agent reached the maximum iterations limit.var ErrExceedMaxIterations = errors.New("exceeds max iterations")
type typedState[M MessageType] struct { Messages []M Extra map[string]any
// ToolInfos contains the tool definitions passed to the model via model.WithTools. // Managed by the framework and modifiable by BeforeModelRewriteState handlers.// … 这只是文件开头 40 行,并非完整声明;点击上方「浏览完整文件」🔑 本章的设计钥匙
把循环「摊平」成图之后,控制流变成了数据。「要不要继续循环」不再是一条
if语句,而是图里一条可以被检查、被中断、被恢复的边。ReAct 的所有高级能力,都来自这次「控制流 → 数据」的转换。
图的拓扑
对照上面的演示,这张图的骨架是:
- Init:把用户输入和系统提示写入初始 state,作为入口。
- ChatModel:调用大模型,产出一条 assistant 消息。
- Branch:检查消息里有没有
tool_calls。这是图的分叉点。- 没有 → 走向 Exit,把最终消息作为输出返回。
- 有 → 走向工具路径。
- CancelCheck:一个安全点节点(下一节详述)。
- ToolNode:把每个
tool_call映射成一个AgentAction并发执行,结果回收成 tool 消息。 - AfterToolCalls:把工具结果并回 state。如果某个工具声明了
returnDirectly,在这里直接结束。 - 返回边:AfterToolCalls 连回 ChatModel,形成循环。这条环边,就是 ReAct 里的那个「Re」(Reasoning-Acting 的往复)。
取消:不是 kill,而是一个节点
新手常把「取消」实现成 context 一到就地 return。问题是:如果模型正写到一半、工具正执行到一半,就地返回会留下不一致的状态。
Eino 的做法是把取消建模为图里的一个节点——CancelCheck。取消信号不会硬中断正在跑的节点,而是等当前节点干净地结束,在到达 CancelCheck 这个安全点时,才优雅地停止整张图。
📝 为什么这很重要
「安全点式取消」意味着:无论你在哪一步取消,系统状态永远停在一个节点边界上——而节点边界正是可以被 checkpoint 序列化的地方(见第 23 章)。取消因此天然地和「中断 + 续跑」共用了同一套机制。
MaxIterations:给循环兜底
图里有环,就必须防止无限循环。ReAct 图默认 MaxIterations = 20(见 adk/react.go):返回边每走一次计一轮,超过上限就强制走向 Exit。这是一个务实的安全阀,而不是业务逻辑——真实任务极少需要 20 轮以上的工具往返。
把工具调用映射成 AgentAction
ToolNode 并不是简单地「顺序调用几个函数」。它把模型返回的每个 tool_call 映射成一个 AgentAction,并发执行,再把结果按顺序收敛回 tool 消息。这个 AgentAction 抽象很关键:第 16 章你会看到,正是它让「一个子 Agent」能够伪装成「一个工具」——因为在这一层,工具和 Agent 都只是「产生结果的 Action」。
💡 动手
打开
adk/react.go和adk/chatmodel.go,找到构建这张图的地方。数一数它添加了哪些节点、连了哪些边,和上面演示里的拓扑逐一对照。然后思考:如果你想在「调用模型之前」加一个限流节点,你会把它插在图的哪条边上?/** Copyright 2025 CloudWeGo Authors** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package adkimport ("bytes""context""encoding/gob""errors""fmt""math""runtime/debug""strings""sync""sync/atomic""github.com/bytedance/sonic""github.com/cloudwego/eino/adk/internal""github.com/cloudwego/eino/components/model""github.com/cloudwego/eino/components/prompt""github.com/cloudwego/eino/components/tool""github.com/cloudwego/eino/compose""github.com/cloudwego/eino/internal/safe""github.com/cloudwego/eino/schema")// … 这只是文件开头 40 行,并非完整声明;点击上方「浏览完整文件」