一个字段变更,引发多米诺骨牌
今年3月15日凌晨,东莞一家电子厂的生产系统出了问题。他们用LangGraph搭建的采购AI Agent,在执行多轮比价任务时,突然陷入死循环。24小时内,系统生成了超过12,000条重复的API调用日志,本地消息队列被彻底积压。
问题的根源,是Agent太“尽责”了。它试图自动完成从ERP库存查询到合同初拟的链条。而在3月15日早上8点,其核心依赖的一家供应商的报价API,悄然变更了返回格式——一个本应为字符串的 unit_price 字段,返回值变成了浮点数。
死循环是怎么炼成的
Agent的工作流定义在LangGraph状态图里,其中“价格校验”节点有个断言:检查价格是否为合理字符串。浮点数传来,断言失败。按预设逻辑,Agent会重试三次,这本身没问题。
但重试策略与上游节点耦合了。每次重试,Agent都会重新触发上游的“供应商状态检查”和“ERP库存锁定”API调用。而这两个API在上午时段有流控限制,开始返回“429 Too Many Requests”。Agent的错误处理器将“429”也视为可重试错误。
于是,一个致命循环形成:价格校验失败 → 重试 → 触发上游流控 → 再次校验 → 再次失败……
根据后来拉取的OpenTelemetry监控数据,这个循环在12小时内产生了117,000次span,平均每秒超过2.7次调用。Agent的本地Redis上下文存储很快被写满,整个工作流状态无法持久化,彻底卡死。
监控盲区:传统APM为何失灵?
企业原有的APM监控的是各个微服务的CPU、内存和网络。供应商API网关、ERP中间件这些单点看都是“正常”的——CPU使用率不过30%,响应时间200ms以内。传统APM感知不到业务层面的“逻辑死循环”,只能看到一堆快速产生和消失的短HTTP请求。
真正的警报,是在订单积压报警触发后才开始的。工程师面对巨大的Trace瀑布图,手动将数万条span按 error.type 聚合,才发现“INVALID_RESPONSE_FORMAT”和“429”以惊人的数量交替出现。这个排障过程耗费了超过4个小时。
从死循环中“醒过来”的三道防线
监控是发现问题,恢复机制是解决问题。事故后,我们实施了三层防御:
强制性状态超时与心跳:在LangGraph每个状态节点设置硬性超时(如
timeout_seconds=30)。工作流定期发出“心跳”。状态执行超时或心跳停止超过一定周期,工作流引擎(如LangGraph的PersistentRunner)强制标记其为失败,触发补偿流程,而不是无限重试。错误分级与熔断:将错误分为“可重试业务错误”(如网络抖动)和“致命逻辑错误”(如数据格式错误、鉴权失败)。对于后者,立即触发熔断,停止所有重试,并通知人工。在代码中,可以定义
FatalError异常,在状态图的重试逻辑中捕获它并直接跳转至终止节点。优雅降级与人工接管:当核心Agent流程因外部依赖崩溃时,系统应能切换到降级模式。例如采购Agent无法比价时,可自动根据历史最优价格生成“建议采购单”,等待人工确认。同时暴露HTTP接口(如
/agent/override/{task_id}),允许运营人员通过API强制修改Agent状态。
监控什么:盯住工作流,而不只是函数
我们重新设计了基于Grafana和Tempo的监控仪表盘,核心是监控工作流的健康度,而不仅仅是单个服务的指标。几个关键点:
- 循环检测:在Agent状态节点切换时,记录
execution_id和state_transition。在Prometheus中设置自定义指标,当同一个state在5秒内被进入超过5次,仪表盘就会报警。 - 外部依赖健康度:不仅监控HTTP状态码,还要监控其业务含义。例如为供应商API调用增加
business.valid=true/false自定义属性。比例飙升,即使HTTP 200,也可能意味着数据格式已变。 - 端到端任务完成率:这是最终的业务指标。使用
workflow.task.completed直方图追踪。持续走低的完成率,比任何技术指标都更有说服力。
这家企业的Agent在引入这套机制后,在5月份的一次模拟测试中,成功在30秒内从一个格式错误死循环中自动恢复,并通过降级模式生成了待审订单。
对于构建复杂多步AI Agent的团队,难点已不在于“让Agent跑起来”,而在于如何让它在生产环境中稳定运行,并优雅地失败。一个陷入死循环、疯狂调用API的Agent,其破坏力和成本,可能远超一个简单的程序崩溃。