面试日记 第 23 天
面试官翻简历,翻到一半,手指停在项目经历那一栏。
"物流查询 Agent,你负责开发和上线?"
"对,跟了快一年。"
她把简历放下,问了今天的第一道题。
Agent死循环问题有遇到过吗?如何解决?
我答得很快:"说实话,真没遇到过。我们提示词管得比较严,上线半年,没出过一次死循环。"
她没接话,把笔记本转过来,屏幕上是一张监控后台的截图。
"你们系统的兜底触发记录。"她手指顺着日志往下滑,"强制终止,47 次。最近一次,上个月。"
我盯着那行字看了两秒。
"......好吧,遇到过。"
她往椅背上一靠:"那就从头讲。怎么死的,怎么救的。"
我把那次事故原原本本交代了。
我们那个 Agent,正常流程是"用订单号查物流单号 → 用物流单号查物流详情 → 返回结果"。上线之后发现有一定概率会卡死:订单号给到大模型,模型拿到了物流单号,再把订单号和物流单号一起喂回去,模型又返回一个物流单号------既不调用查物流详情的工具,也不返回终止标识。Java 侧就一直循环调用,直到超时。
"原因呢?"
"分析下来三个:提示词没有明确状态流转规则、缺少步骤追踪机制、没有兜底保护策略。"
"怎么改的?"
"三个改造。"我竖起手指,"第一,提示词里加状态约束,明确告诉模型:已经拿到物流单号就直接调用查详情接口,不许重复生成单号。第二,加状态变量追踪,用 step=1/2/3 跟踪执行到哪一步,每次调用模型把当前状态带上。第三,设兜底,最多调用 5 次,超过强制终止;同时比对每次返回的内容,连续两次一模一样就直接判死循环,触发降级。"
她听到这里,身子往前倾了一点。
"你说状态变量追踪。"她问,"模型每次调用不是无状态的吗?你这个状态,存在哪?"
这题我接住了:"存在调用侧,不在模型里。每次调用模型,我们把当前状态作为输入的一部分传进去,比如 current_step: 2,completed_steps: 查询订单, 获取物流单号。模型看到这个就知道自己该干什么。本质是用外部存储弥补模型无状态的缺陷,LangChain 的 Memory 组件也是这个思路。"
"那被兜底强制终止的会话呢?"她追问,"总不能直接给用户返回'任务超时'吧?"
"这个......"我顿了一下,"我们会尽量返回中间结果。比如物流详情没查到,但单号拿到了,就告诉用户'已查询到物流单号,详情获取失败请稍后重试'。同时记日志,系统性问题会触发降级,临时切到规则引擎走固定流程。"
她听完,把电脑合上。
"47 次兜底,说明你的三层防护拦住了 47 次事故。"她说,"这不叫没遇到过,这叫遇到过、解决了、还上了保险。下次面试,第一句话就该这么答。"
我把这句话记在了本子上。
回答重点
遇到过,这是 Agent 开发里非常典型的问题。
我们当时做的是一个物流查询 Agent,正常流程是"用订单号查物流单号 → 用物流单号查物流详情 → 返回结果"。但上线后发现有一定概率会出现死循环:订单号给到大模型,拿到了物流单号,再把订单号和物流单号一起给到大模型,模型又返回一个物流单号,没有调用查物流详情的工具,也没有返回终止标识。Java 侧就一直循环调用,直到超时。
分析下来主要是三个原因:提示词没有明确状态流转规则、缺少步骤追踪机制、没有兜底保护策略。

我们做了三个改造:
1)提示词里加状态约束,明确告诉模型"如果已经获取到物流单号,直接调用查物流详情接口,不要重复生成物流单号"。
2)加状态变量追踪,用 step=1/2/3 这种标识跟踪当前执行到哪一步,每次调用模型时把当前状态带上,模型按状态执行对应动作。
3)设置兜底机制,最多调用 5 次,超过就强制终止。同时检查每次返回的内容是否有变化,连续两次返回一样的内容直接判定为死循环,触发降级逻辑。
扩展知识
为什么大模型容易陷入死循环
大模型本质上是一个"输入→输出"的映射函数,它没有真正的状态记忆能力。每次调用都是独立的,模型只能从当前输入里推断应该做什么。
当输入信息复杂或者有歧义的时候,模型很容易混淆。比如同时给它订单号和物流单号,它可能把物流单号当成"之前的输出"而不是"下一步的输入",然后重复之前的动作。这就是所谓的语境短视问题。
另一个常见原因是工具调用的返回格式不一致。有时候工具返回成功但数据为空,有时候返回错误信息,模型不知道怎么处理就开始重试,一重试就停不下来。

