首页 › tutorials › 并发批处理Worker池实战

Claude Code 并发批处理 Worker 池实战:用 Hooks 护栏驱动 -p 批量扇出

概述

本教程聚焦 Claude Code 的自动化三件套——无头 -p 模式、Hooks 事件护栏、批量并发执行——并将它们组装成一个可投产的 Worker 池:一条任务队列被分发到 N 个并发 claude -p 进程,每个进程由同一套 Hooks 策略约束(禁止越权写、自动格式化、回合预算熔断),最后由 stream-json 输出汇聚成一份汇总报告。适合需要在 CI、夜间批跑、多仓库批量改造等场景下"一次跑几十个独立任务"的工程师。

前置条件


一、核心心智模型:确定性护栏 + 并发扇出

很多团队第一次用 claude -p 跑批量任务时,会踩两类坑:

  1. 并发写冲突:多个 claude -p 进程改同一个工作区,互相覆盖。
  2. 护栏缺失:无人值守时模型越权执行 rm -rf、git push --force、写密钥。

解法是把职责拆成两层:

层 载体 职责
执行层 claude -p 进程池 承载一个独立任务,产生 stream-json 结果
策略层 .claude/settings.json 的 Hooks 在每个 Worker 内部拦截/放行/熔断
调度层 Shell 脚本(xargs -P / parallel) 拆队列、控并发、隔离工作区、汇聚结果

关键洞察:Hooks 是"声明一次、对每个 Worker 进程都生效"的策略。你不需要在每个 prompt 里反复叮嘱"别 force push",而是把它写进 PreToolUse 护栏,由 harness 在每次工具调用前强制执行。这就是"确定性"——它不依赖模型的"自觉"。

flowchart LR
    Q[任务队列 jobs.jsonl] --> SCHED[调度器 xargs -P N]
    SCHED --> W1[Worker 1 claude -p]
    SCHED --> W2[Worker 2 claude -p]
    SCHED --> WN[Worker N claude -p]
    W1 -.策略.-> H[(Hooks settings.json)]
    W2 -.策略.-> H
    WN -.策略.-> H
    H -->|PreToolUse 拦截| GUARD{放行/阻断/提醒}
    W1 --> R1[stream-json 结果]
    W2 --> R2[stream-json 结果]
    WN --> RN[stream-json 结果]
    R1 --> AGG[结果汇聚 report.md]
    R2 --> AGG
    RN --> AGG

二、-p 无头模式:单 Worker 的解剖

2.1 三种输出格式

# text:纯文本,最简单
claude -p "把 README 里所有的 TODO 收集成一个列表" 

# json:单条 JSON 对象,便于 jq 解析
claude -p "..." --output-format json | jq -r '.result'

# stream-json:换行分隔的事件流,批处理的黄金格式
claude -p "..." --output-format stream-json

stream-json 会按时间顺序吐出几类事件,批处理最关心的是首尾两个:

一个最小的事件抽取器:

# 从 stream-json 抓出 session_id 和最终结果
claude -p "修复 utils.py 里的类型错误" \
  --output-format stream-json \
  --allowedTools "Edit,Read,Bash(pytest *)" \
  > /tmp/w1.jsonl

# 取 session_id(用于 --resume)
grep '"type":"system"' /tmp/w1.jsonl | head -1 | jq -r '.session_id'

# 取最终结果与成本
grep '"type":"result"' /tmp/w1.jsonl | jq -r '"cost=\(.cost_usd) turns=\(.num_turns)\n\(.result)"'

2.2 工具白名单是第一道护栏

--allowedTools 限定 Worker 只能用列出的工具,逗号分隔,支持通配:

claude -p "..." \
  --allowedTools "Read,Edit,Write,Bash(git *),Bash(npm test)"

常见问题:Worker 报"Tool not allowed"。原因:prompt 触发了一个未在白名单里的工具。解决:要么放宽白名单,要么改 prompt 让模型不依赖该工具。生产中建议先收紧、再按需放开。

2.3 --max-turns 给单 Worker 设硬上限

无人值守必须设回合预算,否则一个 Worker 卡在死循环能烧掉一整夜的额度:

claude -p "重构这个模块" --max-turns 25 --output-format stream-json

--max-turns 25 会在第 25 个回合强制终止,result 事件 subtype 会标记非成功。

2.4 断点续跑:--resume 与 --continue

# 第一次跑,记住 session_id
SID=$(claude -p "..." --output-format stream-json | \
      grep '"type":"system"' | jq -r '.session_id' | head -1)

