并发 25 个 Agent 时上下文爆了——把 Payload 落磁盘就省了 96% token
某次用 AI 代码理解工具分析一个有 600 多个文件的 Next.js 项目,Phase 2 需要把文件切成 25 个批次,每批并发启动一个 file-analyzer 子 Agent。第一轮跑到第 15 批左右,orchestrator session 直接上下文溢出崩掉——根本没跑完。
排查发现问题不在 Agent 逻辑,而在 dispatch 的方式:每次派发子 Agent,都把那一批的完整 JSON payload 内联进 prompt,25 批 × 每批 25 KB = 625 KB 的数据全部堆在 orchestrator 的对话历史里,再加上每个 Agent 的文本回包和 orchestrator 自身的胶水文字,上下文撑爆只是时间问题。
现象
- 启动大批量并发 Agent 后,orchestrator 在中途(一般 10–20 批之后)报上下文预算耗尽
- 每条 dispatch prompt 在调试输出里有 10–30 KB
- 所有 Agent 内部逻辑正常,只是 orchestrator 自己装不下了
根因
问题出在「数据流经由 prompt 传递」这个默认设计上。
许多 Agent skill 定义文件里有类似这样的一段:
cat > input.json << 'ENDJSON'
{
"projectRoot": "...",
"batchFiles": [...],
"batchImportData": {...}
}
ENDJSON这本来是让 orchestrator 把数据直接嵌进 dispatch prompt,然后子 Agent 把它写到磁盘。这在单批次或少量批次时没问题,但当批次数量上去以后,orchestrator 每次发出的 prompt 体积翻倍,而且所有已发出的 prompt 都累积在 conversation history 里——这正是 cache_read 复利的反面:你把昂贵的数据一遍遍带着走,直到上下文满。
25 批次 × 25 KB/批 = 625 KB payload 塞进对话历史。加上回包和胶水文字,orchestrator 在跑完之前就可能耗尽上下文预算,前功尽弃。批次越多、payload 越大,这个炸弹的威力越大。
正解
在开始 dispatch 之前,由 orchestrator 跑一个「预暂存脚本」,把每批 JSON 提前写进磁盘。dispatch prompt 只携带文件路径(约 1 KB),子 Agent 直接读文件,跳过 heredoc 步骤。
第一步:写预暂存脚本,在 dispatch 之前跑一次
// tmp/stage-batches.mjs
import fs from "node:fs";
import path from "node:path";
const PROJECT_ROOT = process.argv[2];
const BATCH_SIZE = 25;
const TMP_DIR = path.join(PROJECT_ROOT, ".tool/tmp");
const scan = JSON.parse(
fs.readFileSync(path.join(PROJECT_ROOT, ".tool/intermediate/scan-result.json"), "utf-8")
);
const files = scan.files;
const importMap = scan.importMap || {};
// 按目录排序,让同目录的文件尽量落在同一批
const sorted = [...files].sort((a, b) =>
path.dirname(a.path).localeCompare(path.dirname(b.path)) || a.path.localeCompare(b.path)
);
const batches = [];
for (let i = 0; i < sorted.length; i += BATCH_SIZE) batches.push(sorted.slice(i, i + BATCH_SIZE));
for (let i = 0; i < batches.length; i++) {
const batchFiles = batches[i].map(f => ({
path: f.path, language: f.language, sizeLines: f.sizeLines,
}));
const batchImportData = {};
for (const f of batchFiles) batchImportData[f.path] = importMap[f.path] || [];
fs.writeFileSync(
path.join(TMP_DIR, `analyzer-input-${i}.json`),
JSON.stringify({ projectRoot: PROJECT_ROOT, batchFiles, batchImportData }, null, 2)
);
}
console.log(`已预暂存 ${batches.length} 批输入到 ${TMP_DIR}`);第二步:dispatch prompt 只传路径指针
你是第 <N> 批的 file-analyzer 子 Agent。
**优化提示**:输入数据已预暂存,直接读取:
tmp/analyzer-input-<N>.json
跳过 heredoc 步骤,从该文件读取所有字段后立即执行提取脚本:
node extract-structure.mjs tmp/analyzer-input-<N>.json tmp/results-<N>.json
输出路径:intermediate/batch-<N>.json
仅返回简短摘要。
第三步:按波次并发 dispatch,每波 5 个
[Agent batch 0, run_in_background=true]
[Agent batch 1, run_in_background=true]
[Agent batch 2, run_in_background=true]
[Agent batch 3, run_in_background=true]
[Agent batch 4, run_in_background=true]
等 5 个任务完成通知后,再发下一波。
| 指标 | 内联 payload | 磁盘预暂存 | |------|-------------|-----------| | 单批 dispatch prompt | 10–30 KB | ~1 KB | | 25 批总 prompt 开销 | 250–750 KB | ~25 KB | | 上下文溢出风险 | 高 | 低 |
25 批全部跑完后 orchestrator 上下文消耗约 30 KB(dispatch prompts 部分),相比内联方案节省约 96%。
预暂存脚本产出的 JSON 字段名必须与 Agent 定义里 heredoc 块产出的完全一致——projectRoot、batchFiles、batchImportData 一个都不能少,否则下游提取脚本找不到字段,静默产出空数据。接手新 Agent 前先仔细读它的 heredoc 块。
一句话外卖
把「数据流经 prompt 传递」改为「数据落磁盘,prompt 只传路径」,是大批量并发 Agent 调度中防止 orchestrator 上下文溢出最直接的手段。