先说结论这类事故十有八九不是模型“变笨了”也不是平台乱扣费而是智能体自己在运行过程中把“失败重试”和“任务拆分”这两件事叠出了雪崩。我处理过一个故障单一个看起来再普通不过的智能体任务用户只是让它“整理一下最近的待办事项并生成周报”结果跑完一看账单账户一次性被扣掉四万七千多积分。按这个平台的计费模型这相当于在几分钟内调用了几百次中高规格的模型接口甚至超过了某些中小团队一个月的用量预算。最离谱的是这个任务最终没有给用户返回任何有效结果——所有积分在无数次内部尝试、拆分、再尝试中被悄无声息地烧光了。这篇文章就复盘这个案例。我会把排查过程、真正的根因、以及后来做的加固方案完整记录下来。无论你是做智能体应用开发、接大模型 API 做自动化还是只负责运营一个 AI 客服机器人看完应该都能避开同一个坑。1. 事件还原一次“常规操作”如何变成账户刺客先说背景。这个智能体运行在某个通用大模型平台上对外表现为一个带工具调用的对话助手。用户可以要求它查询数据、调用内部接口、整理文档。平台计费按“积分”走而积分最终对应的是模型输入输出 token 数。正常情况下一个类似“查待办并写周报”的任务总消耗应该在几百到两千积分之间。那天下午一个测试账号发起了任务。从外部看用户只发了那一句话然后等了十几分钟没响应最后会话超时。正当我们准备按“偶发故障”处理时看了一眼用量明细所有人都不说话了——这一个会话消耗了 47,000 积分单会话消耗是正常值的 20 倍以上。1.1 从账单反推先把异常量化接到报警后我做的第一件事不是看代码而是把积分账单导出来按会话 ID 聚合。这一步很重要没有用量明细后面所有分析都是猜。账单显示这个会话一共产生了 87 次模型调用。其中有 23 次是正常规划11 次是子智能体发起的调用剩下 53 次都是同一类工具失败后的重试和错误处理调用。token 用量从第一轮的 1,200 左右一路涨到最后一轮的 21,000 多。最后一轮单次调用的消耗已经相当于前 10 轮之和。这组数字透露了两个关键信息一是调用次数彻底失控二是单次调用的 token 数在持续膨胀。前者说明智能体的“停止条件”失效了后者说明每一轮新的调用都在携带越来越大的上下文。1.2 先别急着骂计费看看内部发生了什么很多人遇到这种情况第一反应是“平台是不是乱扣费”。坦白说平台计费确实可能存在边界不清晰的问题但这次账单的单位成本并没有异常——每个模型的单价是固定的多出来的全是调用量和 token 量。换句话说问题不在“单价”在“用量”。智能体就像流水阀模型接口是水表智能体编排层是阀门而那天阀门坏了水流到一个小时都没停。在动任何代码之前我先写了一条排查铁律任何一次异常消耗都要从 trace 开始查而不是从费用开始查。费用只是结果trace 才是过程。后面我会详细展开当时的排查路径这里先说根因。2. 根因拆解智能体烧积分最常见的四种死法智能体烧积分不是单一原因而是几类问题的叠加。我复盘了多次这类事故基本逃不出下面四种模型。2.1 无界循环调用失败重试没有上限这是最常见的一种。智能体调用工具接口接口返回异常或超时智能体为了“努力完成任务”会反复调用同一个工具。每调用一次它都会把“之前的完整对话历史工具返回的错误信息自己刚才的分析”全部打包发给模型。失败越多次上下文越长单次调用费越贵。这个场景在代码里表现为while not success: result call_tool() # 没有最大重试次数没有退避策略真实代码当然不会这么直白但编排层如果没设max_attempts效果就是一回事。我们这次事故里同一个工具被重试了 30 多次而且每次重试前智能体还会额外调用一次模型“分析错误原因”——等于一次失败变成两次调用。2.2 多智能体协作的任务爆炸拆分没刹住车当智能体发现自己搞不定时会尝试把任务拆给子智能体。这本来是好设计但如果拆分逻辑没有限制就会出大问题。这次事故中的智能体遇到一个“工具返回数据格式无法解析”的错误它没有直接上报失败而是决定把这个解析任务拆成三个子任务分别交给三个子智能体。每个子智能体拿到任务后又各自把完整的对话历史复制了一份作为上下文。其中一个子智能体遇到同一个解析错误又继续往下拆了一层。每一层拆分都会新增 3 到 5 个调用节点而每个节点都带着前面所有轮次的完整历史。最终一次简单的工具解析失败变成了一棵 37 个节点的调用树。这在编排类智能体里是典型的“循环变递归”事故不是无限循环在同一层而是无限递归在往下炸。2.3 上下文滚雪球没有裁剪只有追加第三个加速器是上下文管理。很多智能体实现里下一轮请求会把“系统提示词历史对话工具定义上一轮输出”原封不动地拼进去。单看一轮没问题但如果前面已经跑了二十多轮这个字符串会越来越长。在我们的账单里最后一轮调用的输入 token 是 18,000 多其中至少有 7,000 个 token 是完全冗余的——都是被多次重复塞入的旧工具返回报文。更气人的是其中两次工具返回的是同一个错误 JSON只是时间戳不同。相当于同样一段垃圾信息被完整地携带了十几轮每轮都付了一遍钱。2.4 计费认知偏差把“有返回”当成“没花钱”还有一类根因藏在开发者认知里。我们一度怀疑是不是平台把失败的请求也算成完整调用但后来发现不是。真正的陷阱在于当工具调用成功返回了一小段 JSON 时很多人会下意识认为这次“没花什么钱”但实际上工具返回的内容会进入下一轮模型上下文每一轮都在变相加价。工具调用越频繁、返回内容越冗长上下文就越臃肿最后消耗的是指数级增长的钱。这次事故就是四种因素叠加的完美风暴一次工具返回了畸形 JSON触发了无界重试重试触发任务拆分拆分导致上下文滚雪球最终四万七千积分灰飞烟灭。3. 排查实录我是怎么一步步锁定真凶的问题的有趣之处在于事发时系统日志里全是“看起来正常”的条目。如果你只看结论不看过程很容易误判为“平台波动”或“用户操作失误”。下面是被验证有效的排查路径。3.1 从用量明细反查建立调用树我首先拿到的不是日志而是平台提供的调用记录。每条记录里有 request_id、会话 ID、父请求 ID、token 用量和模型名称。其实这里的“父请求 ID”就是天然的分层依据我们可以用它重建整棵调用树。我把数据导入本地做聚合先按会话 ID 分组再按父请求 ID 建立树形结构。聚合结果很清晰根节点是初始任务下面第一层只有 6 个节点第二层变成了 19 个第三层 8 个第四层 4 个。从第二层开始几乎所有节点都标记着同一个工具名称——就是那个返回畸形 JSON 的老数据服务。这一步回答了一个关键问题钱不是一次性扣掉的而是随着调用树的膨胀逐步累积的。3.2 逐轮回放找到第一滴血调用树只能告诉你结构不能告诉你动机。为了弄明白“为什么智能体要这么拆”我随机挑选了三条完整链路逐条在本地重建对话消息序列。回放之后“第一滴血”的位置浮现了某工具接口在收到正常请求后返回了一个 HTTP 200但响应体是一个截断的 JSON——缺少右括号且里面有非 UTF-8 字符。智能体的工具调用解析器拿到这个响应后抛出了JSONDecodeError。关键来了这个解析异常被包装成了“工具执行失败”消息重新喂回给模型。模型拿到“工具失败”信号后做了一个看似合理的决定它把“解析这段数据”拆成了几个更小的子目标并分别生成新的调用计划。但它没有意识到那个畸形 JSON 本身是无法通过任何二次解析修复的垃圾数据。于是子任务继续失败继续拆继续递归下去。3.3 识别放大器重试、复制与无预算检查找到第一滴血后我开始找放大器。日志里暴露了三个致命的实现细节重试时没有把同一个错误信息去重导致模型每一轮都重新解析同一个错误文本浪费大量 token 来分析一段已经看过的坏数据。子智能体初始化时会自动继承父会话的全部历史消息而不是只接收精简后的任务摘要。它相当于每次复制整本聊天记录。整个过程中没有任何“积分余额预检”或“单任务消耗上限”。系统在烧到三万多积分时仍在正常调用——不是平台仁慈而是我们自己没设置红线。说实话看到第三个细节时我是有点脸红的。我们给接口层做了鉴权、限流却忘了给模型调用的钱包上锁。3.4 对照试验用最小复现验证结论排查的最后一步不是靠推理而是靠复现。我把测试环境里那个老数据服务的真实响应替换成一个合法的 mock 响应重跑同样的任务消耗立刻降到 600 积分以内。再把这个坏 JSON 挂回去重跑消耗再次冲到四万多。这一对照试验足以锁定根因。那次复现之后我在笔记本上写了一行加粗备忘智能体不会自己省钱它只会完整地执行你的设计。如果你不设置边界“努力完成任务”就是最大的烧钱行为。4. 修复与加固给智能体装五道保险找出根因只是第一步。当时我担心的是就算修好这一个工具接口下次会不会有另一个工具返回同样的坏数据答案是“一定会”。所以完整的修复方案不应该是“修好某个接口”而应该是“让任何单点异常都烧不掉预算”。4.1 硬性消费上限与预算中间件第一件事给每次会话设置硬性消费上限。我们是在编排层加了一个预算中间件在每次模型调用前先做一次预估计费检查把“当前上下文 token 数 × 系数 预估输出 token 数 × 系数”换算成积分。如果本次调用后的累计消耗超过预算阈值我们当时设的是 5,000 积分直接终止会话并返回用户“任务过于复杂请简化需求”。预算中间件的关键在于阈值不能太大也不能太小。太小会让正常长任务无法完成太大起不到保护作用。我们的做法是统计过去一周正常任务消耗的 P95 值然后用 P95 的 2 倍作为硬顶既不影响正常任务又能拦截异常。4.2 循环与递归的双重保险代码层的修复同样重要。我给所有工具调用包了一层统一的调用器强制加入两个参数max_attempts最大重试次数默认 2backoff_factor退避系数每次重试前等待时间指数增长。更重要的是给重试逻辑加了一个“错误指纹”去重机制如果两次重试遇到的是同一个错误消息哈希值相同第三次直接放弃不再向模型发送错误内容。这个小小的改动立竿见影——它让模型不会被同一块垃圾数据反复折磨。然后是多智能体深度限制。我在任务描述和编排层同时加了“最大子任务深度 3”的约束。任何子智能体发现自己已经处于第三层时不再继续拆分而是把未解决问题的报告原样返回给父任务由父任务决定是放弃还是以简化方案兜底。4.3 上下文裁剪与摘要化上下文滚雪球的解法不是简单截断而是“分层摘要”。我的做法是历史消息队列只保留最近 6 轮完整消息6 轮之前的内容每 3 轮合并成一个摘要节点同一工具的错误信息只保留最新一条每次进入子任务时不继承完整父历史而是只传“父任务摘要 当前目标”。这里有个小经验摘要化不要用另一个模型去总结成本高且延迟大。更好的做法是直接丢弃那些“对当前目标无帮助”的历史工具报文只保留最终的结论列表。简单粗暴但省掉的 token 非常可观。4.4 全链路 trace 与告警最后是观测层。我们把记账系统日志升级成了结构化 trace每个请求带一个 trace_id每个节点记录输入 token 数、输出 token 数、工具名、错误标识和父节点 ID。每次调用结束后上报到指标系统实时累加当前 trace 的积分消耗。接着配了两个告警规则单 trace 累计消耗超过 5,000 积分触发 P2 告警单个工具在 1 分钟内有超过 10 次失败时触发 P1 告警。告警的价值不在于事后看报表而在于事发时能主动打断。现在哪怕再出现类似事件我们能在烧到 8% 预算的时候收到通知并自动熔断。4.5 回归测试模拟坏数据不再成为灾难修完上面几层后我专门写了一个“故障注入”测试用例随机把某个工具的返回体改为损坏的 JSON然后跑完整流程。测试通过的标准是整个过程最多消耗 1,200 积分并且系统能在 30 秒内返回用户“暂时无法获取数据”的替代方案而不是无限重试。这个用例现在被固化在发布流水线里。每次改动编排逻辑必须跑一遍故障注入才能合并代码。这比任何代码 review 都更能防住类似的回归问题。提示这类测试环境必须用 mock 工具不要打真实接口。所有故障注入只发生在编排层以下避免污染线上数据。5. 上线前防患于未然四个必做的检查项一次事故治好不难难的是保证下次新项目不再犯。我后来把这类事故的预防总结成四个上线前必做的检查项每次新开智能体项目都会过一遍。5.1 用“最大消耗”而不是“平均消耗”做容量规划团队做预估时经常会问“正常任务消耗多少积分”然后按平均值配置预算。但智能体应用的特殊性在于异常任务的消耗可能是正常任务的 20 倍以上。所以容量规划必须用异常场景来测试故障注入、坏数据、慢接口、并发翻倍这四个场景跑出来的消耗峰值才是配额的门槛。5.2 给平台计费规则做一个映射表很多平台不会直接告诉你“多少 token 等于多少积分”而是有一整套复杂的计费公式。我建议动手写一个小工具每天统计真实调用次数、输入 token、输出 token 和积分消耗反推出平台的计费因子。有了这张映射表你才能在“预算中间件”里准确做预估。如果不做这一步硬顶阈值形同虚设。5.3 明确降级路径拿不到数据就直说智能体和传统接口最大的区别是它天生倾向于“想办法完成任务”而不是“承认失败”。这本身是优点但在生产环境里必须有边界。我建议每个智能体都内置三条降级路径工具数据缺失时直接生成“数据暂时不可用”的回复不触发额外工具调用第一次重试失败后向用户确认是否继续而不是自动重试超过预算阈值时返回“需要人工介入”的提示并附上 trace_id 供技术排查。这三句话听起来朴素但能把绝大多数失控场景拦在源头上。你只需要让“失败时说什么”和“失败时做什么”一样具体。5.4 留一个物理开关所有自动化保护都依赖代码正常执行那如果代码本身就 bug 了怎么办所以必须留一个物理开关在平台控制台或者运营后台随时可以一键停用某个智能体或某个工具接口。我们这次事故里最被动的时刻就是眼看着积分在烧但没有一个按钮能立刻让智能体停下来。只能等会话超时或手动改配置重启。现在所有智能体项目上线前都必须验证“一键熔断”功能拨动开关后所有正在运行的会话最多再执行一个模型调用就会终止。这是最后一道也是最可靠的一道保险。6. 写在后面积分烧掉的不是钱是信任其实四万七千积分折算成钱对一家公司来说未必是伤筋动骨的数目。真正让我觉得必须复盘到底的是这件事暴露出的工程问题我们构建了一个看起来很智能的系统却没有给它配上最基本的“安全边界”。如果把智能体比作一个刚入职的新员工它的能力很强态度也很好愿意加班但你不给它讲清楚“哪些事绝对不能做”和“做到什么程度必须停下来汇报”它就真的会为了整理一份周报把公司全年的水电网费都烧光。这个类比虽然夸张但那天我盯着那串账单数字的时候脑子里就只有这个画面。经此一役我给自己和团队立了一条规矩每做一个智能体不仅要写“它能做什么”的场景列表更要写“它在什么条件下必须停下来”的终止条件清单。后者数据上体现为预算上限、重试次数、递归深度、上下文窗口、熔断开关。没有这些东西的智能体无论模型多聪明都只是一台没有刹车的跑车。最后再分享一个操作层面的小技巧排查此类问题时优先看“失败路径”而不是“成功路径”。所有异常消耗的入口几乎都是失败重试、错误分析、异常拆分和重复填充错误上下文。先定位第一条失败发生在哪一步然后问自己“如果停在这里会怎样”答案通常会直接指向那行缺失的防御代码。