首页 › tutorials › 多进程并行批量改造配质量钩子

概述

本教程聚焦 Claude Code 自动化能力中最被低估的一组组合拳:用 -p 无头模式启动多个独立进程,靠 xargs -P 在操作系统层并行批处理海量文件,再用 PostToolUse 钩子在每个编辑后自动跑校验、失败即阻断。适合已经装通 Claude Code、写过一两个 Skill 或跑过单次 -p 命令、想把"一次改一个文件"升级成"一夜改完一个仓库"的中阶用户。学完你能搭出一条带质量闸口、可重跑、可审计的批量改造流水线。

前置条件


一、为什么是"多进程",而不是"子代理"

Claude Code 内部有两种并行思路,先厘清边界,才不会用错工具:

维度 会话内子代理(Task/Agent) 多进程 -p 并行(本教程)
并行单元 一个会话内 fork 多个 subagent 操作系统级多个 claude 进程
上下文 子代理可共享父会话摘要 每个进程完全独立、互不可见
协调方式 由主模型统一编排 由 shell(xargs/GNU parallel)调度
适合场景 一个复杂任务拆成几块强相关子任务 大量同质、相互独立的文件级作业
失败隔离 一个子代理崩可能污染主会话 一个进程崩不影响其他进程

判断口诀:任务是"几块拼成一个整体"用子代理;任务是"一百个文件各改各的"用多进程并行。本教程处理后者——比如把一个有 200 个 .js 文件的遗留仓库,从回调地狱整体迁到 async/await。每个文件独立、彼此无依赖,天然适合进程级并行。

进程级并行的额外好处是故障隔离彻底:第 37 个文件改坏了,只是那一个进程的 JSON 输出标记 is_error,其余 199 个照常完成;你不必为一个坏文件重启整个批处理。

二、理解 -p 无头模式的五个关键旗标

无头模式(headless / print mode)是批量管道的发动机。把以下五个旗标吃透,管道就稳了一半。

claude -p "你的指令" \
  --permission-mode acceptEdits \
  --max-turns 15 \
  --append-system-prompt "附加约束" \
  --output-format json

1. -p / --print:非交互模式。读取一条指令、执行、输出、退出,绝不弹交互提示。这是能被 shell 调度的前提。

2. --permission-mode:无头模式下没有人在终端点"允许",必须预先决定授权策略。四个取值: - default:每个工具调用按 settings.json 的 allow 规则匹配,匹配不到就卡住——批处理里这是灾难 - acceptEdits:自动放行文件编辑类工具(Edit/Write),其余仍按规则——批量改造的首选 - plan:只规划不执行,适合先做 dry-run - bypassPermissions:全部放行,速度最快但风险最高,仅用于隔离的可信沙箱

批处理推荐 acceptEdits + 一份精简的 allow 清单,既不卡住也不裸奔。

3. --max-turns N:限制单次调用的最大代理轮次。批处理的铁律是必须设上限。否则某个进程遇到诡异文件、陷入"改了又错、错了又改"的死循环,会烧掉整个预算。经验值:简单迁移 8–12 轮,复杂重构 15–20 轮。

4. --append-system-prompt:在默认系统提示后追加你的约束。这是批量作业里统一行为的关键——比如"只改指定文件,禁止新建文件"、"保持对外函数签名不变"、"改动不超过 50 行"。不用它的话,每个进程的"自由发挥"差异会很大。

5. --output-format: - text(默认):纯文本结果,人看舒服,机器解析难 - json:任务结束后一次性吐一个 JSON 对象,含 result、session_id、num_turns、is_error、total_cost_usd、usage——批处理的标配,方便后续审计与重跑 - stream-json:流式逐条 JSON,适合实时观测或做进度条

陷阱提示:还有一个 --bare 旗标,作用是"跳过钩子、技能等的自动发现,换取更快的冷启动"。如果你的管道依赖钩子做质量收口(本教程正是如此),就绝对不能用 --bare——它会把你的钩子一起跳过。--bare 适合纯无副作用的查询管道,不适合本场景。

三、质量闸口:PostToolUse 钩子工作原理

并行多进程解决了"快",但快不等于对。200 个文件同时改,谁保证每个改完都能编译、能过 lint?靠人工抽检不现实。钩子(Hook)就是那条自动质量闸口。

Claude Code 在代理生命周期的关键节点会触发钩子。最常用的几个:

