面试日记 第 35 天
我坐下后没铺垫成绩,先把一次线上问题认了:物流查询 Agent 连续调用同一个工具,Java 侧跟着一直循环,最后只能等超时。
面试官抬眼看我:“你先说说,正常链路是什么?”
“订单号查物流单号,物流单号再查物流详情,拿到详情就返回。”我把三步写在纸上,“出问题那次,模型已经拿到了物流单号,下一轮却又生成了一遍物流单号。它没去调详情工具,也没给终止标识。”
她顺着第三步点了一下:“所以你们当时只是看见它重复,还是已经能让它停?”
这个区别最近也有一个很直接的例子:Cloudflare 的本地开发工具会自动捕获本地 Worker 调用的 OpenTelemetry traces,Agent 可以通过 Local Explorer API 查询结构化追踪,定位失败步骤。

“追踪能帮我找到它卡在哪一步,但不会替我终止循环。”我说,“真要敢写已上线,调用侧还得有状态、重复检测、次数上限和降级逻辑。”
她这才把题目写完整:“Agent死循环问题有遇到过吗?如何解决?”
我从那次物流查询的复盘开始答。这个问题不难在解释 Agent 为什么会重复,难在能不能把“看见重复”和“真的停下来”分清楚。监控负责留下线索,终止条件才负责兜底。
回答重点
遇到过,这是 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 单独拎出来优化。