考察点
这道题筛的是「真把 Agent 跑到失控过」的人。面试官想看你有成体系的兜底设计,而不是只说一个 max_iterations。好的回答应该分层:硬性限制是保险丝、循环检测是主动发现、降级策略是失控后的用户体验保障。追问常往「循环具体怎么检测」「误伤正常重试怎么办」「降级之后用户看到什么」走。
参考答案
为什么会死循环
ReAct 循环没有内在的终止保证,模型每一步都在「继续干活」和「给出答案」之间做选择,选错一侧就停不下来。线上常见的诱因有几类:工具报错信息写得太笼统,模型理解不了原因,反复用同样方式重试同一个必然失败的调用;任务目标客观上不可达,但模型不承认失败,一直换汤不换药地尝试;在两个状态之间震荡,A 改到 B、B 又改回 A,coding agent 改 bug 时尤其常见;还有就是模型能力不够,反复「思考」却没有有效行动。
第一层:硬性迭代与预算限制
最基础的保险丝是最大迭代次数,LangGraph 的 recursion_limit 默认就是 25,自己写循环也得有 max_steps。但单维度限制不够,我一般同时挂三个维度:总步数、token/成本预算(按各模型单价实时累加)、wall-clock 超时。工具层面也要限,同一个工具在一次任务里最多调 N 次,防止单个工具被薅穿。注意超限时不应该直接抛异常结束,而是触发降级路径——这是很多人只做到一半的地方。
第二层:循环检测
硬限制是被动的,循环检测是主动提前发现问题。实用的检测手段有三种:
- 完全重复检测:维护最近 k 步的 (tool_name, args_hash) 记录,同一个工具加同样的参数出现 2 到 3 次就判定循环。逻辑是幂等的查询类工具,参数不变结果就不会变,再调一次纯属浪费。
- 状态震荡检测:对每步的观察结果做 hash 或 embedding,序列里出现 A-B-A-B 的周期模式时打断。
- 无进展检测:连续 n 步没有产出新的有效信息。比如 coding agent 连续 n 步文件 diff 为空,说明它在原地打转。
检测到循环后不要简单 kill。一个性价比很高的做法是往上下文里注入一条系统提示,明确告诉模型「你已经用同样的参数调用 X 三次都失败了,换一个策略,或者基于现有信息给出当前最佳答案」。多数情况下模型能自己跳出来,比硬杀的体验好得多。
第三层:降级策略
降级要分级,不要一刀切:
- 提示纠正,就是上面说的注入反思提示,成本最低,先试这个。
- 模型升级,循环时切到更强的模型跑一两步。弱模型打转的任务,强模型经常一步就找到出路,虽然贵但只花在刀刃上。
- 部分结果交付,把已经完成的中间产物整理成回答,明确告诉用户哪部分完成了、哪部分卡住了,附上原因。这比一个超时错误页面强太多。
- 转人工或反问用户,给出当前状态和可选的下一步,把决策权交回去。
工程上有个原则:降级的判断要放在一个独立的 policy 函数里,不要指望 agent 自己判断自己是不是在循环——当局者迷,模型在死循环里时它的「我是不是卡住了」的判断同样不可靠。
线上监控
兜底的最后一环是监控。对每条 trace 记录终止原因——completed、max_steps、loop_detected、timeout、budget_exceeded,这是迭代 prompt 和工具设计的核心数据来源。再盯步数分布:平均步数和 P95 步数突然上涨,往往不是模型变笨了,而是某个工具在悄悄故障,比如搜索 API 挂了返回空结果,模型就陷入「搜了没结果、换个词再搜」的循环。步数异常是我见过的工具故障最早的信号。
可能的追问
- max_steps 设多少合适? 没有普适值。先把限制放宽跑一轮 eval 统计任务步数分布,取 P95 再留 buffer;coding agent 需要的步数通常远高于问答类 agent。
- 重复检测会不会误伤正常重试? 会,要区分:网络抖动导致的同参数重试是合理的,配合单工具重试次数上限放行一两次;分页浏览是「同工具不同参数」,本来就不在检测范围内。
- 成本预算具体怎么做? 按每一步的 token 用量乘模型单价实时累加,接近上限时走降级;也可以把「剩余预算」显式写进上下文,让模型自己规划省着花。