# 之后基于这个会话继续(保留全部上下文)
claude -p "继续刚才的重构,跑一下测试" --resume "$SID" --output-format stream-json

--continue(-c)则直接续最近一次会话,适合交互式补刀;批处理里更推荐显式 --resume <id>,避免多 Worker 抢同一个"最近会话"。


三、Hooks 策略层:每个 Worker 的内部刹车

Hooks 定义在 .claude/settings.json(项目级)或 ~/.claude/settings.json(用户级)。批处理时,我们通常把策略放在队列共享的项目根 .claude/settings.json,让每个 claude -p 进程自动继承。

3.1 配置骨架

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": ".claude/hooks/guard-bash.sh" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": ".claude/hooks/auto-fmt.sh" }]
      }
    ],
    "Stop": [
      {
        "hooks": [{ "type": "command", "command": ".claude/hooks/budget-guard.sh" }]
      }
    ]
  }
}

matcher 是正则,匹配工具名(Bash、Edit、Write、Read 等),留空表示匹配所有工具。

3.2 Hook 的输入与退出码语义

每个 hook 进程通过 stdin 收到一段 JSON,关键字段:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/repo/worktree-3",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "git push --force origin main", "description": "..." }
}

退出码决定行为:

退出码 含义(PreToolUse)
0 放行;stdout 可作为额外上下文喂给模型
2 阻断该工具调用,stderr 内容会回显给模型(让它知道为什么被拦)
其他非零 非阻断的错误,harness 会告警但继续

3.3 实战护栏 1:禁止危险 Bash(PreToolUse)

.claude/hooks/guard-bash.sh:

#!/usr/bin/env bash
# 读取 hook 输入
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')

# 危险模式黑名单(确定性拦截,不依赖模型判断)
deny_patterns='rm[[:space:]]+-rf|git[[:space:]]+push.*--force|git[[:space:]]+push.*-f|>[[:space:]]*/dev/sd|chmod[[:space:]]+-R[[:space:]]+777'

if echo "$cmd" | grep -Eq "$deny_patterns"; then
  echo "⛔ 被护栏拦截:检测到危险命令模式 → $cmd" >&2
  exit 2   # 阻断,stderr 回显给模型
fi

# 放行
exit 0

chmod +x 之后,任何 Worker 试图跑 git push --force 都会被挡下,模型会收到"被护栏拦截"的反馈并自行改路。

常见坑:很多人在 Edit/Write 上也用 exit 2 想阻止写入,但忘了 Edit 的 tool_input 结构不同(有 file_path/old_string/new_string)。匹配器要用对应的字段,否则永远拦不住。

3.4 实战护栏 2:写后自动格式化(PostToolUse)

.claude/hooks/auto-fmt.sh:

#!/usr/bin/env bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // ""')

[ -z "$file" ] || [ ! -f "$file" ] && exit 0

case "$file" in
  *.py)   ruff format "$file" 2>/dev/null; ruff check --fix "$file" 2>/dev/null ;;
  *.ts|*.tsx|*.js) npx --no-install prettier --write "$file" 2>/dev/null ;;
  *.go)   gofmt -w "$file" 2>/dev/null ;;
esac
exit 0   # PostToolUse 用 0,格式化失败也不阻断

这样每个 Worker 改完文件立即格式化,批跑结束后仓库就是干净的,无需单独跑 lint。

3.5 实战护栏 3:回合预算熔断(Stop)

Stop 事件在模型准备结束回合时触发。我们可以在这里读取累计成本,超阈值就强制继续(continue: false 配合 reason),或者干脆记录告警。

.claude/hooks/budget-guard.sh(轻量版:只告警不强行续):

#!/usr/bin/env bash
input=$(cat)
transcript=$(echo "$input" | jq -r '.transcript_path')

# 从 transcript 估算 token 用量,超阈值就输出一个醒目提醒
# stdout 会被注入为额外上下文(exit 0 时)
echo "⏱️ Worker 即将停止,请确认任务已完成且测试通过。" 
exit 0

若要硬性强制模型继续工作直到测试通过,可以让脚本输出 JSON:

# 强制继续(让模型别急着停)
printf '{"continue": true, "stopReason": "测试尚未全部通过,继续工作"}'
exit 0

注意:continue: true 配合"必须满足某条件才停"的 prompt 容易造成无限循环。务必同时用 --max-turns 做硬上限兜底。

3.6 高级:用 stdout JSON 精确控制