事件 触发时机 典型用途
SessionStart 会话开始 注入项目上下文、加载规则
UserPromptSubmit 用户提交指令 改写/校验输入
PreToolUse 工具执行前 拦截危险命令、二次授权
PostToolUse 工具执行后 自动 lint/format/测试,失败回灌
Stop 主代理回合结束 汇总通知、收尾校验
SubagentStop 子代理结束 子任务结果校验

我们的核心是 PostToolUse + matcher: "Edit|Write|MultiEdit":每当模型完成一次文件编辑,钩子脚本立即对该文件跑校验,把结果回灌给模型。

钩子的"语言"是退出码与 JSON:

钩子通过 stdin 收到一段 JSON,关键字段:

{
  "session_id": "abc123",
  "hook_event_name": "PostToolUse",
  "tool_name": "Edit",
  "tool_input": { "file_path": "/proj/legacy/cart.js", "old_string": "...", "new_string": "..." },
  "tool_response": { ... },
  "cwd": "/proj"
}

钩子脚本的任务:从 tool_input.file_path 取出刚改的文件,决定要不要校验,跑校验,按结果决定退出码。整条链路在 settings.json 里注册:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/scripts/lint-on-edit.sh" }
        ]
      }
    ]
  }
}

$CLAUDE_PROJECT_DIR 会被自动展开为项目根目录。matcher 用正则匹配工具名,写 "Edit|Write|MultiEdit" 就只在这三类编辑工具后触发。

四、动手:搭建并行批量管道

下面分七步搭出一条完整的"回调→async/await 批量迁移"流水线。先把目录建好:

mkdir -p async-migrate/{legacy,scripts,out} && cd async-migrate

步骤 1 准备样本代码

在 legacy/ 放几个回调风格的 .js 文件。这里给一个典型样本 legacy/cart.js:

// legacy/cart.js —— 经典回调地狱
function fetchCart(userId, cb) {
  db.get(userId, function (err, user) {
    if (err) return cb(err);
    db.get(user.cartId, function (err, cart) {
      if (err) return cb(err);
      db.query(cart.items, function (err, items) {
        if (err) return cb(err);
        cb(null, items);
      });
    });
  });
}

再复制几份、改改函数名,凑出 6–10 个文件用于并行演示。真实场景里直接指向你的遗留目录即可。

步骤 2 配置 ESLint 基线

npm init -y >/dev/null
npm install --save-dev eslint >/dev/null
npx eslint --init   # 选默认即可,生成 .eslintrc

ESLint 在这里充当"客观裁判"。钩子不评价代码美不美,只问"能不能过 lint"——这是可自动化、可重复的质量底线。

步骤 3 编写 lint 钩子脚本

新建 scripts/lint-on-edit.sh:

#!/usr/bin/env bash
# PostToolUse 钩子:编辑 .js 文件后自动跑 eslint,失败即阻断
set -u

input=$(cat)
event=$(printf '%s' "$input" | jq -r '.hook_event_name')
[[ "$event" == "PostToolUse" ]] || exit 0

file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
case "$file_path" in
  *.js) ;;              # 只校验 .js
  *) exit 0 ;;
esac

[[ -f "$file_path" ]] || exit 0   # 文件可能被删,跳过

if ! out=$(npx --no-install eslint --no-eslintrc -c .eslintrc "$file_path" 2>&1); then
  # 退出码 2 = 阻断错误,stderr 回灌给 Claude,迫使其继续修复
  printf 'ESLint 校验失败(%s),请修复后再次编辑:\n%s\n' "$file_path" "$out" >&2
  exit 2
fi
exit 0
chmod +x scripts/lint-on-edit.sh

预期结果:脚本对非 .js 文件、不存在的文件直接放行;对 .js 文件跑 eslint,通过则退出 0,失败则把错误打到 stderr 并退出 2。

常见问题: - 钩子不触发:检查 chmod +x 是否执行、command 路径是否用绝对路径或 $CLAUDE_PROJECT_DIR。 - eslint 卡在交互:npx --no-install 避免触发安装提示;或直接用本地 ./node_modules/.bin/eslint。 - 钩子把整个进程拖慢:eslint 比较重,可换更轻的 node --check(只查语法)做第一道闸,复杂规则留给最后的 Stop 钩子。

步骤 4 注册钩子到 settings.json

在项目根创建 .claude/settings.json:

{
  "permissions": {
    "allow": ["Bash(npx eslint:*)", "Bash(./node_modules/.bin/eslint:*)", "Edit", "Write", "Read"]
  },
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/scripts/lint-on-edit.sh" }
        ]
      }
    ]
  }
}

