当你需要让 Claude Code 同时处理几十个任务——为每个源文件生成单元测试、批量翻译文档、给每个模块写 API 说明——串行地一个接一个 claude -p 跑下来,瓶颈不是模型,而是「一次只跑一个」的等待。本教程教你用并行扇出(fan-out)+ git worktree 隔离 + 结构化输出归约这套组合拳,把批处理吞吐量提升 N 倍,同时彻底消除多个 Agent 互相踩踏文件的混乱。适合已经会用 claude -p 跑单任务、想把它工程化到批处理与 CI 流水线的开发者。
claude -p "hi" 能正常返回)xargs、jq)--output-format、--allowedTools 等 -p 模式常用 flag最朴素的批处理长这样:读一个任务列表,循环里逐个调用 claude -p。
# 串行:逐个跑,第二个任务要等第一个结束
while read -r file; do
claude -p "为 $file 生成单元测试" --allowedTools "Read,Write" \
--output-format text > "tests/$(basename "$file" .py)_test.md"
done < files.txt
如果单任务平均耗时 60 秒,50 个文件就要 50 分钟。而模型推理是远程并发的——你的本地 CPU 这 50 分钟里 99% 时间在空转。瓶颈从来不是计算,而是「串行等待」。
并行扇出的核心思路很简单:与其排队,不如同时开 N 个。
# 并行:同时开 4 个,总耗时 ≈ 50/4 × 60s ≈ 12.5 分钟
cat files.txt | xargs -P 4 -I {} \
claude -p "为 {} 生成单元测试" --allowedTools "Read,Write" \
--output-format text > "tests/$(basename {} .py)_test.md"
xargs -P 4 表示最多同时运行 4 个子进程。50 个任务会被切成持续 4 路并发,把那 99% 的空闲填满。
常见问题:
| 现象 | 原因 | 解决 |
|---|---|---|
| 并发开太高被限流 | API 速率限制 | -P 从 4 起步,按 429 错误逐步上调 |
| 成本突然飙升 | 每个进程独立计费 | 给每个任务加 --max-turns 上限 |
| 启动慢 | 每次都加载 hooks/skills/CLAUDE.md | 加 --bare 跳过自动发现 |
我们以「为仓库里每个 Python 文件生成一段模块说明」为例。先准备任务清单:
# 收集所有需要处理的文件路径
git ls-files '*.py' > tasks.txt
wc -l tasks.txt # 比如 48 个文件
写一个单任务执行器(把复杂逻辑封装进函数,便于复用与调试):
# worker.sh —— 处理单个文件
#!/usr/bin/env bash
set -euo pipefail
file="$1"
claude -p "阅读 $file,用 5 条要点总结它的职责。只输出要点。" \
--allowedTools "Read" \
--output-format text \
--bare \
> "out/$(echo "$file" | tr '/' '_').md"
--bare 是提速关键:它跳过 hooks、skills、plugins、MCP 服务器、CLAUDE.md 的自动发现,让每次 -p 启动从「完整会话初始化」降级为「裸推理」。对只读、无状态依赖的批任务,这是免费的加速。
然后扇出:
mkdir -p out
cat tasks.txt | xargs -P 4 -I {} bash worker.sh {}
echo "完成: $(ls out | wc -l) 个结果"
预期结果:终端会看到 4 个 claude 进程同时跑,out/ 目录里陆续出现结果文件,总耗时约为串行的 1/4。
常见问题:
- 结果文件互相覆盖:如果多个任务输出到同名文件,并发写会撞车。务必用文件路径(含目录)做唯一文件名,这里用 tr '/' '_' 把 src/auth/login.py 变成 src_auth_login.md。
- 某任务失败拖累整体:worker.sh 里 set -e 会让脚本在出错时退出,但 xargs 不会因此停掉其他进程——失败的进程悄悄没结果。下文用结构化输出做可观测。
只读批处理(生成说明、翻译)很安全,因为每个进程只读不写共享状态。但一旦任务要修改文件——批量重构、批量生成测试并落地到仓库——并行就会变成灾难:
git checkout 同一个分支 → 冲突package.json,进程 B 同时读它 → 不一致npm test 时,进程 D 正在改测试文件 → 假阳性失败根因:所有进程共享同一个工作目录。解决方案是给每个进程一个独立的工作目录,而 git worktree 天生为此而生。
git worktree add path branch 让你在不 clone 整个仓库的前提下,把某个分支/提交检出到另一个目录——多个目录共享同一个 .git,互不干扰磁盘上的工作文件。Claude Code 原生支持这套机制。
# 手动版:为每个任务开一个 worktree
git worktree add ../worker-wt-auth -b feat/test-auth
(cd ../worker-wt-auth && claude -p "为 auth.py 补单元测试" --allowedTools "Read,Edit,Write,Bash")
git worktree remove ../worker-wt-auth --force
手动管理 worktree 很快就累。Claude Code 提供了 --worktree 标志,让会话自动跑在自己的隔离 worktree 里;并通过 WorktreeCreate / WorktreeRemove 两个 hook 事件把创建与清理彻底自动化。
--worktree 标志:一个 flag 拿到隔离目录给 claude 加 --worktree(或在子智能体里设 isolation: "worktree"),Claude Code 会自动为这次会话开辟一个独立 worktree,会话期间所有文件操作都被限制在里面,结束后自动清理。
# 单个:自动隔离
claude --worktree -p "把 utils.py 里的 print 全部换成 logger.info" \
--allowedTools "Read,Edit,Bash" \
--permission-mode acceptEdits
关键点在于:这个 worktree 是临时的,会话结束即销毁。这意味着如果你想让改动「留下来」,必须在会话内 git commit,结束前把分支合并回主仓,或者显式保留 worktree。
把它和并行扇出结合,就得到了真正安全的多开模式:
cat tasks.txt | xargs -P 4 -I {} bash -c '
claude --worktree -p "为 {} 编写 pytest 单元测试,落地到 test_ 同名文件" \
--allowedTools "Read,Write,Bash" \
--permission-mode acceptEdits \
--output-format json
'
现在 4 个进程各跑各的 worktree,磁盘上互不重叠,没有 git checkout 冲突,没有测试互相干扰。
常见问题:
- worktree 没被清理:异常退出(kill -9)会留下孤儿 worktree。用 git worktree prune 清理。
- 改动消失了:worktree 是临时的,没 commit 就销毁等于白干。在 prompt 里明确要求「完成后 git commit 到当前分支」,并在归约阶段把分支收集起来。
官方 hooks 系统新增了两个事件,专门服务于并行隔离场景:
--worktree 或 isolation: "worktree" 创建 worktree 时触发。这里有一个极其关键、文档里写得很隐蔽的细节:一旦你定义了 WorktreeCreate hook,Claude Code 会把 worktree 的实际创建工作完全交给你——它「替换默认 git 行为」。也就是说,你的 hook 必须真正去执行 git worktree add,否则隔离目录根本不会出现。
典型用法是在 .claude/settings.json 里配置:
{
"hooks": {
"WorktreeCreate": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/wt-create.sh"
}
]
}
],
"WorktreeRemove": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/wt-remove.sh",
"async": true
}
]
}
]
}
}
WorktreeRemove 加了 "async": true——清理是后台异步执行的,不阻塞会话退出,让批处理收尾更快。
worktree 创建脚本(注意它读 stdin 上的 JSON 上下文):
# .claude/hooks/wt-create.sh
#!/usr/bin/env bash
input=$(cat) # Claude Code 把事件 JSON 从 stdin 传入
branch=$(echo "$input" | jq -r '.branch // empty')
path=$(echo "$input" | jq -r '.path // empty')
branch="${branch:-wt/$(basename "$path")}"
# ⚠️ 既然定义了此 hook,必须自己真正创建 worktree
git worktree add "$path" -b "$branch" >/dev/null 2>&1
echo "{\"path\":\"$path\",\"branch\":\"$branch\"}"
清理脚本:
# .claude/hooks/wt-remove.sh
#!/usr/bin/env bash
input=$(cat)
path=$(echo "$input" | jq -r '.path')
git worktree remove "$path" --force 2>/dev/null || rm -rf "$path"
git branch -D "$(echo "$input" | jq -r '.branch')" 2>/dev/null || true
为什么要在 hook 里而不是脚本里管理?因为 hook 对所有 --worktree 调用全局生效——无论是你手动跑、CI 跑、还是子智能体 isolation: worktree 自动跑,创建与清理的逻辑只有一处,永远不会漏。
并行跑起来的下一个痛点是可观测性:哪些任务成功?产出了什么?哪些失败了?把每个进程的输出从 text 升级为 json,就能用 jq 精确归约。
--output-format json 会让 -p 返回一个包含 session_id、result、用量等字段的 JSON 对象:
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"
对批处理,我们把每个任务的 JSON 结果落盘,最后汇总:
# worker2.sh —— 结构化版,写一份带元数据的 JSON
#!/usr/bin/env bash
set -uo pipefail # 注意去掉 -e,让失败也能记录
file="$1"
{
echo "{\"file\":\"$file\","
claude -p "为 $file 生成 pytest 测试。完成后 git commit。" \
--allowedTools "Read,Write,Bash" \
--permission-mode acceptEdits \
--output-format json \
| jq -c '{result: .result, cost_usd: (.cost_usd // .total_cost_usd), turns: (.num_turns // null)}'
echo "}"
} > "out/$(echo "$file" | tr '/' '_').json" 2>/dev/null
扇出后归约成一份清单:
mkdir -p out
cat tasks.txt | xargs -P 4 -I {} bash worker2.sh {}
# 汇总:哪些成功、各自花了多少
jq -s 'map({file, ok: (.result != null), cost: .cost_usd})
| {total: length, succeeded: (map(select(.ok)) | length),
total_cost: (map(.cost // 0) | add)}' out/*.json
--output-format stream-json --verbose --include-partial-messages 则适合需要逐事件流式观察(工具调用、中间消息)的场景,但对「跑完归约」的批处理,json + jq 足够且更易解析。
常见问题:
- 后台任务卡住进程:claude -p 里如果起了 dev server 之类永不退出的后台 shell,旧版本会无限挂起。v2.1.182 起默认 10 分钟上限,可用 CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS 调整,设 0 为不限制。
- JSON 解析失败:某个 -p 因限流没返回合法 JSON,jq 会报错。worker 里用 2>/dev/null 兜底,并在归约时用 select(.result != null) 把失败项标记出来单独重试。
把上面四块拼成一个完整、可运行的项目。目标:给仓库里所有 *.py 文件并行生成单元测试,每个任务隔离在独立 worktree,产出归约为一份带成本与状态的报告。
目录结构:
repo/
├── .claude/
│ ├── settings.json # WorktreeCreate/Remove hooks
│ └── hooks/
│ ├── wt-create.sh
│ └── wt-remove.sh
├── batch/
│ ├── run.sh # 主驱动
│ └── worker.sh # 单任务执行器
└── src/*.py
主驱动 batch/run.sh:
#!/usr/bin/env bash
set -uo pipefail
JOBS="${JOBS:-4}"
MODEL="${MODEL:-sonnet}"
rm -rf batch/out && mkdir -p batch/out
echo "==> 收集任务"
git ls-files '*.py' > batch/tasks.txt
echo " $(wc -l < batch/tasks.txt) 个文件,并发 $JOBS"
echo "==> 扇出"
cat batch/tasks.txt | xargs -P "$JOBS" -I {} \
bash batch/worker.sh {} "$MODEL"
echo "==> 归约报告"
jq -s '
group_by(.ok) as $g
| { succeeded: ($g | map(select(.[0].ok)) | length),
failed: ($g | map(select(.[0].ok | not)) | length),
total_cost_usd: (map(.cost_usd // 0) | add),
failures: (map(select(.ok | not) | .file)) }
' batch/out/*.json | tee batch/report.json
单任务执行器 batch/worker.sh:
#!/usr/bin/env bash
set -uo pipefail
file="$1"; model="$2"
slug=$(echo "$file" | tr '/' '_')
# --worktree: 隔离;--bare 跳过非必要加载;prompt 内要求 commit
claude --worktree --bare -p \
"为 src/$(basename "$file") 编写 pytest 单元测试,覆盖主要分支。
把测试写到 test_$(basename "$file" .py).py,运行 pytest 确认通过,
然后 git commit -m 'test: add tests for $(basename "$file"'。" \
--model "$model" \
--allowedTools "Read,Write,Bash" \
--permission-mode acceptEdits \
--output-format json \
2>/dev/null \
| jq -c --arg f "$file" \
'{file:$f, ok:(.result!=null), cost_usd:(.total_cost_usd // 0),
result:(.result[0:120])}' \
> "batch/out/$slug.json" || \
echo "{\"file\":\"$file\",\"ok\":false,\"cost_usd\":0}" > "batch/out/$slug.json"
运行并对比:
# 串行基准
JOBS=1 time bash batch/run.sh
# 并行
JOBS=4 time bash batch/run.sh
典型结果:48 个文件,单任务约 40 秒,串行约 32 分钟,4 路并行约 9 分钟,吞吐提升约 3.5 倍(不到 4 倍是因为 git worktree 创建/销毁有固定开销、且有 API 限流)。batch/report.json 给出每个任务的成功状态与累计成本,失败的文件可一键重跑:
# 重跑失败项
jq -r '.failures[]' batch/report.json | xargs -P 4 -I {} bash batch/worker.sh {} sonnet
flowchart TD
A["任务清单 tasks.txt<br/>git ls-files '*.py'"] --> B["xargs -P 4 扇出"]
B --> W1["Worker 进程 1"]
B --> W2["Worker 进程 2"]
B --> W3["Worker 进程 3"]
B --> W4["Worker 进程 4"]
W1 --> C1["WorktreeCreate hook<br/>git worktree add"]
W2 --> C2["WorktreeCreate hook"]
W3 --> C3["WorktreeCreate hook"]
W4 --> C4["WorktreeCreate hook"]
C1 --> R1["claude --worktree -p<br/>--output-format json"]
C2 --> R2["claude --worktree -p"]
C3 --> R3["claude --worktree -p"]
C4 --> R4["claude --worktree -p"]
R1 --> J1["out/file1.json"]
R2 --> J2["out/file2.json"]
R3 --> J3["out/file3.json"]
R4 --> J4["out/file4.json"]
R1 --> D1["WorktreeRemove hook async<br/>git worktree remove"]
R2 --> D2["WorktreeRemove async"]
R3 --> D3["WorktreeRemove async"]
R4 --> D4["WorktreeRemove async"]
J1 & J2 & J3 & J4 --> G["jq -s 归约<br/>report.json: 成功/失败/成本"]
G --> H{"有失败?"}
H -- 是 --> A2["重跑失败项"]
H -- 否 --> Z["合并各 worktree 分支<br/>git merge / cherry-pick"]
-P:从 4 起步。只读任务可到 8–16;写任务(worktree 隔离)建议 4–6,避免本地 git I/O 与磁盘抖动。出现 429 就降。--model haiku,复杂重构用 sonnet,省钱又提并发上限。--bare 的取舍:纯无状态只读任务必加;任务需要读项目 CLAUDE.md 或调 skills/MCP 时不能加,否则丢上下文。cherry-pick,要么在 WorktreeRemove hook 里改成「不删」并登记路径。--max-turns 兜底:给每个 worker 设上限(如 --max-turns 20),防止某个任务陷入死循环吞掉整个并发槽。-p、--bare、--output-format、--resume 全部 flag 参考/batch 命令与 claude -p 大规模变更的工程实践claude -p 单任务」「git worktree 基础」之后,作为「从单任务到批处理工程化」的进阶模块。--worktree 标志说明claude -p、--bare、--output-format json/stream-json、--resume 等 flag 权威参考/batch 命令与 claude -p 大规模批处理工程实践[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