除了退出码,hook 还能向 stdout 输出结构化 JSON 做更精细控制,关键字段:

{
  "continue": false,
  "stopReason": "预算耗尽",
  "suppressOutput": false,
  "additionalContext": "(注入到对话的额外上下文)"
}

例如 UserPromptSubmit hook 可以给每个 Worker 的输入注入共享上下文(编码规范、文件清单),避免在每个 prompt 里重复粘贴。


四、调度层:把任务队列扇出到 Worker 池

4.1 任务队列格式

jobs.jsonl,每行一个独立任务:

{"id":"auth","repo":"https://github.com/acme/svc-auth.git","task":"把所有 print 换成 logging,并补类型注解"}
{"id":"billing","repo":"https://github.com/acme/svc-billing.git","task":"为 /invoice 接口补 pytest 覆盖到 80%"}
{"id":"gateway","repo":"https://github.com/acme/svc-gateway.git","task":"把 requests 替换为 httpx,保持行为不变"}

每行一个任务的好处:jq/xargs 处理天然友好,断点续跑只需删掉已完成的行。

4.2 单 Worker 封装脚本

worker.sh 接收一行 job,做四件事:克隆/切 worktree → 跑 claude -p(带护栏与预算)→ 抽取结果 → 写报告。

#!/usr/bin/env bash
# worker.sh —— 单个 Worker 的生命周期
set -u
LINE="$1"
OUT_DIR="${2:-./out}"

id=$(echo "$LINE"   | jq -r '.id')
repo=$(echo "$LINE"  | jq -r '.repo')
task=$(echo "$LINE"  | jq -r '.task')

WORK=".work/$id"
LOG="$OUT_DIR/$id.jsonl"
mkdir -p "$WORK" "$OUT_DIR"

# 1) 获取代码(已存在则 fetch)
if [ -d "$WORK/.git" ]; then
  git -C "$WORK" fetch --quiet && git -C "$WORK" reset --hard origin/HEAD -q
else
  git clone --quiet --depth 1 "$repo" "$WORK"
fi

# 把共享 hooks 策略软链进去(策略唯一来源)
mkdir -p "$WORK/.claude/hooks"
ln -sf "$(pwd)/.claude/settings.json" "$WORK/.claude/settings.json"
ln -sf "$(pwd)/.claude/hooks/"* "$WORK/.claude/hooks/" 2>/dev/null || true

# 2) 跑 Worker(带工具白名单 + 回合预算 + stream-json)
cd "$WORK"
claude -p "$task" \
  --output-format stream-json \
  --allowedTools "Read,Edit,Write,Bash(git *),Bash(pytest*),Bash(npm test)" \
  --max-turns 30 \
  > "$OUT_DIR/$id.jsonl" 2> "$OUT_DIR/$id.err"

cd - >/dev/null

# 3) 抽取结果摘要
sid=$(grep '"type":"system"' "$LOG" | head -1 | jq -r '.session_id')
result=$(grep '"type":"result"' "$LOG" | head -1)
cost=$(echo "$result" | jq -r '.cost_usd // "?"')
turns=$(echo "$result" | jq -r '.num_turns // "?"')
ok=$(echo "$result" | jq -r '.is_error // "true"')

# 4) 汇报行(供后续聚合)
printf '%s\t%s\t%s\t%s\t%s\n' "$id" "$ok" "$cost" "$turns" "$sid" >> "$OUT_DIR/_summary.tsv"
echo "[$id] done ok=$ok cost=\$$cost turns=$turns session=$sid"

隔离要点:每个 Worker 用独立的 .work/<id> 目录克隆(或 git worktree add),从根上避免多进程写同一棵树。git clone --depth 1 让大批仓库快速就绪。

4.3 并发扇出:xargs -P 与 parallel 两种姿势

# 姿势 A:xargs -P(POSIX 通用)
mkdir -p out
cat jobs.jsonl | xargs -P 4 -I{} ./worker.sh {} ./out

# 姿势 B:GNU parallel(输出更整齐,自带进度)
mkdir -p out
parallel --bar -j 4 ./worker.sh {} ./out :::: jobs.jsonl

-P 4 / -j 4 = 最多 4 个 Worker 同时跑。并发度选择经验:

4.4 汇聚报告

# 把 _summary.tsv 渲染成 Markdown 总表
{
  echo "# 批处理报告"
  echo
  echo "| 任务 | 成功 | 成本(\$) | 回合数 | session_id |"
  echo "|---|---|---|---|---|"
  sort out/_summary.tsv | awk -F'\t' '{printf "| %s | %s | %s | %s | `%s` |\n",$1,$2,$3,$4,$5}'
  echo
  total=$(awk -F'\t' '{s+=$3} END{print s}' out/_summary.tsv)
  echo "**总成本:\$${total}**"
} > report.md

