跳到正文

AGENT PROJECT INTERVIEW · Q03 / 25

ReAct Agent 为什么会死循环?停止条件和步数上限怎么设计

从 Thought、Action、Observation 循环出发,拆解 ReAct 不收敛、重复工具调用、上下文膨胀和误判完成的原因,以及生产系统的停止与降级设计。

作者与复核:吴师兄内容依据:公开 Agent 教程、项目题资料与工程推导查看关联教程 →查看吴师兄公开作品 →

25 道题目Q03
本页内容18 节

30 秒先说结论

ReAct 会死循环,通常不是因为 while 写错,而是系统没有可靠的完成条件、工具反馈不可区分、错误被反复回灌,或模型每轮看不到足够状态。生产系统不能只等模型自行停止,要同时设置业务完成条件、最大步数、最大耗时、Token 与工具预算、重复动作检测、连续失败控制和人工接管。停止后还要区分成功、信息不足、预算耗尽和系统故障,不能都包装成最终答案。

面试官问:“你的 Agent 如果一直调用工具不结束,怎么办?”

最常见的回答是:“设置最大循环次数。”

这句话只能证明你知道怎么止损,不能证明你知道为什么循环。最大步数像保险丝,任务失控时可以切断,但如果目标、工具结果和状态设计没有修复,下一次请求仍然会以同样方式撞上保险丝。

这道题真正考的是:ReAct 每轮究竟在更新什么,系统如何判断完成,错误怎样改变下一步,什么时候应该重试、澄清、降级或转人工。

先给出 30 秒回答

ReAct 的 while 往往没有写错,死循环更常来自系统缺少可靠完成条件、工具反馈不可区分、错误被反复回灌,或者模型每轮看不到已完成状态。生产系统不能只等模型自行停止,要同时设置业务完成条件、最大步数、最大耗时、Token 与工具预算、重复动作检测、连续失败控制和人工接管。每轮都要记录结构化 Action、工具结果和状态变化。停止时还要区分成功、信息不足、预算耗尽和系统故障,不能把所有退出都包装成一段最终答案。

如果只回答“设 MAX_STEPS”,面试官通常会继续问:“那设多少?为什么?到上限以后给用户什么?”没有状态和退出语义,就很难接住。

ReAct 到底循环的是什么

ReAct 把推理和行动放在一个反馈循环里。模型根据当前目标与状态提出行动,系统执行工具,把结果作为 Observation 写回,模型再基于新信息决定下一步。

解释 ReAct 时常用 Thought、Action、Observation 描述这个过程。理解原理时可以这样记,但生产系统不必把模型隐藏推理全文展示给用户。更可控的做法是记录结构化决策摘要:当前子目标是什么、选择了哪个工具、参数是什么、工具返回什么、状态发生什么变化。

以教学场景为例,用户要求比较两座城市的天气。第一轮选择天气工具查询城市 A,第二轮查询城市 B,第三轮发现两份数据齐全,于是进入比较并结束。关键不在调用两次工具,而在每次 Observation 都让“还缺什么”发生变化。

如果系统没有“城市 A 已查询”“城市 B 已查询”这样的状态,模型下一轮可能再次查询城市 A。表面上步骤还在继续,实际上只是在重复读取相似上下文。

ReAct 循环必须同时检查状态和出口

每轮循环都应产生可观察的状态变化,否则继续行动没有依据。

为什么模型不返回工具调用,不能直接等于任务成功

最小 Demo 常用一个简单出口:模型没有返回 tool_calls,就把它的文本当作最终答案。这个规则适合帮助初学者看懂循环,却不够支撑复杂业务。

模型不调用工具,可能有四种含义:它认为任务完成;它不知道该调什么;它缺少参数却没有主动提问;它受到上下文干扰直接生成了一段回答。只有第一种可能是成功,而且仍需业务验证。

例如工具任务要求取得一条有来源的最新记录。模型输出“没有发现相关信息”,不代表任务正确完成。系统要检查是否真的执行过允许的搜索、工具是否成功返回、结果时间范围是否符合要求,以及“无结果”是否是可接受结论。

所以停止判断应由模型信号、工具证据和业务状态共同决定。模型负责表达意图,系统负责确认事实。

死循环的四类根因

根因一:目标和完成条件没有落到状态里

“帮我把资料查完整”“尽量多找一些信息”都没有天然出口。模型永远可以再搜一次、再读一篇、再总结一轮。

改进方法不是只缩小步数,而是把目标拆成可验证字段。例如需要哪些主题、每个主题至少需要什么类型的证据、缺失时是否允许明确标注“未找到”、何时必须向用户确认。

完成条件不一定是标准答案,但必须能判断当前任务处于完成、部分完成、等待输入还是失败。