防死循环的系统设计
生产环境的 Agent 必须有完整的防死循环机制,主要从四个层面设计:
1)调用次数硬限制,这是最基础的兜底。设一个最大调用次数,比如 10 次或者 20 次,超过就强制终止返回"任务超时"。LangChain 里有个 max_iterations 参数就是干这事的。
2)语义重复检测,记录每次模型输出的关键信息,比如工具名、参数、返回值。如果连续 2-3 次输出完全一样,大概率是死循环了,直接中断。可以用简单的字符串比对,也可以算 embedding 相似度。
3)状态机管理,把 Agent 的执行流程建模成状态机,每个状态只能转移到特定的下一个状态。比如"查询物流单号"状态只能转移到"查询物流详情"或"返回错误",不能转回"查询物流单号"。这样从架构上就杜绝了循环。
4)超时控制,除了调用次数,还要设总执行时间上限。复杂任务可能每一步都不一样但就是跑不完,这时候次数限制没用,时间限制能兜住。
提示词层面的防护
提示词设计得好可以大幅降低死循环概率。关键是让模型清楚知道三件事:当前在哪、要去哪、什么时候停。

1)明确当前状态,每次调用时把"你已经完成了 xxx,当前需要执行 xxx"写清楚。比如"你已经获取到物流单号 SF123456,现在请用这个单号调用查询物流详情的接口"。
2)定义终止条件,告诉模型什么情况下应该结束。比如"当你获取到完整的物流轨迹信息后,返回 FINISH 标识并输出结果"。
3)处理异常情况,告诉模型工具调用失败怎么办。比如"如果工具返回错误,最多重试 1 次,仍然失败就返回错误信息给用户,不要继续尝试"。
ReAct 框架的死循环问题
ReAct 是目前最流行的 Agent 框架,"思考→行动→观察"的循环本身就容易出问题。
常见的死循环模式是:模型思考后决定调用工具 A,观察到结果,又思考决定还是调用工具 A,无限循环。原因可能是观察结果不够明确,模型判断任务还没完成。
解决方案是在 observation 里加更多上下文,比如"这是第 3 次调用该工具,之前两次的结果分别是 xxx",让模型意识到自己在重复。或者直接在 prompt 里写"如果连续两次调用同一个工具得到相同结果,请停止并返回当前结果"。
监控和报警
线上 Agent 必须配监控。关键指标包括:
1)单次会话的工具调用次数分布,正常应该是长尾分布,如果出现大量 10 次以上的会话就要排查。
2)死循环触发率,被兜底机制强制终止的会话占比,这个指标反映 Agent 的健康度。
3)平均执行时间,如果突然变长可能是死循环增多的信号。
我们用 Prometheus 采集这些指标,Grafana 做可视化,设了阈值报警。死循环率超过 5% 就会触发告警,值班的同学去看日志排查。
面试官追问
追问:你刚才说用状态变量追踪步骤,这个状态是存在哪的?模型每次调用不是无状态的吗?
回答:状态存在调用侧,不是模型里。每次调用模型时,我们把当前状态作为输入的一部分传进去,比如"current_step: 2, completed_steps: 查询订单, 获取物流单号"。模型看到这个就知道自己该干什么了。本质上是用外部存储弥补模型无状态的缺陷,LangChain 里的 Memory 组件也是这个思路。
追问:状态机的方案听起来不错,但实际业务流程可能很复杂,状态机不好维护怎么办?
回答:确实,复杂流程用状态机会很臃肿。这种情况可以用 DAG 来管理,每个节点是一个任务,边表示依赖关系。执行时按拓扑序跑,已经执行过的节点打标记,不会重复执行。LangGraph 就是这个思路,比纯状态机灵活,也比纯 ReAct 可控。
追问:死循环被强制终止后,用户体验怎么保证?总不能直接返回"任务超时"吧?
回答:肯定不能这么粗暴。我们的做法是尽量返回中间结果,比如虽然没查到物流详情,但物流单号已经拿到了,就告诉用户"已查询到物流单号 SF123456,详情获取失败请稍后重试"。同时记录日志方便复盘,如果是系统性问题会触发降级策略,比如暂时切到规则引擎走固定流程。
追问:有没有遇到过不是死循环但执行路径特别长的情况?比如模型绕了一大圈才完成任务?
回答:有,这叫路径爆炸问题。模型可能会调用一堆不必要的工具来"确认"信息。解决思路一是优化提示词,给模型更明确的执行路径建议;二是加工具调用成本的概念,prompt 里写"尽量用最少的步骤完成任务";三是做事后分析,统计每个任务的平均路径长度,长得离谱的 case 单独拎出来优化。
回去我把那 47 条兜底日志又翻了一遍,顺手又看了眼手机里那条新闻------OpenClaw 团队 30 天烧掉 6030 亿 token,原因也是智能体反复循环、每轮把结果重新塞回上下文。只不过他们烧的是钱,我们烧的是超时,本质是同一种病:循环没人掐停。
这道题最容易答错的地方,不是技术方案,而是第一句"没遇到过"。面试官问"有遇到过吗",要的不是一个干净的履历,而是你有没有真的被线上毒打过、打完有没有长出机制。标准答案就一句话:死循环来自模型无状态和提示词缺约束,解法靠提示词状态约束、步骤追踪、次数兜底加重复检测,线上再配监控报警兜住长尾。
完整题解和更多追问已经整理在面试鸭小程序,准备 Agent / 大模型应用方向的同学,建议把这题和监控、降级相关的一组题一起过一遍。