ReAct 到底在循环什么:从零拆解 Agent 的「想一步、做一步」机制

场景

先想一个很常见的下午。你手头有个活儿:看看三个竞品上个月有没有发新版本,整理成一张对比表。你懒得自己一个个翻官网,于是把这句话原封不动丢给一个聊天模型。

它多半会给你一张表。格式整齐,列名齐全,甚至还贴心地加了「亮点分析」。问题在于内容全是编的------版本号是编的,发布日期是编的,那几个「亮点」也是它根据竞品名字的语感猜出来的。你盯着这张表,越看越不对劲:它怎么知道人家发了新版?它根本没去看过。

这不是模型不够聪明。换个更强的模型,它编得更像真的,但依然是编的。原因很朴素:聊天模型只有「生成」这一个动作,没有「查」这个动作。它的工作方式是线性的------你给一段文字,它吐一段文字,结束。它不会在中途停下来问自己一句「我手上的信息够不够」,因为它的结构里压根没有「中途」这个位置。

而你要的这件事,天然是分步的:先查 A 的版本历史,看完发现还得知道发布时间,再去查时间;三家的数据凑齐了,才能做对比。每一步查什么,取决于上一步看到了什么。这种「走一步看一步」的推进方式,线性的一问一答给不了。

那能不能我在代码里把流程写死?先搜 A、再搜 B、再搜 C、最后总结。可以,但这就不是 Agent 了,这叫工作流(Workflow)。工作流的每一步是你提前定好的,Agent 的每一步是模型在运行时自己定的。这个区别看着小,它决定了后面所有的实现细节:既然步骤是模型定的,你就得给它一个能反复说话的通道,还得给它一套说错话时怎么收场的兜底。

所以真正卡住人的不是「模型不会查」,而是模型自己不知道什么时候该查、什么时候该停。要让它在运行时决定下一步,代码结构必须从「调一次 API 拿结果」变成「调很多次 API,每次的结果都攒起来,下一次带着全部历史再问一遍」。这个结构长什么样,就是接下来要拆的东西。

原理

上一章留下的结论是:必须反复调 API,并且把每次结果攒起来。这个「反复调、攒结果」的结构,就是 ReAct 的全部骨架。它由三根支柱撑着,缺一根都转不起来。

第一根支柱:把模型的输出切成三种角色。

ReAct 这个名字来自 Reasoning + Acting,意思是「推理 + 行动」。它的做法不是让模型输出更复杂的东西,恰恰相反------它约定模型在每一轮只做一件事:要么输出一段思考加一个工具调用请求,要么直接输出最终答案。

这里我用一个类比把它讲实。把模型当成一个只能通过纸条跟你沟通的同事:你俩不能打电话,只能递纸条。ReAct 就是你们事先约好的纸条格式------纸条上要么写「我需要查一下 X,请把结果给我」,要么写「我想清楚了,答案是 Y」。他递第一种,你就去查,把结果写在下一张纸条上递回去;他递第二种,对话结束。整个 Agent 就是这么一个来回递纸条的过程,只不过纸条递得飞快,几十轮也就几秒钟。

有个地方特别容易误解,我得点出来:模型本身不会「自动」进入这个循环。它只是被提示词要求按这个格式输出。所谓「ReAct 模式」,本质是你写在系统提示词里的一段格式约定,加上你在代码里对这段格式的解析。换句话说,ReAct 是协议,不是能力。模型不会因为用了 ReAct 就变聪明,它只是被要求把「我想干什么」和「我干完了」分开说清楚,好让你的代码能接得住。

换个角度说,模型在每一轮里其实同时干了两件事:先写一段自然语言的推理(也就是「我现在缺什么、我打算怎么补」),再给出一个动作。这两件事写在同一次输出里,顺序是先想后做------这就是「Reasoning + Acting」这个命名的由来。之所以强调「同一轮」,是因为早期有些做法是「先让模型纯想几轮,再让它纯做几轮」,那样模型在想的阶段看不到真实反馈,容易越想越偏;ReAct 把想和做压进同一轮,模型每次想之前都能看到上一轮工具返回的真实结果,推理始终踩在事实之上。

这里还可以再往下挖一层:为什么「先想后做」比「直接做」更可靠?因为自然语言的那段推理,实际上是模型在给自己做一次显式的中间计算。它把「我缺什么、我准备怎么补」写出来之后,这个判断就留在了上下文里,下一轮它自己能看到。这相当于给模型的决策过程加了一道留痕------它不能再悄悄改口,因为它上一轮白纸黑字写了要干什么。我在调试 Agent 时特别依赖这一点:出问题时,那段推理文本往往是唯一能告诉我「它当时在想什么」的线索。

