返回博客

并发 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 复利的反面:你把昂贵的数据一遍遍带着走,直到上下文满。

坑:Inline payload × N 批次 = 上下文定时炸弹

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

| 指标 | 内联 payload | 磁盘预暂存 | |------|-------------|-----------| | 单批 dispatch prompt | 10–30 KB | ~1 KB | | 25 批总 prompt 开销 | 250–750 KB | ~25 KB | | 上下文溢出风险 | 高 | 低 |

25 批全部跑完后 orchestrator 上下文消耗约 30 KB(dispatch prompts 部分),相比内联方案节省约 96%。

注意:字段 schema 必须与 heredoc 完全一致

预暂存脚本产出的 JSON 字段名必须与 Agent 定义里 heredoc 块产出的完全一致——projectRootbatchFilesbatchImportData 一个都不能少,否则下游提取脚本找不到字段,静默产出空数据。接手新 Agent 前先仔细读它的 heredoc 块。

一句话外卖

把「数据流经 prompt 传递」改为「数据落磁盘,prompt 只传路径」,是大批量并发 Agent 调度中防止 orchestrator 上下文溢出最直接的手段。