前两天做 Agent 项目复盘,我把最早基于 ReAct 写的那套代码又翻了出来。说实话,如果两年前有人跟我说:Agent 会从 ReAct 这种"想一步、做一步"的短循环,一路进化到 Ralph Loop 这种可持续迭代的闭环范式,我大概率会觉得他又在造概念。但等我真正把一套运营分析 Agent 从单轮问答重构到带经验库的持续迭代结构,又跑了几百个任务之后,我才确信这条演进路线不是什么术语游戏,而是 Agent 从"能跑通 Demo"走向"能稳定干活"的分水岭。
这篇文章想聊清楚三件事:第一,ReAct 的设计里到底哪些地方是真正有用的,哪些地方注定了它撑不起复杂任务;第二,Ralph Loop 这类持续迭代范式在结构上改了什么,一个完整迭代单元的内部长什么样;第三,也是我觉得最值得看的,把一套 ReAct 代码迁移到循环范式时,每一步应该怎么做,以及上线之后我踩过的坑。内容不挑框架,适合正在做 Agent 开发的朋友,也适合想搞明白"Agent 为什么需要循环"的产品同学。
1. ReAct 为什么值得被记住:它把"行动"还给了模型
1.1 一段典型的 ReAct 轨迹:思考、调用、观察交替出现
很多人把 ReAct 当成一种"提示词技巧",我觉得这低估了它。它的原始想法非常朴素:不要让大模型直接回答一个需要外部信息的问题,而是逼着模型先想一句"我现在缺什么信息",再动手去调一个工具,拿到工具的返回结果之后再决定下一步。这个过程里,Thought、Action、Observation 三者交替出现,直到模型觉得可以参考已有信息输出答案为止。
举个具体例子。假设 Agent 的目标是"查一下上季度销售额,并给出渠道层面的分析",一条典型的 ReAct 轨迹长这样:
Thought:用户需要的是上季度的真实数据,我不能凭记忆编,先查销售库。 Action:query_sales(quarter="Q3") Observation:返回 12 行记录,包含渠道、月份、销售额。 Thought:原始记录太细,直接回答会很长,按渠道聚合更合适。 Action:aggregate(by="channel") Observation:聚合结果出来了,三个渠道合计 XX 万。 Thought:现在信息够了,可以回答。 Final Answer:按渠道拆分给出结论。
这段轨迹看起来平淡,但它做对了一件很关键的事:模型回复里的每一个关键论断,都经过了工具结果的确认。这就是 ReAct 抑制幻觉的真正机制——它没有让模型"记住正确答案",而是让模型学会"去什么地方找正确答案"。生活里类比就是问路,ReAct 不是逼着你凭印象画地图,而是允许你掏出导航软件一步步看路。
1.2 它设计上的巧劲:推理和行动之间只隔一条很窄的协议
ReAct 能火起来,除了效果好,更重要的原因是它实现起来太轻了。模型的输出被约定成一种包含 Thought 和 Action 两段文字的格式,运行时只要解析出动作名和参数,去调用对应的工具函数,把返回值拼回上下文。没有状态机,没有记忆模块,没有额外的训练阶段。它把"推理"这件事留给模型,把"执行"交给工具,两者之间的接口就是一条非常窄的文本协议。
这在当年算是一种"最小可行设计"。那时大规模微调还没普及,通用大模型的工具调用能力并不稳定,ReAct 用几条 few-shot 示例就能让模型学会多步工具调用,而且换一批工具也能迁移。从工程角度讲,这个设计的优雅之处在于:它不假设模型有多强,只假设模型"会读返回结果、会接着往下想"。后来的很多范式都在这条轨迹上加东西,但加得越少,越容易被广泛复制,ReAct 恰恰是因为轻,才成为 Agent 这个领域的底盘。
1.3 三个"一次性"缺陷:记忆、反馈、目标都撑不住长任务
先说记忆。ReAct 的唯一记忆载体是上下文窗口。同一个 Agent 跑完一个任务,这场对话里积累的所有观察和中间推理,随着任务结束就丢掉了。下一次它遇到类似问题,又从零开始摸索。我见过很多团队把 Agent 当成"自带记忆"来接客,实际跑起来才发现它每个会话都是失忆的。
再说反馈。ReAct 的循环里,模型只接收"当前这一步"的 Observation,没有"事后复盘"的环节。这意味着即便它在这一轮里犯了某个模式性的错误,比如把日期格式解析错了,它也只会在报错之后尝试换一种解析方式,而不会停下来想一想:"这类日期解析任务,我是不是应该永远先试 ISO 格式?" 每个任务都在重复交学费,这在大规模并发场景里是很致命的——同一个坑被一百个会话各踩一次。
最后说目标。ReAct 的任务边界是一条问题,模型认为信息够了就会直接输出 Final Answer。它天生不具备"为同一个长期目标持续工作几小时"的能力,也不会主动判断"我距离目标还差哪些证据"。换句话说,ReAct 适合"应答",不适合"使命"。当任务变成"帮我持续监控这个季度的渠道健康度,发现异常就深挖原因",它立刻就会露馅。这三个一次性缺陷,就是后面所有迭代范式想解决的核心问题。
2. Ralph Loop 改的是什么:从"执行器"变成"自迭代器"
2.1 名字的来历和 Ralph 的常见拆解
"Ralph Loop"这个说法我在内部项目里第一次见到,当时只是给那套"永远不自以为完成"的循环结构起的代号。后来在同行交流时,发现各家对 Ralph 的拆解并不完全统一,最常见的记忆方式是把一次循环拆成 Reflect、Act、Learn、Plan、Hypothesize 五个动作,首字母正好拼成 Ralph。
我不打算把某个拆解当成唯一正解,因为这类实践概念本来就是在迭代中被重新定义的。真正重要的是它背后的范式主张:Agent 不是一个"调用一次、回答一次"的生成器,而是一个持续运行的反馈闭环。在闭环里,每一次行动产生的不是最终答案,而是可供下一轮改进的证据;每一轮结束时的产物,不是答案,而是经验、计划和新的假设。理解了这一层,拆成几个字母都只是方便记忆而已。
2.2 一张表看两种范式的结构性差异
我用一张表总结了迁移前后的关键差异,方便直接用:
| 维度 | ReAct | Ralph Loop |
|---|---|---|
| 最小运行单元 | 单轮任务:问题到最终答案 | 持续闭环:目标到收敛退出 |
| 反馈来源 | 当前动作的即时 Observation | 全回合轨迹的结构化复盘 |
| 记忆位置 | 上下文窗口内 | 独立经验库,跨任务复用 |
| 错误处理 | 靠下一步思考临时补救 | 靠历史经验指导 + 计划重排 |
| 计划方式 | 走一步看一步 | 滚动规划的任务树 |
| 循环退出 | 模型判定可以回答就停 | 预算、收敛、置信度三重判定 |
| 适用场景 | 一次性查询、单步工具 | 长期任务、复杂推理、自我优化 |
这张表是我重构以后才画出来的。一开始我以为差别只在"加了个 while 循环",后来发现远不止如此:真正的变化是 Agent 的"体内循环"从模型单次推理延伸到了系统层面,记忆和计划都从上下文中剥离出来,变成了系统自己可以读写的资产。
2.3 三个关键转变:反馈升级、经验外置、计划滚动
第一个转变是反馈升级。ReAct 的反馈是"事情的结果",Ralph 的反馈是"这件事教会了我什么"。前者只是一条数据,后者是在数据之上做的抽象。同样是调用查询接口报错,ReAct 只看到"报错了,换个参数再试";Ralph 会多走一步:把"这类指标名容易写错,先查指标字典再拼参数"沉淀成一条规则,下次遇见同类任务直接命中。
第二个转变是经验外置。经验不再挤在上下文窗口里,而是落在独立的经验库中,用场景标签、关键词甚至向量索引来检索。上下文只保留"当前这轮需要的卡片"。这等于给 Agent 装了一个外挂硬盘,滚动窗口再长也不怕,反而越用越熟。
第三个转变是计划滚动更新。ReAct 没有严格意义上的计划,都是临场想下一步;Ralph Loop 里有一个明确的 planner,每一轮基于最新观察修订计划。计划不是一开始就写死的承诺,而是随着证据增加不断修正的假设。这三个转变合在一起,才让 Agent 第一次拥有了"自我迭代"的能力,而不只是"连环问答"。
3. 拆解一个 Ralph 迭代单元:五个步骤各自在解决什么问题
3.1 Reflect:复盘不是写日记,边界要刻意收窄
很多第一次做反思模块的人,会让模型"总结一下这段经历",结果得到的全是"这次任务总体顺利,我在过程中学会了更好地使用工具"这种正确的废话。复盘必须收窄边界,我用的模板就三个问题:
- 目标是否完成?如果没有,差距具体是什么?
- 哪一步动作导致了偏差?这一步的 Observation 原文是什么?
- 这条偏差能否抽象成一条可复用的规则?规则必须带反例。
这三个问题强制模型引用实际观察到的内容,而不是凭印象写作文。复盘还要限制长度,超过两句话就说明它又进入了泛泛而谈的状态。反思本身也是消耗 Token 的,不收窄边界的反思,等于在循环里额外开了一条"话痨"通道。如果复盘产出的规则不能放进某个后续任务里验证,这条规则就不应该被写进经验库。
3.2 Act:动作层要长出"仪表盘",否则一切复盘都是空中楼阁
循环越深,动作的"可观测性"就越重要。我迁移后的第一件事,不是加记忆,而是给每一次工具调用加上结构化追踪字段:动作 ID、动作名、参数摘要、开始结束时间、返回状态、返回结果摘要、全程 Token 消耗。看起来啰嗦,但没有这些字段,后面的 Reflect 和 Learn 就没有数据可用,反思是在沙子上面盖楼。
还有一个很容易被忽略的细节:动作的可重放性。每条追踪必须能还原成"当时到底发生了什么",包括请求参数和返回原文。调试的时候我经常发现模型在 Observation 摘要里"洗完数据",明明工具返回了 20 行,它只把结果总结成一句话,后续推理就建立在这句话上。所以我在执行层强制要求:关键的 Observation 必须保留原文摘录,复盘时模型要能回看原始返回。这一步做扎实了,后面的经验库才有可信度。
3.3 Learn:经验卡片的三段式结构,和从"候选"到"验证"的晋升机制
经验库我建议设计成"经验卡片"的形式,每张卡片三段式:场景标签(什么时候适用)、规则正文(可执行的判断或动作)、反例/前提(什么时候不能用)。举个真实例子,一张日期解析卡片可以长这样:
场景:日期解析 / 销售周报 规则:先试 ISO8601,再试 yyyy/MM/dd,美国式 MM/dd/yyyy 总是在字段缺失时被误判,务必先看数据样例。 反例:当输入来源是系统日志且格式固定时,直接按日志格式解析,不要走通用规则。
存储上先用 SQLite 或 JSON 就行,不用一上来上向量库。我是这样建表的:
CREATE TABLE experience_cards ( id INTEGER PRIMARY KEY, scene TEXT, -- 适用场景标签 title TEXT, rule TEXT, -- 规则正文 counterexample TEXT, -- 反例 source_task TEXT, -- 来源任务ID,可追溯 status TEXT DEFAULT 'candidate', -- candidate / verified use_count INTEGER DEFAULT 0, created_at TEXT, updated_at TEXT );这里我强烈建议加一列 status。反思产生的卡片一律是 candidate,只有同一个解决方式在另一个任务里再次验证成功,才能提升为 verified。这样能挡住不少"伪经验",后面第 5 节我会专门讲它带来的坑。
3.4 Plan:从"线性步骤"到"任务树",计划是证据的产物
Ralph 的规划器和 ReAct 的"下一步思考"最大的区别是:规划器维护一棵任务树,每个节点有状态(pending / running / done / aborted),planner 每轮基于最新观察重新约束和修订这棵树,executor 只执行当前"前沿节点"。计划里的每个节点都挂着一个前置证据:我之所以要执行这个子任务,是因为上一轮看到了什么。
这种做法带来的变化是,中间某条路走不通时,Agent 不会傻傻地顺着原计划继续,而是把这棵树上相关的子任务标成 aborted,然后重新分配资源。规划本身也变成了一种可以评估的产物:如果连续三轮修订,目标节点却没有任何推进,说明这个 Agent 在"原地换姿势",应该触发终止条件。计划不是一开始就承诺的路线,而是随着证据不断修正的临时假设,这是它和传统任务拆解的本质区别。
3.5 Hypothesize:把"试错"变成"实验",让循环的每一步都留下知识
大多数 ReAct 实现在遇到不确定性时,动作就是"再乱调一次工具试试"。Ralph Loop 喜欢把这步包装成显式的假设检验:先写下"前提假设",再定义"执行动作",接着给出"预期结果",最后等实际结果出来,结论要么接受假设、要么拒绝假设。
还是拿运营分析举例。Agent 怀疑"本周新增下降是投放渠道缩量导致的",它会先把这句话写成一个假设,再设计一个查询动作:拉取各渠道投放量和新增用户的时间序列。如果数据支持,假设升级为结论,写入经验库;如果不支持,它就得重新提出下一个假设。这样一来,循环里的每一次试错都在积累信息,而不只是消耗 Token。循环收敛得快不快,很大程度上取决于假设提得准不准——这又反过来依赖经验库里的历史卡片能不能给出候选假设。所以五个步骤是咬合在一起转的,单个步骤再快,链条断了也白搭。
4. 从 ReAct 迁移到 Ralph Loop:一次完整实战的四个步骤
4.1 案例背景:一个单轮问答的运营看板 Demo 为什么撑不住
我手上的模拟项目是一个运营看板 Agent,最早就是纯 ReAct:用户问一句"为什么本周新增用户下降",Agent 调两三个查询接口,输出一段分析。Demo 阶段一切正常,一旦给真实业务用,问题立刻暴露。用户不会只问一次,他会追问"对比上周呢""拆到渠道看呢""和投放数据对一下呢",每一轮追问都是一次全新的任务,Agent 每次都重新摸索。
更麻烦的是两类错误反复出现:日期格式解析错了、指标别名查不到。每换一个会话,同样的错误就再来一遍,因为它没有跨会话记忆。这个场景天然适合 Ralph Loop:任务目标持续存在、需要多渠道取数、错误模式高度重复。我于是把迁移分成四步,每一步都刻意控制范围,避免一口吃成胖子。
4.2 第一步:先加 Trace,再谈循环
我犯过的第一个错误,就是跳过了追踪直接上循环。结果模型每轮都在制造新上下文,我却根本不知道它在干什么,出了问题连回溯的抓手都没有。所以迁移的第一步,不是改循环,而是把 ReAct 现有的每一条 Thought / Action / Observation 都加上结构化日志,输出成 JSON Lines 文件,每条包含时间戳、动作名、参数、返回摘要、Token 数。
这一步做完,我会拿过去一周的真实问题回放一遍日志,统计出任务失败时最常见的断点。那次统计让我看清了两件事:第一,大多数失败发生在日期解析和指标别名解析两个环节;第二,模型经常在拿到工具返回后"二次化简",把关键数字弄丢。这两条结论直接决定了后面的经验库应该先写哪类卡片。没有追踪,这些全是不可见的损耗。
4.3 第二步:用一个最轻量的经验库,先装两条验证过的卡片
经验库一开始不要做得重。我用 SQLite 建了上一节那张卡片表,检索先用场景标签加关键词过滤,没上向量检索。原因很简单:早期卡片只有几十张,关键词足够用,向量检索属于过度设计,反而让调试链路变长。
存入的第一张卡片就是日期解析规则,第二张是"销售指标查询前必须先对齐指标字典"。这两张卡片都附上了来源任务 ID。我在这里用的策略是"先手动种三张高质量种子卡片,再看自动沉淀效果"。种子卡片能让 Agent 在第一轮迭代就表现出明显进步,团队才有信心继续推进自动反思,否则一上来就全自动,经验库会被噪声淹没。
4.4 第三步:把"一次回答"改成"一个受控循环"
循环骨架我用一段伪代码来说明,具体框架可以替换成你手头的那套:
def run_agent(goal, max_iterations=8, token_budget=20000): context = build_initial_context(goal) for i in range(max_iterations): if token_budget <= 0: break plan = planner.plan(goal, context) for step in select_next_steps(plan): obs = executor.execute(step.action, step.args) context.append(step, obs) lessons = reflector.reflect(goal, context, last_plan=plan) for lesson in lessons: experience_store.add_or_update(lesson) relevant = experience_store.retrieve( scene=goal.type, top_k=5 ) context = rebuild_context_with_cards(context, relevant) token_budget -= estimate_tokens(context) if is_converged(goal, context): break return synthesize(goal, context)这段结构里最值得一说的是rebuild_context_with_cards。它不是简单地把经验卡片塞进上下文,而是把"当前轮观察"和"历史经验卡"重新编排,裁剪掉已经过时的中间推理,控制上下文长度。否则每轮追加,几轮之后窗口就爆了。这是循环能持续转下去的前提:上下文必须是"可重建的",而不是"只增不减的"。
4.5 第四步:终结条件要同时看三样东西,缺一不可
循环最怕的就是"不知道该什么时候停",所以终结条件我同时挂了三个开关:
- 目标达成且置信度足够:Agent 给出的最终分析能引用到本轮或历史轮的 Observation 原文,而不是纯推理;
- 震荡检测:连续三轮计划修订但目标节点没有推进,或最新结论和上一轮结论完全相反,直接停;
- 硬预算:迭代次数上限和 Token 总预算,任何一个触顶就退出,并且把"未完成"状态写进结果。
在实现里,我用一个is_converged函数封装这三个条件,返回结果是"收敛 / 超时 / 震荡"中的一种。这里有个经验之谈:收敛判定一定要绑定"原始问题有没有被回答",而不是绑定"模型还想不想继续探索"。因为循环做深之后,模型会表现得像个好奇心过剩的实习生,发现一个相关指标就想再挖一层,越挖越远。终结条件的作用就是不断把它拽回主线。
5. 上线之后踩过的坑:震荡、重复,还有"迭代成瘾"
5.1 坑一:循环最大的风险不是死循环,而是"永远在发现新问题"
我的系统上线第一周就出现过一个经典案例:用户问"销售额为什么下降",Agent 已经找到了主因,却在最后阶段发现一个异常渠道,于是主动加了一个深挖任务。结果深挖出来的信息又引出两个新疑问,循环一口气跑了 9 轮才被预算拦住,费用翻了三倍,用户拿到一份信息过载到没法读的长报告。
问题出在终结条件只判断了"有没有发现新问题",没有判断"原始问题是否已经回答"。后来我把收敛标准改成三层递进:原始问题是否回答、答案是否有观察支撑、是否有未验证的关键假设。前两层通过就直接收敛,第三层如果存在,也只允许再跑一轮去验证,不让它无限扩散。这个改动把平均迭代轮数从 5.8 压到了 3.1,回复时长下降一大截。
5.2 坑二:伪经验污染,反思出来的"规则"不一定是真的
经验库上线之后的第二个坑更隐蔽。某次 Agent 在复盘时把一次失败的因果判断错了,写成了一张"规则":遇到渠道数据为空时,一律视为投放暂停。这张卡片被后面的任务检索到,连续误导了四五个查询。我回查发现,那次任务的数据为空其实是取数权限过期,根本不是投放暂停。
这就是为什么我坚持两件事:第一,卡片必须带来源任务 ID,复盘模型不能改写原始 Observation,只能引用;第二,新卡片一律是 candidate,只有"相同解法在另一个任务中再次验证成功"才升级为 verified。检索时 verified 卡片的权重是 candidate 的三倍。这等于给经验库加了一道天然的质量闸门——机器学习都知道不能拿单条样本训模型,经验库凭什么拿单次反思定规则。
5.3 坑三:计划版本不一致,执行器还在用旧计划
还有一类问题出在计划和执行的衔接上。planner 每轮都在修订任务树,但 executor 如果还拿着上一轮选出来的步骤执行,就会出现"新计划说查 A,执行器却在执行旧的 B 查询"这种精神分裂现场。表现出来就是循环明明在转,推进的却全是无效动作。
修法是把计划做成带版本号的数据结构,每一次修订都生成新的 plan_version,executor 只能从当前版本的任务树里选节点执行,选完就在节点上标记 running。执行结果回来之后,先回填到这个节点上,再触发下一轮 planner 修订。这样计划、执行、回填三者就形成了闭环,不会出现各跑各的情况。调试这类问题也简单:只要看两个相邻迭代日志里的 plan_version,就能确认是不是执行器滞后了。
5.4 调试必须能"回放":把每次迭代录下来,对比两轮之间的变化
最后一个经验是关于调试方式的。我给系统加了一个 trace replay 功能:每个任务跑完,能按迭代轮数逐帧回放,对比第 3 轮和第 5 轮的 context、plan、反思结果。有了回放,很多问题一眼就能看出来:要么是反思产出太泛,下一轮根本没用上;要么是模型在同一类报错上反复打转,说明经验卡片没被正确检索到。
一个小技巧是强制模型在做 Observation 摘要时引用原文,禁止用"返回了一些数据"这种说法。这样回放时我能直接看清它到底"看没看到"关键数字。实践下来,这个细节比任何调参都更能提升循环的稳定度——因为大多数"看起来在打转"的情况,其实都是模型在没看清证据的情况下强行推理。
6. Ralph Loop 之后:从单个 Agent 到 Agent 集体的迭代
6.1 个体迭代升级为群体迭代:经验库变成公共设施之后
Ralph Loop 把经验沉淀到独立经验库之后,一个很自然的延伸就是让多个 Agent 共享这个库。不同 Agent 负责不同场景,但可以把各自的验证成功卡片贡献到公共区。我试过之后发现收益明显,但也带来了新问题:卡片多了会互相打架,A 场景验证的规则可能在 B 场景完全不适用。
所以我给公共经验库加了"来源场景 + 适用场景"双标签,检索时优先命中同场景卡片,跨场景卡片权重降级。卡片不是越全越好,而是越"分得清边界"越好。这一步做好了,整个 Agent 系统才有了"越用越聪明"的集体记忆,否则只是把噪声放大几倍而已。
6.2 评估体系必须跟得上:没有标尺的循环只是空转
加了循环之后,最容易被忽略的是评估体系。我发现团队一开始都忙着调反思模板、调计划策略,却没人说得清"变好了还是变坏了"。我后来建了两套指标:离线指标在固定任务集上跑,算完成率、每任务成本、平均迭代轮数;在线指标看真实用户场景里的任务完成率、人工介入率、单任务费用。两套指标一出来,很多感性判断立刻被打脸。
比如离线集上反思模板改了三个版本,完成率几乎没动,但单任务 Token 成本涨了 12%,那这个改动就该回滚。循环结构最怕"看起来在努力":迭代轮数不是指标,成本也不是指标,任务完成率和单位成本才是。没有标尺,持续迭代就只是持续空转。
6.3 范式不是银弹:循环深度要对着任务复杂度来调
最后说一句可能不太讨喜的话:不是所有任务都应该上 Ralph Loop。我给任务分了三档:单轮问答、不需要多步证据、错误模式不明显的,继续用 ReAct 或干脆直接让模型回答,成本最低;需要 3 到 5 步工具调用、错误可能重复的任务,上一圈浅迭代就够,比如 ReAct 加一个简单的反思收尾;只有长期目标、跨会话积累经验的任务,才值得完整上 Ralph Loop。
我自己现在的原则很简单:让循环的深度等于任务的不确定程度。不确定低的任务,循环本身就是一种浪费;不确定高的任务,才需要用迭代换稳定。把这条原则想清楚,再回看 ReAct 到 Ralph Loop 的演进,你会发现它不是一个取代关系,而是一条光谱上的两个坐标点——你要做的,是给手里的 task 找到它在光谱上的位置。
最后再分享一个小习惯。我以前设计 Agent 的第一反应是"怎么让它回答得更聪明",现在我会先问自己:这个 Agent 在持续服务的过程中,最需要积累哪一类经验?把这个问题想清楚,再决定要不要上循环、上多深的循环,架构基本就不会走偏。另外记住:循环的出口和入口一样重要,能优雅终止的循环,才配叫持续迭代。