第二根支柱:工具清单,以及它为什么每轮都要重发。

模型要「请求查一下 X」,它得先知道有哪些东西可查、每个要传什么参数。这份清单就是工具定义:每个工具包含一个名字(name)、一段用途描述(description)、一份参数结构(schema)。模型看不到函数体,只能看到这三样,靠 description 判断该不该调、调哪个。

我踩过的一个坑就在这里:工具定义不是注册时发一次就够,而是每轮请求都要带上。原因是循环过程中工具集合可能变------比如用户中途挂载了一个新的外部服务,新工具只有在下一轮请求里出现在清单上,模型才可能用上它。如果只在第一轮发,模型后面就「瞎」了,明明有工具摆在眼前也调不到。

还有一点值得单独说:description 的分量比大多数人想的重。模型选工具完全靠读描述,描述写得含糊,它就会该调不调、或者反复调同一个。这不是玄学调参,是信息质量问题。我习惯把每个工具的 description 当成写给新同事看的接口注释------说清「什么时候用、参数什么意思、返回什么」,而不是「查询天气」四个字了事。

这里还有个成本上的连带效应,容易被忽略:工具清单每轮都发,意味着它会占用每一轮的输入 token。工具一多,光是清单本身就能吃掉可观的上下文预算。所以工具不是越多越好------每加一个工具,你都在为它付出「每一轮都要重新描述一遍」的代价。我一般的判断是:一个 Agent 挂十几个工具已经偏多了,超过这个量就该考虑按场景分组,或者把一组相关操作收进一个带 action 参数的工具里,而不是平铺成十几个独立函数。

第三根支柱:退出条件,也就是这个循环怎么停。

最理想的情况是模型自己说「我想清楚了」,也就是本轮返回里没有工具调用请求,循环正常结束。但你不能只靠这一条,因为模型可能陷入死循环:反复调同一个工具、参数都不变,或者一直调下去烧 token。

所以要设几道兜底。一是轮次上限 ,比如超过 100 轮强制停;二是 token 上限 ,累计消耗达到模型上下文窗口的某个比例(常见默认值是 80%)就停;三是重复检测,同一个工具配同一组参数连续调用到第 3 次,不直接停,而是往对话里塞一条纠正消息,提醒模型换个思路。

前两类是硬停,第三类是软停。软停的设计意图值得琢磨:它给模型一次自我纠正的机会,而不是粗暴掐断。硬停是「我不信你能自己收敛了」,软停是「我再给你一次机会,但你得换个走法」。这两种处理在工程上要分开,因为它们对应的故障不一样------硬停防的是成本失控,软停防的是原地打转。

为什么软停要「塞一条消息」而不是「直接跳过这次调用」?因为模型是无状态的:它这一轮的判断完全基于消息数组里能看到的内容。如果你只是悄悄不执行这次重复调用,模型下一轮看到的还是「我请求了、但没有结果」,它很可能原样再请求一次。把「这个调用重复了,请换一种方式」写进历史,模型才有依据改变行为。这一点是所有兜底设计的通用原则:要让模型改变行为,就得把改变的理由放进它能看见的上下文里。

把三根支柱串起来,骨架就清楚了。

实现的骨架是一份消息数组,里面按顺序放系统提示、用户问题、模型的每次输出、每次工具返回的结果。每一轮循环做四件事:检查预算(token 和轮次)、必要时压缩上下文、把整个消息数组加工具清单发给模型、看返回里有没有工具调用。有就执行、把结果追加进数组、进入下一轮;没有就返回最终答案。

这里可以顺手把「推理模式有哪些」这个问题回答掉。ReAct 是边想边做 ;Plan-and-Execute 是先列完所有步骤再逐步执行 ,适合步骤能提前看清的任务;Reflexion 是做完之后回头自我批评再改一版 ,相当于给循环加了一层复盘;Multi-Agent 是把不同角色拆成多个循环并行跑。它们的共同前提是「模型的输出可以被结构化解析」------没有这个前提,什么模式都落不了地。对绝大多数「需要查外部信息再汇总」的任务,ReAct 是最小可用的那个,其他模式更像是它的补丁,而不是替代品。

我自己的判断是:别一上来就挑模式。先把 ReAct 这个最小循环手写一遍,你会对「模型在运行时决定下一步」这件事有具体的体感;有了这个体感,再去看 Plan-and-Execute 或 Reflexion,你会立刻明白它们各自在补哪个洞。反过来,如果连基础循环都没写过,直接上手多智能体框架,很容易被一堆抽象名词绕晕,最后连问题出在哪一层都说不清。

实例

原理章讲清了循环的四步骨架。现在把它落到代码里,看它到底长什么样,跑起来之后你会看到什么现象。