根因二:工具 Observation 没有清楚语义

工具返回空字符串,到底表示确实没有结果、请求参数错误、服务超时,还是解析器坏了?如果系统统一回灌“没有结果,请重试”,模型最容易做的就是再次调用同一工具。

工具结果至少应包含状态类别、是否可重试、数据或错误摘要、来源和必要的下一步提示。错误堆栈不应原样塞给模型,既可能泄露内部信息,也会迅速撑大上下文。

根因三:每轮看不到可靠的已完成状态

如果系统只把所有对话和工具返回按时间堆进 messages,关键状态会淹没在长上下文里。模型可能忘记之前已经查过什么,也可能把旧任务结果当成当前状态。

应单独维护任务状态,包括已完成节点、关键事实、失败工具、剩余预算和当前等待事项。状态摘要不是为了替代原始 Trace,而是让下一轮决策获得清晰输入。

根因四:错误策略只有“重试”

参数缺失、权限拒绝、资源不存在、短暂超时和服务永久下线,不应该执行同一动作。参数缺失要向用户澄清;权限拒绝要停止或申请授权;短暂故障可以在预算内重试;永久错误应换路或降级。

如果所有错误都进入同一个 Retry 分支,死循环只是时间问题。

死循环通常来自四类根因

最大步数负责最后止损,目标、Observation、状态和错误策略才决定系统能否收敛。

一套完整停止条件应该包含什么

1. 业务成功条件

这是最重要也最容易被忽略的出口。成功应由当前任务的真实状态判断。

查询任务可以要求取得指定字段和来源;代码任务可以要求测试命令通过;数据分析任务可以要求结果表生成且关键检查通过。不同任务必须定义自己的完成证据,不能只依赖一句通用 Prompt:“如果你认为完成了就结束。”

2. 信息不足,等待用户

用户没有提供订单号、日期范围或目标对象时,继续猜测会让错误沿链路传播。系统应进入 waiting_for_user 一类状态,保存已完成工作,提出最小必要问题。

等待用户不是任务失败,也不应该占用后台循环继续尝试。用户补充后,从保存状态继续。

3. 不可恢复的业务失败

权限不足、资源不存在、请求违反规则等错误,通常不能通过换一种说法绕过。系统应停止自动执行,解释原因或进入授权流程。

这里尤其要防止 Agent 把“换工具”当成绕过权限的方法。权限检查必须在所有工具后端统一执行。

4. 运行预算耗尽

预算至少包括最大步骤、总耗时、模型调用、工具调用、重试和上下文。步数只是其中一项。

为什么不直接给一个通用数字?因为查询一个状态与完成一组多步分析的合理长度不同;工具延迟、模型能力和任务风险也不同。阈值要从真实任务分布、服务目标和成本约束中确定,并按任务类型配置。

5. 重复或无进展

即使没有达到最大步数,连续执行相同动作且状态没有新增信息,也应提前停止。

一种教学实现是为 Action 生成指纹:工具名、规范化参数、关键状态版本。如果同一指纹多次出现,Observation 也没有变化,就触发重复检查。真实系统还要允许合理重试,例如第一次超时、第二次成功,因此重复检测要结合错误类型与重试预算。

6. 用户取消或人工接管

长任务必须允许取消。高风险、连续失败或系统无法确定下一步时,可以转人工。取消和接管应作为一等状态写入存储,而不是粗暴杀死进程后丢失全部上下文。

最大步数应该怎么定

最大步数不是凭感觉填一个整数,可以从任务结构反推。

先统计一批真实或教学任务的必要工具链:最短需要几步,正常情况下大多数任务需要几步,发生一次可恢复失败时增加几步。再观察长尾任务为什么变长,是合理探索、工具慢、参数错误,还是循环。

设置上限时要同时考虑三件事:正常任务不应频繁被误杀;异常任务不能无限占用资源;达到上限后用户能得到有意义的状态,而不是一段伪成功答案。

上限可以按任务类型不同。单步查询和多资料分析不必共用同一预算。高风险写操作甚至不应该用“多尝试几次”提高成功率,而应更早进入人工确认。

最重要的是监控上限命中样本。只统计“有多少任务达到上限”还不够,要查看它们是目标太复杂、工具失败、状态丢失还是阈值过低。上限是诊断入口,不是问题修复本身。

工具失败时,到底重试、换工具还是询问用户

可以按错误的责任归属和可恢复性判断。

参数缺失或含义不清

例如用户说“查一下那个订单”,系统无法确定订单 ID。不要让模型枚举猜测,也不要不断调用搜索。应明确询问用户,或在权限允许的候选很少时展示候选让用户选择。

参数格式错误

如果字段类型、枚举或范围不合法,先由 Schema 校验器拒绝。可以把可公开的错误摘要回灌给模型,让它修正一次;连续生成同类错误时停止或使用确定性修复,不能无限让模型试。

