ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

限额团灭449个子Agent后,如何设计可恢复的并行任务流

限额团灭449个子Agent后,如何设计可恢复的并行任务流 上周我带着一个小团队做一次大迁移把一个有点年头的 Java 服务整体改成 Kotlin顺手把构建脚本、目录结构、测试命名全部重排了一遍。当时我给自己列了 449 个独立子任务并且把大部分子任务都丢给了Claude Code 的子 Agent去并行处理——听起来挺顺利对吧现实是等我去翻结果目录的时候449 个子 Agent 全部因为同一类原因被平台限额中止。逐条翻完日志我确认其中 438 个子 Agent 没有任何可恢复的中间产物最终只能从头重做。今天这篇就聊聊这次翻车事故的前因后果、复盘数据以及我后来怎么改造子 Agent 工作流把“限额杀掉”这件事变成可预期、可恢复的常态。如果你也在用 Claude Code 跑批量开发任务或者正准备把大任务拆给子 Agent这篇文章应该能帮你少踩几个相当大的坑。1. 这次到底在跑什么一次大规模并行子 Agent 任务现场先交代一下背景。整个项目我拆成了三段第一段是静态改造把 Java 类的包名、import、基础注解迁移到 Kotlin 等价写法。第二段是行为改造把 Spring 的 Bean 注入方式、事务边界、异常处理迁移到 Kotlin 惯用写法。第三段是验证改造每个模块迁移完必须跑通测试、修掉编译问题、记录风险点。三段加起来一共 449 个任务。我当时的想法很简单既然 Claude Code 支持子 Agent那就让子 Agent 各管一摊互不干扰既能在隔离上下文里干活也不会让主线程的上下文被撑爆。所以我写了一个很粗糙的调度脚本从任务清单里不断拉起新的子 Agent 实例。调度策略是每批最多 4 个子 Agent 并发每个子 Agent 用一个独立的 prompt 启动prompt 里包含了任务说明、相关文件路径、预期输出位置。听起来很稳妥而且前 20 分钟内确实一批一批的任务被正常拉起、正常执行、正常产出结果。转折发生在大约 40 分钟后。我注意到任务队列开始积压接着日志里开始出现同一类失败子 Agent 刚开始处理甚至已经处理到一半会话突然被中断原因指向账号/API 这一层的额度限制。我一开始以为是偶发所以只做了简单重试。但后续几个小时里失败像潮水一样涌过来。到当天结束时449 个子 Agent 全军覆没。注意不是全部失败是有 11 个子 Agent 留下了相对完整的产出另外 438 个基本等于白跑。这次事故让我彻底重新审视了一件事把任务丢给子 Agent 并行跑不等于把任务安全地交给子 Agent 并行跑。如果没有在工程层面做好中断恢复设计并行的规模越大翻车时浪费的时间就越多。2. 被限额杀掉的子 Agent死因到底出在哪几个环节很多人会下意识觉得限额杀掉子 Agent 只是一个“调用次数超了”的问题。但我翻了 449 份日志之后发现死因不是一个而是四个不同环节叠加。2.1 并发数量与共享配额之间的矛盾Claude Code 的用户界面里你看到的是一个聊天入口或命令行入口但在背后所有请求都消耗同一个配额池。主 Agent 会占一部分每个子 Agent 也在消耗请求量、Token 量而且这种消耗不是均匀的。我实际跑起来之后每批 4 个子 Agent 看起来峰值远大于 4 个会话的量。因为这些子 Agent 内部还会自己调用工具、读取大文件、生成较长方案一次任务里可能产生几十次请求。配额池被瞬间抽干自然就开始杀后面的请求。2.2 上下文越长单次请求越重另一个死因是单个子 Agent 携带的上下文过长。我最初给子 Agent 的 prompt 里塞满了背景说明、编码规范、目标文件全文、相关类的关系图。子 Agent 在运行中还会不断追加中间结果、调试信息导致每轮请求携带的上下文越来越大。到后半段很多子 Agent 一次请求就要消耗非常大的 Token 量。而 Token 配额通常又不是无限续的这就导致子 Agent 跑得越久死在后半程的概率越高。从我的日志统计来看死在“已经完成 60% 以上进度”的子 Agent 占了很大比例这是最亏的一种死法。2.3 会话被打断后思维链路没有落盘Claude Code 的子 Agent 在正常运行的时候它的“思考”和“讨论”都保留在会话上下文里。一旦会话被限额杀掉这个思维链路就断了。我原本以为文件系统里至少会留下一些过程产物比如子 Agent 写了一半的代码、改了一半的配置。翻下来发现大部分子 Agent 在会话被杀之前还没有来得及把任何中间结果写进文件。它们处于“正在脑子里设计方案”的阶段而这个阶段的东西你根本无法从外部读取。这就是所谓的“思维没有落盘”。2.4 主线程并不是可靠的工作记录器还有一种情况是子 Agent 被杀死之后主线程里只留下了“这个子任务失败了”的日志但完全没有子任务内部运行轨迹的记录。你根本不知道它已经决策到什么程度、尝试过哪个方案、卡在哪一个文件。大多数时候唯一的线索就是子 Agent 启动时的完整 prompt以及它被杀前的最后一句输出。这两样东西根本不足以支撑“从断点继续”。所以结论只有一个重新做一遍。3. 复盘实录我是怎么把 438 个任务判定为“必须重做”的这里说的“判定”不是凭感觉是真的翻了每一份运行记录。我写了一个小脚本把所有子 Agent 的输出目录跑了一遍然后把结果汇总成一张表来逐条判断。表格的大致结构是这样的任务 ID处理阶段被杀时刻是否留下文件产物是否有可读的进度记录结论task_0001已编译通过收尾阶段有产物有算完成task_0002设计方案中刚启动 3 分钟无无直接重做task_0003已生成一半代码中途被杀有半个文件无判重做task_0004正在修测试中途被杀无无判重做总共 449 条记录我一条一条核对过去的。最终结果11 个任务有明确产出代码完整测试通过可以认为是活的。5 个任务留有部分文件但无法确定这些文件是否处于可用状态我没有冒险采用。433 个任务完全没有有效产出只能重做。加上那 5 个无法确认的实际被我从头重做的一共 438 个。这个 438 的数字就是这么来的。3.1 我为什么不敢采用那 5 个“部分文件”说实话看到有部分文件的子 Agent 时我一开始还抱了一丝希望。但打开文件一看问题很大有的文件只写了类声明和几个空方法没有任何业务逻辑。有的文件生成了但 import 是一半 Kotlin 一半 Java根本编译不过。有的文件里留了大量 TODO 和自问自答属于子 Agent 给自己做的“思路草稿”不能当代码用。这些半成品如果直接交给编译器它会给你几百个报错如果交给另一个 Agent 去补充又需要传递大量背景信息代价比重新跑一次还高。实际尝试补了 2 个之后我果断放弃了全部归入重做。3.2 那 11 个“幸存者”有什么共同特征这 11 个活着回来的子 Agent 变成了我最重要的研究样本。我把它们全部调出来对比发现它们有三个共同点第一它们都是短任务。没有一个任务超过 30 分钟普遍在 10 到 20 分钟之间就结束了。短任务意味着单个会话消耗的 Token 少在半路触达概率低。第二它们都在早期就把中间结果写进了磁盘。有的子 Agent 会按阶段生成阶段报告有的直接把改动后的代码立刻写到文件里然后再继续下一步。第三它们的最终输出都不大。没有一个子 Agent 最后给我交了一篇几千字的“分析和总结”而是干净利落地给了结果文件路径和几条要点。这 11 个幸存者用实例告诉我子 Agent 是否能在限额下存活不取决于运气取决于你有没有逼它把状态及时落盘。4. 从头重做一遍的成本到底有多高先看一组来自我复盘时的统计数字。为了让你有体感我把这一轮的数字稍微做了聚合没有精确到单次调用。第一次奔跑阶段449 个子 Agent 合计跑了约 340 小时。这里面大概有 316 小时是浪费掉的因为那些任务最后没有可用产出。重做阶段438 个任务我换了一种调度方式并没有全部从头并行跑而是先跑一部分、密切关注剩余配额、再放开一部分。结果很有意思重做阶段的总耗时只有约 150 小时比第一次少了很多。这个差距不是因为我第一次蠢、重做阶段聪明而是我第一次跑的时候产出了大量可以作为参考的“废料”。比如有些任务的 prompt 里包含了当时我手动分析过的类结构关系图这些关系不会失效有些任务重跑时我直接把第一次失败的 prompt 原封不动拿来用省掉了重新梳理的时间还有一些任务的失败日志里其实记录了接口签名、编译错误信息这些信息重跑时可以直接当作输入。所以严格说这 438 个任务也不是 100% 从零开始而是“从零开始跑逻辑但带着上一次的情报”。4.1 Token 消耗的浪费更难以接受时间上的浪费还好说毕竟可以重跑。真正让我心疼的是 Token 消耗。第一次奔跑阶段所有子 Agent 的请求量已经把配额吃掉了很大一部分。但因为大量子 Agent 死在后半程这些 Token 没有转化出任何可用的代码或文档。相当于你付了完整工程的钱工程队在工地上住了一个月最后你发现地基都没打。第二次重跑时又会产生新的消耗。所以“限额把你杀掉”这件事从来不只是时间问题它是双倍甚至三倍的资源消耗问题。把这一点想清楚你就知道为什么在项目开始前考虑“如何避免被限额杀掉”比事后补救重要一百倍。4.2 心理上的损耗反而占大头还有一笔账不太好算但很真实当你知道 438 个任务全部要重做时那种挫败感会直接摧毁你对整个工作流的信心。你会怀疑子 Agent 到底能不能用于这种批量任务甚至会怀疑自己拆任务的方式。我后来给自己的心理建设是这次翻车不是“子 Agent 不靠谱”而是“我用错了子 Agent”。限额是客观约束我要做的是在客观约束下重新设计用法。5. 扛住限额的子 Agent 实操改造模板这部分的标题应该换成“逃生预案”因为整个改造的核心思路就是假设每一个子 Agent 都会中途死掉那么它死掉之后应该留下什么。以下五个改动是我在重跑阶段用过的方案实测下来很稳。5.1 任务拆小把“一次做完”变成“一段一段做完”我第一批丢给子 Agent 的任务很多是“重构这个模块并修复所有测试”这种大而全的任务。这种任务对上下文要求极高子 Agent 很容易在后期被限额杀掉。重跑阶段把所有任务拆成可独立验证的较小单元不再要求“重构整个 service 层”而是“迁移 UserService 类只做语法迁移不讨论优化”。不再要求“顺手优化代码结构”而是“只处理编译错误保持行为一致”。不再要求“补充完善单元测试”而是“修复现有测试不新增测试逻辑”。每个子 Agent 的目标都改成了 10 到 30 分钟内能完成的小任务。任务越小单次请求消耗越少越不容易触达限额。5.2 强制进度落盘把思维外置到文件这是最核心的一条。我在每个子 Agent 的 prompt 里都加了一段硬性要求你可以直接抄你会在任务执行过程中随时可能被中断。因此你每完成一个小阶段必须立刻将当前进度写入文件progress.json。文件内容包括已完成步骤、当前遇到的问题、下一步计划、已经生成的文件路径和内容摘要。写完后才能继续下一步。progress.json就是一个简单的 JSON 结构我让它每次都覆盖写入保持小体积{ task_id: task_0142, status: migrating_entity_layer, completed_steps: [ read UserService.java, created UserService.kt prototype ], blocker: Entity 命名冲突等待确认, next_action: 修改 Entity 引用, generated_files: { src/main/kotlin/service/UserService.kt: 完全迁移待编译验证 } }这个文件不需要很长也不需要写得多规范关键是子 Agent 必须“边做边写”。有了这个文件即使会话被杀下一个子 Agent 可以通过读取progress.json快速了解它已经做到哪一步、打算怎么做。这不是严格意义上的断点续跑但已经能让我避免“从头再来”。5.3 让所有操作具备幂等性重跑时敢直接覆盖很多子 Agent 重跑时会因为“上一次已经生成了这个文件”而感到困惑。它们会停下来询问要不要覆盖要不要保留旧文件这个交互本身就会浪费时间和配额。为了避免这个问题我在重跑任务的 prompt 里统一写了一句本任务可能已经执行过一次所有目标文件都应视为旧版本。请直接覆盖覆盖前不需要征求确认。所有操作可以重复执行且结果一致。这句话听起来简单但实际效果非常好。子 Agent 不再犹豫也不会把时间花在“检查是否有旧文件”上。对代码迁移类任务来说幂等性几乎可以完全消除重跑的阻碍。5.4 调度侧节流限制并发观察配额水温重跑阶段我没有再采用每批 4 个并发的方式而是改成更保守的模型每批只跑 2 个并发。任务启动前先记录当前剩余配额。每完成一个任务检查配额消耗速度。如果消耗速度超过预期立刻暂停发布新任务。等待正在运行的任务自然结束后再重新开放。这里的关键是不要一上来就跑满并发因为并行度和成功率不是线性关系。只有把并发降下来才能保证已启动的任务有足够配额跑完。你宁可跑得慢也不要在半路集体死亡。我用的调度脚本核心逻辑类似这样while read -r task; do wait_until_parallel_slots_available check_quota_remaining if [ $quota_low true ]; then sleep 60 continue fi launch_subagent $task done task_list.txt看起来很傻但在限额环境下这种“老实排队”的策略反而效率最高。5.5 提前熔断让子 Agent 主动交回控制权我还给子 Agent 增加了一个“自行熔断”能力。在 prompt 里规定如果你发现任务实际规模远超预期或者需要修改的代码量超过初步评估的两倍不要硬撑。请在 progress.json 中标记为 BLOCKED并立即停止执行等待任务被重新拆分或补充信息。这样做的目的是避免子 Agent 在一个太过复杂的任务里耗尽所有配额却什么都没产出。与其让它跑到一半被限额杀不如让它提前承认“我搞不定”把控制权交回调度层我再把任务拆成更小的单元重新分配。这个方法帮我避免了不少隐形浪费。因为在实际运行中很多任务看起来是小改打开代码才发现涉及十几个类和一堆循环依赖。如果没有熔断机制子 Agent 会在一个无限膨胀的任务里挣扎到被限额杀掉为止。6. 常见问题与排查技巧实录重跑阶段和后续两周的实践里我又整理出一批高频问题。贴出来供你排查时对照。现象可能原因处理办法子 Agent 执行到一半突然没有输出触达单次请求限额检查日志中是否有配额相关错误降低单任务上下文体积并行子 Agent 连环失败主线程却正常并发消耗共享配额池超出窗口降低每批并发数改为 1 到 2 个并发重跑时子 Agent 问“是否覆盖旧文件”没有声明幂等要求在 prompt 里加入覆盖权限声明恢复任务时明明有日志但无法继续日志只有外部行为没有思维状态只能依靠 progress.json日志不能替代状态落盘子 Agent 产出了一些文件但无法编译半途被杀遗留中间态代码把文件当作参考情报不直接使用重新完整迁移一遍单个子 Agent 单次请求消耗异常大prompt 或上下文塞入了整个仓库文件内容只给出必要文件路径让子 Agent 按需读取6.1 日志到底该怎么翻很多朋友习惯直接在 IDE 控制台里翻输出但子 Agent 数量一多控制台根本刷不过来。我建议所有子 Agent 启动时把日志写到独立目录命名按任务 ID 来比如logs/task_0142/run.log logs/task_0142/progress.json logs/task_0142/output/这样排查时可以批量拉取按关键字过滤。我实际用到的过滤词包括9856、limit、quota、rate、以及context基本一轮就能筛出绝大多数死因。6.2 settings.json 里的配置和限额恢复没有直接关系网上很多时候提到 Claude Code 的settings.json似乎改几行配置就能解决问题。坦白说我在限流排查中并没有通过设置配置来解决额度问题。我理解的settings.json主要控制的是工具权限、模型行为、系统提示这些方面。它不会让你的配额变多也不能恢复一个已经死掉的会话。真正有用的不是去翻配置而是调整你的调度策略。后续我也只是把settings.json里允许的文件访问范围明确了一下避免子 Agent 因为权限问题无谓地读取大文件。6.3 为什么有些错误看起来很像“网络问题”子 Agent 被杀时错误信息里可能出现看起来像网络超时的文字。一开始我也误判为网络不稳定重跑了很多遍浪费了一整晚。后来发现这种超时往往是请求已经发出但响应在配额限制下没有正常回来。判断方法是看所有并发子 Agent 是否在同一时间段集体失败。如果集体失败八成是配额问题不是网络。千万别把集体失败当成偶发网络抖动去重试那样只会把剩余配额也烧光重伤第二次重试。7. 复盘后我给自己定下的几个硬规矩这轮事故之后我再也不做“裸奔式”的子 Agent 批量任务了。有几条硬规矩分享给你第一每一个子 Agent 的任务描述里必须有“会被中断”这一句。让子 Agent 从一开始就带着逃生意识去工作而不是默认它有无限生命。第二启动子 Agent 前强迫自己先回答三个问题它如果只完成 20% 就死我能拿到什么如果只完成 50% 就死我能不能接管如果它生成一半文件就死这些文件有没有二次使用价值只要有一个回答是“没有”那就说明任务太大先拆。第三把调度脚本写得克制一点。最开始的版本像开闸放水一样拼命拉任务后来改成“每完成一个再补一个”的方式。跑完一个验收一个验收不过的不要进下一个。这个慢一点但每一步都在积累可复用的状态。第四不要把希望寄托在“这一次应该不会死”。平台限额是弹性约束任何规模的并发都有触发概率。你唯一能控制的是让每个子 Agent 死得其所——至少留下progress.json和一份可以继续使用的文件。这一轮我从 449 个被限额杀掉的子 Agent 里学到的核心就一句话子 Agent 能不能扛住限额不取决于它有多会写代码而取决于你有多会替它处理死亡。留好退路再让它往前冲。
返回列表