场景延续第一章:让助手去「查」两个数再相加,而不是自己算。这个任务小到能一眼看完,但完整覆盖了「思考---调用---观察---再思考---收尾」的全过程。

第一步:定义工具和它的元信息

先写两个工具,一个查数、一个做加法。关键不在函数体,而在描述和参数结构------模型就是靠这两样决定调不调。

python 复制代码
tools = [
    {
        "name": "lookup",
        "description": "查询某个名字对应的数值,参数 key 为要查的名字",
        "parameters": {"type": "object", "properties": {"key": {"type": "string"}}, "required": ["key"]},
    },
    {
        "name": "add",
        "description": "把两个数相加,参数 a、b 为两个加数",
        "parameters": {"type": "object", "properties": {"a": {"type": "number"}, "b": {"type": "number"}}, "required": ["a", "b"]},
    },
]

写完这一步你要意识到:模型此刻并不知道 lookup 背后是查数据库还是查一个写死的字典,它只知道「有这么个东西,能按名字查数」。这就是工具抽象的全部意义------模型面对的是接口,不是实现。

第二步:搭消息数组,把系统提示写死

系统提示里要明确告诉模型「你需要什么就调工具,别自己编」,并规定它输出工具请求的格式。这一步是把原理章说的「纸条约定」真正落到文字上。

python 复制代码
messages = [
    {"role": "system", "content": "你是助手。需要数据时调用工具,不要凭空猜测。信息足够后直接给出答案。"},
    {"role": "user", "content": "帮我查一下 apple 和 banana 的值,然后告诉我它们的和。"},
]

第三步:写主循环,四件事按顺序做

这是全文最核心的一段代码,也是原理章那张骨架图的直接翻译。

python 复制代码
for turn in range(MAX_TURNS):                      # 轮次兜底
    if used_tokens > 0.8 * CONTEXT_WINDOW:         # token 兜底
        break
    resp = llm.chat(messages, tools=tools)         # 每轮都带上工具清单
    messages.append(resp.message)                  # 模型输出进历史
    if not resp.tool_calls:                        # 没有工具请求 = 正常结束
        print(resp.content)
        break
    for call in resp.tool_calls:                   # 有请求就执行
        result = run_tool(call.name, call.arguments)
        messages.append({"role": "tool", "tool_call_id": call.id, "content": result})

跑起来你会看到什么:第一轮模型返回一个 lookup(apple) 的请求,工具返回数值,消息数组多两条;第二轮它再请求 lookup(banana);第三轮请求 add;第四轮它不再请求工具,直接输出「两个数之和是 X」。

整个过程里没有任何一行代码写「先查 apple 再查 banana」------顺序是模型自己走出来的。这就是第一章说的「Agent 和工作流的区别」在代码里的样子。你给的是工具和约定,走法由它在运行时决定。

第四步:故意让它出错,看兜底怎么生效

把 add 工具的 description 改得含糊一点,比如只写「计算」两个字,再跑一次。你会看到模型开始反复试探,甚至连续几轮调同一个工具。

这时重复检测生效:同一工具同参数到第 3 次,往消息里插一条「该调用已重复,请换一种方式」,模型通常会改口直接给答案,或者换一个工具。这一步的意义是让你亲眼看到:兜底不是可选项,是让循环能收敛的必要零件。没有它,这个 demo 会一直转到轮次上限才停。

第五步:观察 token 增长,理解压缩为什么必要

在循环里打印每轮的消息数组长度。跑几个稍长的任务你会发现,工具返回的长文本会把历史迅速撑大------一次网页抓取回来几千字,几轮下来就顶到窗口上限了。

当占用逼近上限,就得把早期轮次摘要成一段话,保留最近几轮原文,重建消息数组。结构大致是:系统提示词、一条装着摘要的用户消息、一条「好的,我已了解之前的上下文」的助手消息,再接上最近几轮的原文。

这里有个实操细节要提醒:切分点必须落在一条完整用户消息的边界上,不能把「工具请求」和「工具返回」拆开。否则模型看到的是半截上下文------它看到一个工具调用,却找不到对应的结果,后面的推理就全乱了。我一般会先把对话按轮次切开,再决定从哪一轮开始摘要。

边界

上一章的最小循环能跑通,但它离能用还有距离。这条路径在几个地方会翻车,我按踩坑频率排一下。

第一类坑:不可控的轮次和成本。 循环次数由模型决定,这意味着单次任务的耗时和费用是波动的。同一个问题,模型今天三轮收敛,明天可能十轮。所以轮次上限和 token 上限不是「以防万一」,而是要按你的成本预算倒推着设。我一般的做法是先用一个偏紧的上限跑一批真实任务,看正常任务的轮次分布,再把上限设在分布的高位,而不是拍脑袋给个 100。上限设太松,等于没设;设太紧,正常任务会被误杀。