permissions.allow 预授权了 eslint 和编辑类工具,避免 acceptEdits 模式下仍因未授权而卡住。

步骤 5 单文件试跑与自愈观察

先单文件验证整条链路,别一上来就并行:

claude -p "把 legacy/cart.js 里所有回调式异步改写成 async/await,保持 fetchCart 的对外签名与错误语义不变。" \
  --permission-mode acceptEdits \
  --max-turns 12 \
  --append-system-prompt "只允许修改 legacy/cart.js 这一个文件,禁止新建任何文件。" \
  --output-format json | jq '{ok: .is_error, turns: .num_turns, cost: .total_cost_usd, session: .session_id}'

预期结果:模型读取文件 → 编辑 → 触发钩子跑 eslint。若第一版编辑有语法问题,钩子退出 2、错误回灌,模型会再改一轮,直到 eslint 通过或撞到 --max-turns 上限。最终 JSON 输出 is_error: false、turns 通常 2–5。

常见问题: - 模型无视钩子反馈:确认钩子退出码确实是 2(不是 1)。退出码 1 是非阻断,模型不会因此重试。 - 改动溢出到别的文件:--append-system-prompt 里把"禁止新建/改其他文件"写死,必要时在 permissions 里只 allow 目标路径。

步骤 6 多进程并行批跑

单文件跑通后,扩到并行。核心是 xargs -P N:

mkdir -p out
find legacy -name '*.js' -printf '%f\n' | \
  xargs -I{} -P 4 sh -c '
    claude -p "把 legacy/{} 里所有回调式异步改写成 async/await,保持函数签名与错误语义不变。" \
      --permission-mode acceptEdits \
      --max-turns 15 \
      --append-system-prompt "只允许修改 legacy/{} 这一个文件,禁止新建文件;改完确保能通过 eslint。" \
      --output-format json > "out/{}.json" 2> "out/{}.log"
  '

-P 4 表示同时跑 4 个 claude 进程。你的 CPU 核数、API 并发上限、预算共同决定这个值。

预期结果:4 个进程并发,每个进程独立加载自己的钩子、独立编辑自己的文件、独立跑 eslint。终端看不到交互,全部结果落盘到 out/。10 个文件、-P 4 大约相当于串行耗时的 1/3 到 1/4(受 API 延迟主导时加速比更接近并发数)。

常见问题: - API 限流(429):并发太高,把 -P 调小,或在 xargs 外层加 sleep 节流。 - 进程互相踩文件:确保每个 -p 实例只改一个文件(靠 prompt + system prompt 双重约束)。本设计的进程隔离前提就是"输入互不重叠"。 - 想看实时进度:把 --output-format json 换成 --output-format stream-json,配合 jq --unbuffered 过滤事件类型,能看到每个进程的工具调用流。但落盘用 json 更省心。

步骤 7 结构化结果与失败重跑

批处理跑完,第一件事是生成清单、揪出失败项:

