万级智能体连跑几十小时:先把断点、资源账和失败隔离定下来

一、规模与时长把治理从可选项推到前置位
据公开报道,有团队公开过一次万级智能体的极限实测:一万个智能体同时运行、连续跑上几十小时、每天的 Token 消耗量按万亿计。这三个数字我们只当新闻读,不作为本篇任何判断的依据,也不由此推断模型能力排位或者任务类型,实测的评分与任务构成没有公开细节,任何推论都站不住。但它透露的一个工程事实值得抄下来:当并发单元数进入万级、单次运行时长进入几十小时这个量级,运行治理的许多事项会从上线后再补,变成上线前必须定义。
原因不是模型变差,而是三类放大效应同时出现。第一,小概率事件变成日常事件:单个单元的异常率再低,乘上一万个单元,每天都在发生,在几十小时的重复轮次里还会累积成几百次。第二,长时运行让状态成为瓶颈:进程内的缓存活不过一次重启,而一次重启的成本从几秒变成几小时,于是恢复语义比性能更重要。第三,共享配额让局部错误变成全局事故:一个单元陷入重试循环,可以拖着同租户的其他单元一起排队。
我们做企业级智能体交付时能明显感到客户验收问法的变化。小规模时问的是这个任务的结果对不对,大规模时问的是这一轮有多少单元没跑完、分别卡在哪一步、这些消耗算在谁头上。第二组问题需要一整套事先定义好的东西来回答,事后临时加日志补不出来。所以本篇不谈提示词与模型选型,只谈六项前置定义:状态放哪里、断点怎么续、账按什么维度记、失败怎么隔离、背压放在哪一层、事后能不能回答哪个单元在什么时候因为什么停了一次。
二、状态外置:三类状态各有归属
第一个要拆干净的问题是,一个执行单元死掉之后,什么东西必须还活着。习惯上我们把三类状态混在一起:会话状态、中间产物、外部世界状态。它们的外置方式完全不同,混放是绝大多数续跑续出脏数据的根源。
会话状态指对话历史、当前步骤、已经做出的决策序列。它必须落在共享存储里,并带单调递增的序号,而不是只带时间戳,因为几十小时的运行里会出现时钟回拨和跨节点时间差。序号决定两件事:重放时从哪里开始,以及两条分支同时写同一个会话时谁被判定为过期。
中间产物指文件、抽取结果、向量片段、临时构建物。它的正确位置是内容寻址的对象存储,键由输入摘要决定,而不是由单元编号决定。理由很实际:一万个单元里有相当比例在做重复工作,内容寻址让第二个单元直接命中第一个的结果,省下的是真实调用量;同时它让某个产物是哪一版输入这件事可以事后追认。
外部世界状态最难,因为它不由我们持有:工单已经开了、邮件已经发了、库存数量已经改了。这一类的正确做法不是外置而是前置,在动手之前先把这次动作声明成一条待执行记录,写清幂等键,再交给执行器去真正发起。声明与执行分离之后,半途成功才有可判定含义:待执行记录存在但没有回执,就是需要人工确认的一格,而不是含糊的大概做了。
三、断点续跑的三档语义与各自要留的产物
支持断点续跑这句话必须拆成三档,否则合同与实现之间必然有落差。三档的存储成本和工程约束差别很大,应该在需求阶段就选定并写进验收条款。
第一档是整轮重来。任何一次失败就把这一轮全部作废,重新发起。它只需要留下输入快照和随机种子,成本最低,但几十小时的运行接受不了,一次网络抖动就重来一次,永远跑不完。第二档是从检查点续。要求每个单元在明确的边界处落检查点,即完成一个可独立提交的最小阶段之后,而不是完成一次循环之后。检查点里必须写入我此时认为外部世界是什么样的断言,恢复时先验证断言,验证不过就退回重新观察,而不是直接往下走。第三档是只重放失败的子任务。它要求任务图是显式的,每个节点声明依赖哪些节点的哪些输出,失败节点的下游能被精确列出;同时要求所有节点满足幂等,否则重放等于重复写入。
顺带把幂等键和超时判定说清楚。用单元编号加阶段编号只能防住同一个单元的重复,跨天运行时单元会被回收再分配,编号会重复使用。可靠的组合是轮次标识、输入摘要、阶段序号三段。超时判定同理,一个阶段的超时不能按墙钟起点算,要按该阶段最后一次检查点的落盘时间算,否则排队等待会被算进执行耗时,导致大面积误判为失败。
四、资源账:总量要能归因到具体动作
据公开报道的那次实测里,消耗量是按天公布的。天级总量对我们的意义不是容量规划,而是一个反面提示:如果只能拿到总量,那么这一天的消耗里有多少属于有效产出,就无从回答。长时大规模运行的账至少要在四个维度上落地,缺一个就会出现解释不了的差额。
| 记账维度 | 关键字段 | 回答什么问题 | 缺失时的典型现象 |
|---|---|---|---|
| 执行单元 | 单元标识、轮次、起止检查点 | 哪个单元吃掉了大头 | 只能按平均值估算,无法定位 |
| 租户或项目 | 归属主体、配额来源、超额责任人 | 这笔账算在谁头上 | 结算周期末出现无人认领的用量 |
| 任务类型 | 阶段名、模型档位、单次输入的规模 | 哪类动作最贵 | 优化只做在感觉贵的地方 |
| 重试与失败 | 重试次数、失败原因码、是否命中熔断 | 有效消耗与无效消耗各占多少 | 故障期的暴涨被当成业务增长 |
四个维度里最常被省略的是最后一个,而它在长时运行里最贵。一个单元反复重试同一个失败动作,账面上和正常推进长得一模一样;只有把重试次数单独列成一列,才能把因为故障产生的消耗和因为业务产生的消耗分开。归因的做法很朴素:每次调用发起之前,把单元标识、轮次、阶段、重试序号写进调用日志,并作为配额扣减的凭证。账单来了才有对得上的东西,超额争议也才有讨论基础。
五、失败隔离、重试预算与背压层级
一万个单元共享配额时,最大的风险不是某个单元失败,而是失败之后它的行为方式。重试是最容易被写坏的一段逻辑:常见配置的退避间隔会越拉越长,但永远不会自己放弃。一万个单元里只要有一小撮进入循环,就能长期占住配额,把正常推进的单元拖成排队。
治理靠两条线。一条是重试预算:每个单元在每个阶段的重试次数与消耗量都有独立额度,额度用完就把该子任务标记为待人工处理,不再发起新调用。预算要按阶段定义而不是全局定义,因为不同阶段单次重试的成本差好几个量级。另一条是熔断线:以任务类型为单位统计失败率,超过阈值时该类整体暂停,只放少量单元作为探针继续,探到成功再逐步放量。熔断必须带一句对外说明,讲清被熔断的是哪一类、恢复条件是什么,否则调度层会以为只是变慢,继续往里塞任务。
背压则要明确放在哪一层。放在入口层,好处是能把超额请求直接挡在外面,坏处是它看不见真实容量。放在调度器层,好处是能结合配额与队列做准入决策,坏处是队列本身会堆积。放在模型服务层,能精确反映推理容量,但只对单一后端有效。三者不是选一个而是分工:入口做总量闸门,调度器做准入与配额分配,模型服务层用拒绝码把真实压力回传给调度器。经验上最容易缺的是调度器这一环的准入,典型症状是入口没拦住、服务层已经在拒绝、调度器还在提交。
六、核心交付物:六项前置定义表与验收判据
上线之前把下面六项写成文档并与客户对齐,比任何运行期调优都值钱。每一项都要有明确产物形态,能验证才算真的定义过。
| 前置项 | 必须在上线前定下的内容 | 验收时看的证据 |
|---|---|---|
| 状态外置 | 三类状态的存放位置、序号规则、声明与执行分离的边界 | 杀掉进程后恢复,外部动作不重复发生 |
| 断点语义 | 选用哪一档、检查点落盘时机、断言失败后的退回路径 | 注入中断后从检查点继续且结果一致 |
| 幂等与超时 | 幂等键的组合方式、阶段超时的计算起点、半途成功的判定 | 重复投递同一任务不产生第二次外部写入 |
| 资源账 | 四个维度的字段、归因凭证、无效消耗的统计口径 | 任一天的总量能拆到单元与具体动作 |
| 失败隔离 | 每阶段重试预算、熔断阈值与探针策略 | 单个单元的循环无法耗尽全局配额 |
| 可观测性 | 单元生命周期事件、原因码字典、日志保留期 | 能回答哪个单元何时因何停过 |
最后一条判据最实用:随便挑一个时间点,能不能回答哪个单元在什么时候因为什么停了一次。答得出来,说明六项都落了地;只能给出总耗时和成功率的,说明日志只是运行的副产品,不是治理能力。这份六项表我们在洹拓的内部上线评审里当门禁用,缺一项就当未就绪。也不必等到万级才动手,几百个单元连跑过夜就会遇到同样的问题,只是数字小一些、后果晚一些。