短暂超时或限流

在工具允许重试、且动作没有不可控副作用时,可以按工具策略重试。重试使用统一预算,并记录尝试次数。若已有备用工具,可在满足数据与权限要求时切换。

空结果

空结果有时是正确答案,有时是搜索表达不合适。系统要看任务是否允许“未找到”、工具覆盖范围和是否还有独立查询方式。不要默认空结果等于失败,也不要默认换关键词就一定能找到。

权限拒绝或业务冲突

停止自动执行,提示需要授权、人工处理或修改请求。不得通过其他工具绕过。

不同失败要走不同出口

错误语义决定下一步,重试只是有限且受预算约束的一种动作。

怎样记录 Trace,才能定位一次循环

至少为每个任务分配 trace_id,每轮记录:轮次、当前目标、状态版本、结构化 Action、参数摘要、工具结果状态、关键 Observation、耗时、预算变化和停止判断。

不要只保存模型最终输出,也不要依赖向用户展示完整隐藏推理。排障真正需要的是可复现的输入、决策、调用和状态差异。

教学 Trace 可以长这样:

turn=3
state_version=7
pending_goal=读取资料 B 的发布日期
action=read_metadata
args={document_id: B}
tool_status=not_found
state_delta=无新增事实
budget=剩余工具调用 2 次
decision=询问用户确认资料版本

这里没有虚构模型内部长思维链,但足以回答:为什么调用工具、工具发生什么、状态有没有变化、为什么停止或继续。

如何做死循环故障测试

固定任务与基线

准备能正常完成的单步、多步和需要澄清的任务,记录没有故障时的轨迹。基线不是为了追求某个固定步数,而是知道合理流程长什么样。

注入可解释故障

让工具分别返回一次超时、持续超时、空结果、参数错误、权限拒绝和格式变化。写操作只用 Mock 或 Dry-run,不对真实系统产生副作用。

检查出口而不只看最终文本

系统是否按错误类型进入重试、换路、澄清、停止或人工?预算是否减少?重复 Action 是否被识别?状态是否保留?最终状态是否准确标为成功、部分完成或失败?

做相同 Trace 回放

修改 Prompt、模型、工具描述或错误策略后,使用相同输入和工具返回回放。比较行动序列、停止原因和状态变化,确认修复没有让其他任务更早误停。

上下文膨胀为什么也会制造循环

ReAct 每轮都会增加模型消息、工具参数和 Observation。长日志、网页全文或重复错误全部塞回上下文后,模型很难抓住目标和最新状态,调用成本也持续上升。

工具返回应先结构化和裁剪,只保留决策所需字段;大结果保存到外部存储,在上下文中传引用和摘要;任务状态单独维护;历史失败可以汇总为“已尝试的方法及原因”。

压缩也有风险。如果摘要删掉用户硬约束或已完成步骤,模型会重新走旧路。每次压缩都要保留目标、不可违反的限制、已确认事实、未完成事项和来源指针。

并行工具调用让停止判断更复杂

当两个只读工具互不依赖时,系统可能并行执行。并行能减少等待,但也引入新状态:部分完成、部分失败、晚到结果和取消中的请求。

Planner 不能因为第一个结果回来就宣布整个任务完成,也不能在某一路失败后立即重复发起全部调用。状态管理器应为每个工具请求分配 ID,分别记录待执行、运行中、成功、失败、取消和已忽略。汇合节点依据任务规则判断需要等待全部结果、任意一个结果,还是满足最低证据集合即可继续。

如果总预算耗尽或用户取消,系统要向仍在运行的工具发送取消信号;无法取消时,也要把晚到结果标记为不再参与当前决策,避免旧 Observation 在任务结束后重新触发循环。

部分完成不是失败的同义词

多步任务可能取得一部分可靠结果,但另一部分因权限、信息缺失或工具故障无法完成。系统应保留已经验证的事实,列出未完成项和原因,并用明确状态结束。

若只允许“成功/失败”两个状态,模型容易为了给出成功答案而补写缺失信息,或者因为一个次要工具失败丢掉全部有效结果。部分完成状态能让用户决定是否补充信息、授权或稍后重试。

任务结束后还要处理迟到和重复事件

外部工具可能在超时后返回;队列可能重复投递;网络重试可能让相同结果到达两次。结束状态要具备版本和幂等处理:只接受属于当前任务版本、尚未处理的结果。旧事件应写入 Trace,但不能让已完成任务重新进入运行状态。

重试预算要跨工具统一核算

如果每个工具都各自允许多次重试,Planner 再不断切换备用工具,单个任务的总尝试次数仍可能失控。任务层应有统一重试预算,工具层只定义某类错误是否允许重试以及一次尝试怎样退避。