五、完整实战:跨 3 个仓库的并发代码改造

把上述拼成一个一键脚本 batch.sh:

#!/usr/bin/env bash
set -euo pipefail

JOBS="${1:-jobs.jsonl}"
PARALLEL="${2:-4}"
rm -rf out .work && mkdir -p out .work

echo "→ 扇出 $PARALLEL 个 Worker 处理 $(wc -l < "$JOBS") 个任务"
parallel -j "$PARALLEL" ./worker.sh {} ./out :::: "$JOBS"

echo "→ 汇聚报告"
./report.sh        # 即上一节的 awk 片段
echo "完成,见 report.md"

预期结果:out/<id>.jsonl 各自记录完整事件流;out/_summary.tsv 汇总每任务成败/成本/会话;report.md 是给人看的总表。任一任务失败(is_error: true),可直接用其 session_id 续跑:

claude -p "上一次中断了,继续完成并把测试跑绿" --resume <session_id>

常见问题:部分 Worker 报 is_error。先看 out/<id>.err 与 transcript 末尾。九成是护栏阻断(说明策略生效了)或 --allowedTools 太紧(放宽)、或单任务超 --max-turns(拆任务或上调预算)。

六、安全与成本红线

维度 手段
越权写 PreToolUse 黑名单 rm -rf / git push -f / 写 /etc
密钥泄露 PreToolUse 对 Write/Bash 扫描 AKIA*、sk-、-----BEGIN 等模式,命中即 exit 2
预算失控 每个 Worker --max-turns + Stop hook 双保险
并发写冲突 每 Worker 独立 worktree/clone,绝不共享工作区
速率限制 并发从 4 起步,监控 429,xargs/parallel 失败自动重排
可审计 全量保留 *.jsonl 事件流,含每步工具调用与成本

七、进阶:用 Subagent 在单 Worker 内并行

当单个任务内部有可并行的子项(如一个大仓里改 10 个互不相关的模块),不必起 10 个 claude -p,而是在一个 Worker 内用 Subagent 扇出。在仓库的 .claude/agents/ 下定义:

# .claude/agents/refactor-worker.yaml
---
name: refactor-worker
description: 独立重构一个模块并跑测试,用于批量子任务并行
tools: Read, Edit, Write, Bash(git *), Bash(pytest*)
model: sonnet
---
你是模块重构专员。只改分配给你的模块目录,完成后必须运行测试并报告结果。
不要触碰其他模块。

主 Worker(claude -p)在 prompt 里被指示"对每个模块派发一个 refactor-worker 子代理并行处理",harness 会并发调度它们。子代理与外层 Worker 池的区别:子代理共享同一仓库上下文但有独立 token 窗口,适合"同仓多模块";外层 Worker 池适合"多仓多任务"。两者可叠加:外层多仓 × 内层多模块。

八、何时该用哪一层

flowchart TD
    A[批量任务需求] --> B{任务间共享代码库?}
    B -- 多个独立仓库 --> C[外层 Worker 池<br/>xargs -P + claude -p]
    B -- 同一仓库多模块 --> D[Subagent 内层扇出]
    A --> E{需要确定性安全约束?}
    E -- 是 --> F[PreToolUse/PostToolUse Hooks]
    E -- 仅需预算控制 --> G[Stop hook + --max-turns]
    C --> F
    D --> F
    F --> Z[汇聚 stream-json → report]
    G --> Z

延伸阅读

训练营适用性评估

参考来源

  1. Run Claude Code programmatically(Headless 官方文档) — -p、--output-format stream-json、--resume、--allowedTools 的权威定义
  2. Hooks reference(Claude Code 官方文档) — 生命周期事件、matcher 正则、stdin JSON、退出码 2 阻断语义
  3. Create custom subagents(Claude Code 官方文档) — .claude/agents/*.yaml frontmatter 与并行子代理调度
  4. Claude Code Headless Mode: The Complete Self-Hosting Guide (2026) — CI/CD 与夜间批跑的 auth、成本追踪、并发模式
  5. How Do I Run Parallel Batch Operations with Claude Code CLI? — xargs -P / parallel 配合 --allowedTools 的并发实战与速率控制经验

[!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 等场景全覆盖。

51学通信知识星球海报-扫码加入