首页 › tutorials › 造自愈Agent:失败自检与自动修复闭环

概述

本教程教你用 Claude Code 的 Agent 模式(-p 无头运行)+ Hooks 钩子 + 外层重试循环,搭建一个「写代码 → 跑测试 → 失败自动诊断 → 自动修复 → 重试」的自愈型 Agent。学完你能让 Claude Code 在无人值守下把自己写的代码改到测试通过为止,并掌握权限模式选择、钩子拦截、失控防护三大自动化关键。适合已有 Claude Code 基础、想让 AI 真正「端到端干活」的开发者与 DevOps 工程师。

前置条件


第一步:理解自愈闭环的三个角色

让 Agent「自愈」不是一句 prompt 就能搞定的魔法,而是三个明确角色的配合。把它想清楚,后面每一行配置都有归属:

角色 职责 在 Claude Code 里由谁扮演
执行者(Agent) 读代码、改代码、调用工具 claude -p 无头进程本身
验证器(Verifier) 客观判定「现在对不对」 测试命令 / 类型检查 / Lint(npm test 等)
调度器(Orchestrator) 决定「再试一次还是停」 外层 Bash 循环 + Stop 钩子

自愈的本质是闭环反馈:验证器的失败信号,必须能流回执行者,触发下一轮修改。少了反馈通道,Agent 改完就停,永远是「单次尝试」而非「自愈」。

很多人卡在这一步——以为加了 --dangerously-skip-permissions 就叫自动化,其实那只是「不再问你」,并不会让 Agent 自己验证结果。真正的自动化 = 无人值守 × 自我验证 × 失败重试,三者缺一不可。

预期结果:你能用一句话说清自己要搭的系统里,谁负责改、谁负责判、谁负责循环。

常见问题: - 「Agent 改了但没说改对没」 → 缺验证器,必须有一条非交互、有明确退出码的测试命令。 - 「Agent 一直说完成了,其实没通过」 → 见第六步,用 Stop 钩子卡住「假完成」。


第二步:选对权限模式,让 Agent 无人值守

Claude Code 有四种权限模式,选错要么被频繁打断,要么门户大开。Shift+Tab 可在交互里循环切换,但自愈 Agent 是无头的,要用命令行参数固化。

# 查看当前模式相关设置
claude config get defaultMode 2>/dev/null || true

四种模式对比:

模式 标志 文件编辑 危险命令/网络 适合场景
default 默认 每次询问 询问 人盯着
acceptEdits --permission-mode acceptEdits 自动接受 仍询问 自愈 Agent 首选
plan --permission-mode plan 禁止(只读) 禁止 只调研不改
bypassPermissions --dangerously-skip-permissions 自动接受 自动接受 沙箱/容器内激进

关键决策:自愈 Agent 推荐用 acceptEdits 而不是 bypassPermissions。前者让 Agent 自由改文件,但碰到 rm -rf、git push --force、读 .env 这类危险操作仍会停下问你——这正是你要的安全垫。bypassPermissions(俗称 YOLO 模式)只在一次性容器或隔离 worktree 里用,因为它对一切放行。

预期结果:你能用一句话向同事解释为什么自愈循环用 acceptEdits 而不是 --dangerously-skip-permissions。

常见问题: - 「循环跑到一半停下来等确认」 → 某条 Bash 命令或网络请求没在允许列表里。要么改用 acceptEdits,要么把该命令加进 settings.json 的 permissions.allow。 - 「plan 模式下 Agent 不改文件」 → 这是设计如此,plan 模式只读。要让它动手必须切回 acceptEdits 或 default。


第三步:用无头模式拿到结构化结果

自愈调度器要靠机器可读的结果做决策,不能靠「肉眼读终端」。-p(即 --print)让 Claude Code 跑完即退,配合 --output-format 拿到结构化输出。

# 最简无头调用:改完即退,stdout 是纯文本结果
claude -p "把 src/utils.js 里的 add 函数修好" \
  --permission-mode acceptEdits

# 拿 JSON 结果,便于脚本解析
claude -p "修复 add 函数" \
  --permission-mode acceptEdits \
  --output-format json \
  > result.json
# result.json 含 result 文本、cost_usd、duration_ms、is_error 等字段

