目录 · 第 14 / 28 章
EinoPart III · ADK 的实现

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 的所有高级能力,都来自这次「控制流 → 数据」的转换。

图的拓扑

对照上面的演示,这张图的骨架是:

  1. Init:把用户输入和系统提示写入初始 state,作为入口。
  2. ChatModel:调用大模型,产出一条 assistant 消息。
  3. Branch:检查消息里有没有 tool_calls。这是图的分叉点。
    • 没有 → 走向 Exit,把最终消息作为输出返回。
    • 有 → 走向工具路径。
  4. CancelCheck:一个安全点节点(下一节详述)。
  5. ToolNode:把每个 tool_call 映射成一个 AgentAction 并发执行,结果回收成 tool 消息。
  6. AfterToolCalls:把工具结果并回 state。如果某个工具声明了 returnDirectly,在这里直接结束。
  7. 返回边: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.goadk/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 adk
import (
"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 行,并非完整声明;点击上方「浏览完整文件」
源码

正在读取完整文件…