第二类坑:工具描述就是你的接口文档。 描述含糊,模型就乱调;描述太长,又白烧 token。这不是提示词工程的花活,是接口设计问题。每个工具的 description 应该像写给新同事看的注释,说清「什么时候用、参数什么意思、返回什么」。这件事没有捷径,只能一个工具一个工具地抠。

第三类坑:并行调用和超时。 一轮里模型可能一次返回多个工具请求。串行执行简单但慢,并行要加超时保护------单个调用超时、整批调用超时都得设,否则一个卡住的请求会拖死整个循环。超时的工具要返回明确的失败状态,而不是静默丢弃,让模型知道「这条没拿到」。我见过最坑的情况是超时被当成空结果返回,模型以为查到了空值,接着往下推,最后给出一份基于空数据的结论。

第四类坑:幻觉工具名。 模型有时会编出一个不存在的工具名,比如凭空冒出一个 delete_database。正确做法不是崩溃,而是把「未知工具:delete_database」作为工具结果塞回历史,模型看到后一般会自我纠正,换一个正确的工具重试。这个处理很便宜,但没有它,循环会直接抛异常。

什么情况下别用 ReAct。 如果任务的步骤是固定的、可枚举的------比如「读文件、解析、写库」这种每次都一样的流程------那就写工作流,别上 Agent。让模型去「决定」一件本来就确定的事,只会引入不确定性和额外成本。ReAct 的价值在于步骤无法提前确定、需要根据中间结果临时决策的场景。另外,如果任务需要严格可审计、可回放,循环式 Agent 也不是好选择,因为它的执行路径每次都不一样,你没法拿昨天的日志去复现今天的问题。

小结

回到开头那个下午:那份编造出来的竞品对比表,问题不在模型不够强,而在于它没有被放进一个能「查了再想」的结构里。ReAct 补的就是这个结构,而它由三样东西组成------一份约定好格式的提示词、一份每轮都要重发的工具清单、一套包含硬停和软停的退出兜底。

几个能带走的判断:ReAct 是提示词层面的协议,不是库也不是模型能力,理解这一点就不会被各种框架名字绕晕。循环的本质是往一个消息数组里反复追加内容,模型的每次输出和每次工具返回都进这个数组。正常结束的标志是模型不再请求工具,其余所有停止都是兜底,兜底必须设。工具描述的质量直接决定 Agent 的表现,它更像接口设计而不是调参。步骤固定的任务用工作流,步骤需要临场决定的才用 ReAct。

至于 Plan-and-Execute、Reflexion、Multi-Agent,可以先把它们理解成给这个基础循环打的补丁:分别补「提前规划」「事后复盘」「角色分工」。等你手写过一遍最小循环,再去看这些模式,会发现它们改的都是同一件事------循环里那一步「想」的内容和时机。

最后留个问题给你:你在自己的项目里,有没有哪一步其实是「模型必须临场决定」的?如果一步都没有,那你要的可能不是 Agent。

相关推荐
码事漫谈7 小时前
三步改掉 AI 味,附可直接复制的去 AI 味提示词
后端
IT_陈寒8 小时前
Redis卡顿的锅,这次真不是大key的错
前端·人工智能·后端
可乐鸡翅yeah_8 小时前
video.js 集成 hls.js 开发 M3U8 播放器,新手高频踩坑
开发语言·前端·javascript·后端·ecmascript·m3u8·音视频在线播放
程序猿_极客8 小时前
【免费】分享一套优质的基于SpringBoot的服装商城管理系统的设计与实现(带可视化图表、协同过滤功能),源码+文档+视频详解(讲解)
java·spring boot·后端·服装商城管理系统
明月_清风8 小时前
面对陌生的 GitHub 项目无从下手?这 4 个网站帮你快速读懂源码
前端·后端·github
bug菌9 小时前
🤔同事突然问我:Spring的注解 @Component 和 @Service 有何不同?
java·spring boot·后端
leobertlan9 小时前
痛苦系列 | DSP-02 频域切片:DTFT与DFT的探索
android·后端
加我攻城狮9 小时前
当大模型把「景区」定位到附近公共厕所:我的 AI 旅行小程序踩坑实录
后端·微信
Lonely丶墨轩9 小时前
用 Vue + Spring Boot + Python 做一个 AI 双人海龟汤游戏:从出题、联机到自动审稿等核心技术设计与实现
spring boot·后端·游戏
猪猪拆迁队9 小时前
跨电脑复制共享-WebRTC 打洞踩坑
前端·后端·go