# 流式 JSON:每个事件一行,适合长任务监控
claude -p "重构整个 utils 模块" \
  --permission-mode acceptEdits \
  --output-format stream-json \
  --verbose

--output-format json 返回的退出码同样可靠:进程退出码 0 = 正常结束,非 0 = 出错。但注意——「Agent 正常结束」≠「代码正确」。这正是下一步要补的洞。

预期结果:跑一次上面的命令,能看到 result.json 里有 is_error 字段,并能用 jq -r .is_error result.json 取出布尔值。

常见问题: - 「流式输出刷屏」 → 加 --include-partial-messages 控制粒度,或干脆用非流式 json。 - 「Agent 跑很久不退出」 → 加 --max-turns 20 设硬上限,防止单轮无限循环(见第七步)。


第四步:用 PostToolUse 钩子挂「即时自动测试」

钩子(Hooks)是 Claude Code 的确定性自动化层——它不是 LLM 判断,而是事件触发的脚本,保证每次都执行。这是它和「在 prompt 里叮嘱 Agent 跑测试」的根本区别:叮嘱可能被忽略,钩子不会。

钩子配置写在 .claude/settings.json(项目级)或 ~/.claude/settings.json(全局)。PostToolUse 在每次工具调用之后触发,matcher 按工具名过滤。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | { read f; case \"$f\" in *.js|*.ts) npx prettier --write \"$f\" >/dev/null 2>&1 ;; esac; }"
          }
        ]
      }
    ]
  }
}

这段做的事:Agent 每编辑一个 .js/.ts 文件,钩子立刻用 Prettier 格式化它。stdin 传进来的是一段 JSON,含 tool_input.file_path,用 jq 取出即可。

把格式化换成「跑受影响文件的单元测试」,就是即时自愈的第一道反馈:

{
  "type": "command",
  "command": "npm test -- --silent >/tmp/quick.log 2>&1 || (echo '⚠️ 测试失败,相关错误已记录' >> /tmp/feedback.txt; tail -5 /tmp/quick.log >> /tmp/feedback.txt)"
}

预期结果:编辑任意 .js 文件后,无需手动操作,文件已被格式化;测试失败时 /tmp/feedback.txt 里累积了错误片段。

常见问题: - 「钩子没触发」 → 90% 是 JSON 语法错。用 cat .claude/settings.json | jq . 验证能否解析;matcher 用正则,Edit|Write 这种竖线分隔必须有效。 - 「钩子拖慢每次编辑」 → 别在 PostToolUse 跑全量测试套件,只跑该文件的快速测试,全量交给第六步的 Stop 钩子。


第五步:外层 Bash 重试循环——自愈的真正引擎

钩子负责「改完立刻反馈」,但要实现「失败就自动再改、改到通过为止」,需要一个有边界的外层循环。这是调度器的核心:

#!/usr/bin/env bash
# heal.sh — 自愈循环:让 Agent 把测试改到通过
set -uo pipefail

MAX_RETRIES="${MAX_RETRIES:-5}"
TASK="$1"
FEEDBACK=""

for i in $(seq 1 "$MAX_RETRIES"); do
  echo "=== 第 $i / $MAX_RETRIES 轮 ==="

  # 角色1·执行者:Agent 在无头模式下改代码
  claude -p "${TASK}${FEEDBACK}" \
    --permission-mode acceptEdits \
    --output-format json \
    > /tmp/agent.json 2>/dev/null || true

  # 角色2·验证器:客观判定
  if npm test > /tmp/test.log 2>&1; then
    echo "✅ 第 $i 轮修复成功,测试通过"
    exit 0
  fi

  # 失败 → 角色3·调度器:把错误喂回执行者,进入下一轮
  echo "❌ 测试失败,提取错误反馈给 Agent"
  FEEDBACK=$'\n\n注意:上一轮改动后测试仍未通过,错误如下,请只针对失败点修复:\n'"$(tail -25 /tmp/test.log)"
done

echo "🔥 达到最大重试次数 $MAX_RETRIES 仍未通过,请人工介入"
echo "   最后一次测试日志:/tmp/test.log"
exit 1

运行:

chmod +x heal.sh
./heal.sh "修复 src/utils.js 中 add 函数的错误,使测试通过"