# 汇总每个文件的结果
for f in out/*.json; do
  jq -r '"\(.is_error)\t\(.num_turns)\t\(.total_cost_usd)\t\(.session_id)\t'"$f" "$f"
done | column -t -N 'error,turns,cost,session,file'

对失败项,用 --resume <session_id> 接着原上下文重跑,而不是从零开始——模型记得自己改到哪、钩子报了什么错:

# 假设 cart.json 标记 is_error=true
SID=$(jq -r '.session_id' out/cart.json)
claude -p "继续完成上次的 async/await 迁移,确保最终能通过 eslint。" \
  --resume "$SID" \
  --permission-mode acceptEdits \
  --max-turns 15 \
  --output-format json > out/cart.retry.json

--resume 续跑的是同一会话,模型带着完整的编辑历史与钩子反馈。这比对失败文件重新发一条新指令更高效——尤其当失败原因是"改到一半撞了轮次上限"时。

五、实战案例:一夜迁完 200 文件的完整脚本

把上面七步封装成一个可复用的批处理脚本 run-batch.sh:

#!/usr/bin/env bash
# 用法: ./run-batch.sh <目录> <并发数> <迁移指令文件>
set -euo pipefail

DIR="${1:-legacy}"
PARALLEL="${2:-4}"
PROMPT_FILE="${3:-prompt.txt}"
OUT=out; mkdir -p "$OUT"

INSTRUCTION=$(cat "$PROMPT_FILE")

find "$DIR" -name '*.js' -printf '%P\n' | while read -r rel; do
  printf '%s\n' "$rel"
done | xargs -I{} -P "$PARALLEL" sh -c '
  rel="$1"; instr="$2"; out="out/$rel.json"; log="out/$rel.log"
  mkdir -p "$(dirname "$out")"
  claude -p "对文件 $rel 执行:$instr" \
    --permission-mode acceptEdits \
    --max-turns 15 \
    --append-system-prompt "只允许修改 $rel 这一个文件,禁止新建文件,改完必须能通过 eslint。" \
    --output-format json > "$out" 2> "$log" \
    && echo "[OK]   $rel" \
    || echo "[FAIL] $rel (见 $log)"
' _ {} "$INSTRUCTION"

echo "---- 批处理完成,汇总 ----"
ok=0; fail=0
for f in out/**/*.json out/*.json; do
  [[ -f "$f" ]] || continue
  if jq -e '.is_error == false' "$f" >/dev/null 2>&1; then ok=$((ok+1)); else fail=$((fail+1)); fi
done
echo "成功: $ok 失败: $fail"

# 自动重跑失败项
for f in out/*.json; do
  [[ -f "$f" ]] || continue
  if ! jq -e '.is_error == false' "$f" >/dev/null 2>&1; then
    sid=$(jq -r '.session_id' "$f")
    [[ "$sid" != "null" ]] || continue
    echo "[RETRY] $f session=$sid"
    claude -p "继续完成迁移任务,确保通过 eslint。" --resume "$sid" \
      --permission-mode acceptEdits --max-turns 15 --output-format json > "${f%.json}.retry.json"
  fi
done

配套 prompt.txt:

把该文件中所有基于回调(callback / function(err, ...))的异步逻辑改写为 async/await。
要求:保持每个对外导出函数的签名与错误语义不变;Promise 化内部调用;移除冗余回调嵌套。

这个脚本一次性完成"分发 → 并行执行 → 钩子收口 → 结果汇总 → 失败重跑"全链路。

六、流程与架构图

整体管道架构:

flowchart LR
    A[find 列出 N 个文件] --> B[xargs -P 4 并行分发]
    B --> P1[claude -p 进程1]
    B --> P2[claude -p 进程2]
    B --> P3[claude -p 进程3]
    B --> P4[claude -p 进程4]

    subgraph 每个进程独立闭环
      P1 --> E1[Edit 编辑文件]
      E1 --> H{PostToolUse 钩子}
      H -->|eslint 通过| C1[继续/完成]
      H -->|eslint 失败 exit 2| E1
    end

    C1 --> O1[out/file1.json]
    P2 --> O2[out/file2.json]
    P3 --> O3[out/file3.json]
    P4 --> O4[out/file4.json]

    O1 --> S[汇总清单]
    O2 --> S
    O3 --> S
    O4 --> S
    S --> R{is_error?}
    R -->|true| RR[--resume 重跑]
    R -->|false| DONE[完成]

钩子内部决策流:

sequenceDiagram
    participant M as 模型(代理)
    participant T as Claude Code 运行时
    participant K as lint-on-edit.sh
    participant L as eslint

    M->>T: 调用 Edit(file_path)
    T->>T: 执行编辑
    T->>K: PostToolUse 事件(stdin=JSON)
    K->>K: 解析 file_path,判断 .js
    K->>L: eslint file_path
    L-->>K: 结果
    alt 通过
        K-->>T: exit 0
        T-->>M: 编辑成功,继续
    else 失败
        K-->>T: stderr=错误详情, exit 2
        T-->>M: 阻断错误回灌上下文
        M->>T: 再次调用 Edit 修复
    end

七、性能与成本调优

八、常见问题速查

延伸阅读

训练营适用性评估

参考来源

  1. Run Claude Code programmatically — Claude Code Docs — 官方无头模式与 Agent SDK 编程入口,--bare、--permission-mode 等旗标权威说明
  2. Hooks reference — Claude Code Docs — 钩子事件、JSON 输入输出 schema、退出码(0/2/其他)与 $CLAUDE_PROJECT_DIR 展开的官方规范
  3. Claude Code Hooks: PreToolUse & PostToolUse Tutorial — paul-schick.com — 把 CLAUDE.md 约束落成可执行闸口的实战范例
  4. Claude Code Batch Processing Complete Guide — smartscope.blog — /batch 命令与 claude -p 大规模批处理对比,含并发与重跑策略
  5. CLI reference — Claude Code Docs — --output-format、--max-turns、--append-system-prompt、--resume 等全部旗标速查

[!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学通信知识星球海报-扫码加入