切换备用工具也要消耗预算,并记录为何切换。两个工具若依赖同一个下游服务,换名字并不能解决故障;若返回口径不同,还要重新验证数据是否能用于当前目标。把“换工具”当作免费动作,会把重试风暴隐藏在路由层。

预算耗尽后,系统保留已验证结果和最后错误,进入降级或人工出口,而不是重置计数再开启一轮新的 ReAct。

停止原因必须面向后续动作

max_steps_reached 只是系统原因,用户还需要知道当前已经完成什么、缺少什么、是否可以补充参数、是否需要稍后再试。对运营和工程人员,则要保留工具、错误类别、预算和最后状态,便于定位。

同一个停止事件需要两种表达:对用户是清晰可行动的说明;对系统是结构化、可统计和可回放的状态。不要把内部堆栈直接展示,也不要只留一句模糊的“任务未完成”。

面试官会怎样连续追问

追问一:除了最大步数,还有什么停止条件

业务完成、信息不足等待用户、不可恢复失败、总耗时或 Token 预算、重复无进展、用户取消和人工接管。

追问二:最大步数为什么这样设

根据任务类型的正常轨迹、可恢复失败带来的额外步骤、服务目标和成本确定,并持续审查达到上限的样本,不背通用数字。

追问三:怎样判断重复调用

结合工具名、规范化参数、关键状态版本和 Observation 变化生成指纹;同时参考错误是否可重试,避免把一次合理重试误判成死循环。

追问四:模型说完成了,为什么还不结束

模型声明只是信号。系统还要检查任务需要的业务字段、工具结果、测试或来源证据是否满足完成条件。

追问五:工具一直超时怎么办

在统一预算内按工具策略有限重试,必要时切备用、返回部分结果或转人工;有副作用的动作不能盲目重试。

追问六:怎么复盘一次死循环

按 Trace 回放每轮状态、Action、工具结果和预算变化,判断根因属于目标、工具、状态、错误策略还是上下文,再用同一轨迹验证修复。

ReAct 循环验收清单

能安全停止、准确说明停止原因,与能完成任务同样重要。

ReAct 为什么会停:一段完整回答

ReAct 的循环失控,通常源于任务没有可靠完成条件、工具错误语义不清、状态没有记录已完成步骤,或者所有异常都统一重试,并非一处 while 语法问题。我会把循环控制分成三层。第一层是业务状态,定义成功、等待用户、不可恢复失败、取消和人工接管;模型不再返回工具调用不能直接等于成功。第二层是系统预算,包括最大步骤、总耗时、Token、工具和重试次数,并按任务类型确定,不背固定数字。第三层是无进展检测,用工具名、规范化参数、状态版本和 Observation 变化判断重复动作,同时允许符合工具策略的有限重试。每轮记录结构化 Action、工具状态、状态差异、预算和停止原因,用 Trace 回放定位是目标、工具、状态还是策略问题。工具超时、参数缺失、权限拒绝和空结果分别进入重试、澄清、停止或降级。达到上限后也要准确标记部分完成或失败,不能包装成成功答案。

停止条件是否真的落地:六项证据

如果项目声称解决了循环失控,回答时至少要拿出:

  1. 每种任务的业务完成条件,而不只是模型停止条件。
  2. 步数、耗时、Token、工具和重试预算的配置依据。
  3. 成功、等待用户、失败、取消和人工接管的状态定义。
  4. 一条重复调用 Trace,能指出状态为什么没有变化。
  5. 空结果、超时、权限和参数错误的分流策略。
  6. 一组故障注入回归任务,以及修改前后的轨迹差异。

如果现在只有一个 while TrueMAX_STEPS,就如实说这是最小教学实现。下一步的工程化不是简单把上限调小,而是让系统知道自己完成了什么、缺少什么、为什么继续,以及什么时候必须停下。

INTERVIEW FOLLOW-UPS

面试官会怎样继续追问?

这些追问会继续检查方案取舍、验证方法和项目事实。
  1. Q01

    除了最大步数,ReAct 还应该有哪些停止条件?

  2. Q02

    怎样判断两次工具调用属于无效重复,而不是必要重试?

  3. Q03

    工具连续失败时,应该重试、换工具、询问用户还是直接停止?

  4. Q04

    如何用 Trace 回放一次死循环,并找到真正的根因?

PROJECT CHECK

换成你的项目,这几件事能否说清楚?

  • 定义业务成功、失败、等待用户和预算耗尽状态
  • 为步数、耗时、Token 和工具调用设置服务端预算
  • 记录每轮 Action、参数摘要、Observation 和状态变化
  • 准备重复调用、空结果和参数错误三类故障样本
用自己的项目经历练一次