这个脚本的精髓在 FEEDBACK 变量——每轮把上一轮的真实测试错误拼进下一轮的 prompt,闭环就此闭合。MAX_RETRIES 是防失控的第一道闸(见第七步)。

预期结果:终端依次打印「第 1 轮…失败」「第 2 轮…失败」「第 3 轮…✅修复成功」,退出码为 0;若 5 轮没修好,退出码 1 并留下日志。

常见问题: - 「Agent 每轮都重复同一个错误修法」 → 反馈文本里追加「不要重复上一轮无效的修改」,并要求它先复述失败原因再动手。 - 「npm test 退出码不准」 → 某些测试框架默认即使有失败也返回 0。检查 package.json 的 test 脚本,确保失败时非 0(如 mocha ... || exit 1)。


第六步:用 Stop 钩子卡住「假完成」

Agent 有个通病:明明测试没过,它却自信地说「已修复,任务完成」。第五步的外层循环能兜底,但更优雅的做法是在 Agent 即将结束的那一刻强制验证——这就是 Stop 钩子。

写一个门禁脚本 stop-gate.sh:

#!/usr/bin/env bash
# stop-gate.sh — Agent 想结束前,先跑测试
if npm test > /tmp/gate.log 2>&1; then
  exit 0                        # 通过 → 允许结束
fi

# 失败 → 输出 JSON,decision=block 把原因喂回 Agent,禁止它停下
REASON=$(tail -15 /tmp/gate.log | tr '\n' ' ' | sed 's/"/\\"/g')
cat <<EOF
{"decision":"block","reason":"测试未通过,禁止结束。请先修复:${REASON}"}
EOF
exit 0

注册到 settings.json:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [{ "type": "command", "command": "bash .claude/stop-gate.sh" }]
      }
    ]
  }
}

机制:Agent 每次想停下,Stop 钩子跑测试;不通过就返回 decision: block,这个 reason 会被当作新反馈塞回 Agent,逼它继续改。Claude Code 内置 stop_hook_active 标志防止钩子无限递归——当 Stop 是因自身反馈而再次触发时,系统会安全放行,不会死循环。

于是第五步的 Bash 循环和第六步的 Stop 钩子形成双层自愈:钩子做「单轮内」的反复修,Bash 循环做「跨进程」的总重试。

预期结果:即便 Agent 自信宣布完成,只要测试没过它就会被钩子顶回去继续干;最终真正退出时,测试必然是绿的。

常见问题: - 「Stop 钩子把 Agent 卡到死」 → 必须配 --max-turns(下一步),且确认 stop_hook_active 逻辑生效;钩子脚本本身要有 exit 0 兜底,别让脚本异常导致行为不可预测。 - 「reason 里 JSON 转义炸了」 → 用 jq 构造 JSON 而非手工拼接:jq -nc --arg r "$REASON" '{decision:"block",reason:$r}'。


第七步:加三道边界,防止 Agent 失控

自动化最大的风险是「跑飞了」。三道闸缺一不可:

① 危险操作拦截(PreToolUse,退出码 2 = 阻断)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "command": "jq -r '.tool_input.command' | grep -qE 'rm -rf|\\.env|git push --force|curl .*\\| *sh' && exit 2 || exit 0"
        }]
      }
    ]
  }
}

退出码 2 是 Claude Code 的硬约定——PreToolUse 钩子返回 2,该工具调用被取消,Agent 收到「被阻止」的反馈。

② 单轮硬上限:--max-turns 30,防 Agent 在一个进程里无限折腾。

③ 隔离运行环境:在 Git worktree 或容器里跑自愈循环,改动不污染主分支。改完用 git diff 审查、git checkout . 一键回滚:

git worktree add ../heal-sandbox -b heal/$(date +%s)
cd ../heal-sandbox && ../project/heal.sh "修复 X"
# 满意 → git commit;不满意 → git worktree remove .

预期结果:即便 Agent 试图 rm -rf,调用被即时阻止;单轮最多 30 步;所有改动隔离在 worktree,主仓库零风险。


实战案例:一个会自愈的「坏计算器」

把以上七步捏成一个完整例子。项目结构:

heal-demo/
├── src/utils.js        # 故意写错的 add 函数
├── test/utils.test.js  # 会失败的测试
├── package.json        # test 脚本
├── .claude/
│   ├── settings.json   # 三类钩子
│   ├── stop-gate.sh
└── heal.sh             # 自愈循环

src/utils.js(故意有 bug:把 + 写成了 -):

function add(a, b) {
  return a - b;   // 故意写错
}
module.exports = { add };

test/utils.test.js:

const assert = require('assert');
const { add } = require('../src/utils');
assert.strictEqual(add(2, 3), 5, 'add(2,3) 应等于 5');
console.log('✅ 测试通过');

package.json:

{ "scripts": { "test": "node test/utils.test.js" } }

.claude/settings.json(整合第四、六、七步的钩子):

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command",
        "command": "jq -r '.tool_input.command' | grep -qE 'rm -rf|git push --force' && exit 2 || exit 0" }]
    }],
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{ "type": "command",
        "command": "jq -r '.tool_input.file_path' | grep -qE '\\.js$' && echo '已记录编辑' >> /tmp/edits.log || true" }]
    }],
    "Stop": [{
      "matcher": "",
      "hooks": [{ "type": "command", "command": "bash .claude/stop-gate.sh" }]
    }]
  }
}

一键启动:

cd heal-demo
./heal.sh "让 src/utils.js 的 add 函数返回两数之和,使 npm test 通过"

典型运行轨迹:

=== 第 1 / 5 轮 ===
❌ 测试失败,提取错误反馈给 Agent      # Agent 把 - 改成了 +,但可能改错地方
=== 第 2 / 5 轮 ===
✅ 第 2 轮修复成功,测试通过

期间 stop-gate.sh 会在第 1 轮里把 Agent 顶回去——它本想宣布完成,但测试红着,钩子不放行,直到 add 真的返回 a + b。整个过程无人值守,结束时 git diff 只剩一行正确改动:

-  return a - b;
+  return a + b;

把这个模式套到真实仓库,把 npm test 换成 pytest -q、go test ./...、tsc --noEmit,就是一条通用的「自愈 CI」管线。


自愈闭环工作流

flowchart TD
    A[启动 heal.sh<br/>带初始任务] --> B[执行者: claude -p 改代码<br/>acceptEdits 模式]
    B --> C[PostToolUse 钩子<br/>即时格式化/快测]
    B --> D{Agent 想结束?}
    D -->|是| E[Stop 钩子 stop-gate.sh<br/>跑全量测试]
    E -->|失败 decision=block| B
    E -->|通过| F[Agent 进程正常退出]
    F --> G[验证器: npm test<br/>判定通过/失败]
    G -->|通过 exit 0| H[✅ 自愈成功 结束]
    G -->|失败| I{已达 MAX_RETRIES?}
    I -->|否| J[把错误拼进下一轮 prompt<br/>反馈通道闭合]
    J --> B
    I -->|是| K[🔥 放弃 留日志 待人工]

    style H fill:#c8e6c9
    style K fill:#ffcdd2
    style E fill:#fff9c4

读图要点:内环(B→C→D→E→B)是单进程内由 Stop 钩子驱动的「改-验」循环;外环(A→B…→G→I→J→B)是 Bash 调度器驱动的跨进程重试。两条环都通向同一个验证器 npm test,保证「正确性」有且只有一个客观来源。


延伸阅读


训练营适用性评估


参考来源

  1. Hooks reference — Claude Code Docs — 钩子事件类型、matcher、JSON 输入输出与退出码(exit 2 阻断、Stop 的 decision=block)的官方规范。
  2. Choose a permission mode — Claude Code Docs — default/acceptEdits/plan/bypassPermissions 四模式语义与 Shift+Tab 切换。
  3. Enabling Claude Code to work more autonomously — Anthropic — 官方对自主 Agent、-p 无头模式与 Claude Agent SDK 的定位说明。
  4. Claude Code Autonomous Debugging: 7 Atomic Task Patterns — 带 pass/fail 判据的原子任务编排模式,本文重试循环的设计来源之一。
  5. Self-Healing AI Coding Agents — OpenHelm — 自愈反馈环、阈值调参与失控防护的工程实践。

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