首页 › tutorials › git bisect 配 Claude 自动揪出坏提交

git bisect 配 Claude 自动揪出坏提交:让无头模式当"语义法官"

概述

当你知道"线上坏了",却不知道是过去几百次提交里的哪一次把代码改坏了,git bisect 是标准答案——但它需要一个能自动判定"这个版本好还是坏"的脚本。传统做法是手写一个复现用例,可一旦回归是"输出语气不对""布局错位""某个边界语义错了"这类难以量化的 bug,脚本根本写不出来。本教程教你把 Claude Code 的 -p 无头模式塞进 git bisect run,让大模型充当语义法官:每个被二分到的提交,自动读代码、按 bug 描述判定好坏、吐出结构化结论,最终无人值守地定位到那一个"罪魁祸首"提交。适合已熟悉 Git 基本操作、想把 Claude Code 接进 CI 与自动化排查流程的后端/前端工程师与 SRE。

前置条件


正文

步骤 1:理解"二分 + 法官"的工作原理

git bisect 在好提交(good)和坏提交(bad)之间做二分搜索:每次 checkout 到中点,由你(或一个脚本)告诉它"这个版本是好是坏",它再砍一半,直到锁定第一个引入回归的提交。git bisect run <script> 把"告诉它好坏"这一步交给脚本——脚本的退出码就是判词:

脚本退出码 含义
0 当前提交是 good(没有该 bug)
1–124 当前提交是 bad(有该 bug)
125 跳过此提交(编不过、跑不了,无法判定)
128 及以上 中止整个 bisect

关键洞察:脚本内部用什么手段得出退出码,git 完全不关心。 传统脚本跑一个测试套件,而我们要做的,是让脚本调用 claude -p——把"读代码 + 对照 bug 描述 + 下判词"这件事交给模型。这就是"语义法官"模式。

它的价值在"难量化回归"上才真正凸显:

步骤 2:圈定 good 与 bad,写清楚"案情描述"

bisect 的输入是两个锚点。先确认它们:

# 当前 HEAD 已知是坏的(线上故障复现于此)
git checkout main
BAD=$(git rev-parse HEAD)

# 翻到一个你确信"还没坏"的旧版本,比如上个发布 tag
GOOD=v1.4.0
git log --oneline $GOOD..$BAD | wc -l   # 看看要二分的区间有多大

接着准备一份案情描述,放进 bug-report.md。写得越具体、越可验证,法官判得越准:

# 回归案情

## 现象
调用 `grade.letter_grade(90)` 期望返回 "A",但在 HEAD 上返回了 "B"。
score 恰好等于分级阈值(90/80/70/60)时,等级偏低一档。

## 判定标准(法官请据此判 good/bad)
- 阅读 src/grade.py 中的 letter_grade 函数。
- 若阈值的比较运算符是 `>=`(含等号),判 **good**。
- 若任一阈值写成 `>`(不含等号),判 **bad**。
- 只看 letter_grade 的阈值比较,不要被其它改动干扰。

常见问题:区间太大、提交太多怎么办? 先用 git bisect visualize 或 git log --oneline 看一眼,必要时用 git bisect skip 跳过明显无关的合并提交。区间每翻倍,调用次数只多一次,不必过度收窄。

步骤 3:编写调用 Claude 的判定脚本

新建 judge.sh,它的职责是:读当前 checkout 出来的代码 → 调 claude -p → 解析结论 → 退出 0/1/125。

#!/usr/bin/env bash
# judge.sh — 让 Claude 当语义法官,判定当前提交是 good 还是 bad
set -uo pipefail

REPORT="bug-report.md"

# --bare:CI/脚本推荐模式,跳过本地 hooks/skills/MCP/CLAUDE.md,
#         保证每台机器结果一致。认证必须来自 ANTHROPIC_API_KEY。
# --allowedTools "Read":法官只需读代码,不给写权限,避免误改文件污染工作区。
# --output-format json + --json-schema:强制模型返回固定结构,便于 jq 解析。
OUT=$(claude --bare -p \
  --allowedTools "Read" \
  --output-format json \
  --json-schema '{
    "type":"object",
    "properties":{
      "verdict":{"type":"string","enum":["good","bad"]},
      "reason":{"type":"string"}
    },
    "required":["verdict","reason"]
  }' \
  "你是回归判定法官。请先用 Read 工具读取 src/grade.py,然后阅读 $(cat $REPORT) 中的判定标准。严格按照标准输出 verdict:阈值含等号=good,不含等号=bad。不要被其它改动干扰。" \
  2>/dev/null)

# 模型调用本身失败(额度、网络)→ 退出 125 跳过此提交,而不是误判
if [ -z "$OUT" ]; then
  echo "judge: empty response, skipping this commit" >&2
  exit 125
fi

VERDICT=$(echo "$OUT" | jq -r '.structured_output.verdict // empty' 2>/dev/null)
REASON=$(echo "$OUT" | jq -r '.structured_output.reason // "(no reason)"' 2>/dev/null)

case "$VERDICT" in
  good) echo "judge: GOOD  — $REASON"; exit 0 ;;
  bad)  echo "judge: BAD   — $REASON"; exit 1 ;;
  *)    echo "judge: AMBIGUOUS ($VERDICT), skipping — $REASON" >&2; exit 125 ;;
esac

几个关键设计:

  1. --bare 是脚本场景的推荐模式。 官方文档明确:它会跳过自动发现的 hooks、skills、plugins、MCP 服务器和 CLAUDE.md,只认你显式传入的 flag。这意味着你队友 ~/.claude 里某个奇怪的 hook 不会污染判定结果,CI 上每台机器行为一致。--bare 将成为 -p 的未来默认值。
  2. --allowedTools "Read" 用的是权限规则语法,只给只读权限——法官不能改代码,否则它在某个提交上"顺手修一下"会毁掉工作区,让 bisect 的 checkout 失败。
  3. --json-schema 强制结构化输出,结论落在 .structured_output.verdict。这是让"模型自由文本"变成"可解析判词"的关键,比正则匹配可靠得多。
  4. 模型失败 → exit 125(跳过),而不是 exit 1。 限流或网络抖动不该被当成"这个提交有 bug"。125 让 bisect 跳过当前点、继续推进,事后你可以对这些 skip 点单独复查。

常见问题:法官判得不稳,同一段代码时好时坏怎么办? 三招:(a) 把判定标准写到"非黑即白"——明确给出"看到 X 就是 good,看到 Y 就是 bad",避免开放式判断;(b) 用 --append-system-prompt "只依据判定标准,不要引入外部知识";(c) 把要判定的文件缩小到子目录,减少噪声。若仍不稳,可在脚本里对该提交连判 3 次取多数票。

步骤 4:让 git bisect run 全自动跑起来

赋予执行权限,启动二分:

chmod +x judge.sh

git bisect start
git bisect bad  "$BAD"
git bisect good "$GOOD"
git bisect run ./judge.sh

之后你就可以去倒杯咖啡。git 会反复 checkout、调用 judge.sh、根据退出码砍半。某次运行的输出大致长这样:

running ./judge.sh
judge: GOOD  — letter_grade uses >= for all thresholds
Bisecting: 37 revisions left to test after this (roughly 5 steps)
[abc1234] refactor grade thresholds
running ./judge.sh
judge: BAD   — line 12 uses > 90 instead of >= 90
Bisecting: 18 revisions left to test after this (roughly 4 steps)
...
def5678 is the first bad commit
commit def5678
Author: ...
    refactor: simplify grade thresholds

最后一行 def5678 is the first bad commit 就是凶手。看一下它改了什么:

git bisect reset          # 收尾,回到原来的分支
git show def5678          # 复盘这次回归到底动了哪几行

常见问题:bisect 跑到一半,工作区被法官弄脏了? 因为给了 Read-only 权限,理论上不会改文件;若你放宽了权限,git bisect reset --hard 可强制清回干净状态后重启。跑到一半想换 bug 描述? 直接改 bug-report.md,已判过的提交结论不会重算——若想全部重来,git bisect reset 再重新 start。

步骤 5:在 CI 里固化(--bare、续跑、成本可见)

把这套流程搬进 CI 时,重点是用 --bare 保证可重现,并捕获会话 ID 以便失败续跑。下面是一个可直接放进 .github/workflows/bisect.yml(或 GitLab CI、Jenkins)的骨架:

name: auto-bisect
on:
  workflow_dispatch:
    inputs:
      good: { required: true }   # 例如 v1.4.0
jobs:
  bisect:
    runs-on: ubuntu-latest
    env:
      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}   # --bare 不读 keychain,必须用环境变量
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }          # bisect 需要完整历史
      - run: chmod +x scripts/judge.sh
      - run: |
          git bisect start
          git bisect bad  ${{ github.sha }}
          git bisect good ${{ inputs.good }}
          git bisect run ./scripts/judge.sh
          git bisect reset

几个 CI 专属要点:

步骤 6:用 Stop 钩子收尾通知与归档

bisect 跑完往往在 CI 或长脚本里,没人盯着。可以用 Claude Code 的 Stop 钩子做收尾:当一次 -p 调用结束,自动把"法官结论 + 凶手提交"推到 IM 或写进归档。

在项目 .claude/settings.json 里加(这是可提交、团队共享的位置):

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/scripts/on-judge-done.sh"
          }
        ]
      }
    ]
  }
}

on-judge-done.sh 从 stdin 读 JSON(钩子输入字段在 stdin),提取结论:

#!/usr/bin/env bash
# on-judge-done.sh — 每次 -p 法官判定结束后触发
IN=$(cat)
REASON=$(echo "$IN" | jq -r '. // empty' 2>/dev/null)
# 这里可换成飞书/钉钉/Slack webhook,或追加写入 bisect 归档日志
echo "[$(date -u +%FT%TZ)] judge turn finished" >> "${CLAUDE_PROJECT_DIR}/bisect.log"

钩子配置分三层:hook event(如 Stop)→ matcher group → hook handler。matcher 为空表示对该事件的每次发生都触发。注意 --bare 模式不会读取这些钩子配置——所以"收尾通知"钩子要在交互式或非 bare 的编排脚本里才生效;纯 CI 的 --bare 调用想发通知,直接在 judge.sh 末尾 curl 一个 webhook 更直接。


实战案例:5 分钟造一个回归并自动定位

下面是一个从零开始、完整可复现的练手项目。我们造一个 grade 模块,故意在中间某次提交引入阈值 off-by-one 回归,然后用 judge.sh 自动揪出它。

1) 建仓库 + 三个提交(前两个 good,第三个 bad):

mkdir bisect-lab && cd bisect-lab && git init -q
mkdir -p src

# commit 1 (good):初版,阈值用 >=
cat > src/grade.py <<'PY'
def letter_grade(score):
    if score >= 90: return "A"
    if score >= 80: return "B"
    if score >= 70: return "C"
    if score >= 60: return "D"
    return "F"
PY
git add -A && git commit -qm "feat: add letter_grade"

# commit 2 (good):加文档,逻辑不变
cat >> src/grade.py <<'PY'

# Convert a 0-100 numeric score into a letter grade.
PY
git add -A && git commit -qm "docs: document letter_grade"

GOOD=$(git rev-parse HEAD)

# commit 3 (bad):重构时把第一个阈值改成了 > (引入回归!)
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("src/grade.py")
t = p.read_text().replace("score >= 90", "score > 90")
p.write_text(t)
PY
git add -A && git commit -qm "refactor: simplify grade thresholds"

BAD=$(git rev-parse HEAD)
echo "GOOD=$GOOD  BAD=$BAD"

2) 写案情描述(沿用步骤 2 的 bug-report.md):

cat > bug-report.md <<'MD'
# 判定标准
- 阅读 src/grade.py 的 letter_grade。
- 所有阈值比较含 `>=`(等号)→ good。
- 任一阈值写成 `>`(无等号)→ bad。
- 只看阈值运算符,忽略其它改动。
MD

3) 把步骤 3 的 judge.sh 放进项目,赋予执行权限。

4) 一键自动二分:

git bisect start
git bisect bad  "$BAD"
git bisect good "$GOOD"
git bisect run ./judge.sh

预期结果: 法官在前两个提交上看到 >=,判 GOOD(exit 0);在第三个提交上看到 > 90,判 BAD(exit 1)。git 大约 1–2 步即可锁定,最终输出:

<第三个提交的短 hash> is the first bad commit

5) 复盘与收尾:

git bisect reset
git show HEAD     # 确认就是那个 "refactor: simplify grade thresholds"

这个案例虽然小,但完整覆盖了"语义法官"的全部要素:没有写任何针对 letter_grade 的断言测试,纯靠模型读代码 + 标准描述就完成了定位。把它换成你项目里"CSV 表头顺序""错误信息措辞""生成 SQL 的 JOIN 缺失"这类难量化回归,套路一模一样。


工作流程图

flowchart TD
    A[确认 BAD=HEAD<br/>GOOD=旧 tag] --> B[编写 bug-report.md<br/>判定标准]
    B --> C[编写 judge.sh<br/>调用 claude --bare -p]
    C --> D[git bisect start<br/>good / bad / run judge.sh]
    D --> E{git checkout 中点提交}
    E --> F[judge.sh 读取当前代码]
    F --> G[claude -p 当法官<br/>--json-schema 强制 verdict]
    G --> H{verdict?}
    H -- good --> I[exit 0<br/>git 砍向更新的那一半]
    H -- bad --> J[exit 1<br/>git 砍向更旧的那一半]
    H -- 调用失败/模糊 --> K[exit 125<br/>跳过此提交]
    I --> L{区间收敛?}
    J --> L
    K --> L
    L -- 否 --> E
    L -- 是 --> M[输出 first bad commit<br/>git bisect reset]
    M --> N[Stop 钩子 / webhook<br/>归档通知]

常见问题与陷阱


延伸阅读


训练营适用性评估


参考来源

  1. Run Claude Code programmatically — Claude Code Docs — -p / --bare / --output-format / --json-schema / --allowedTools / --resume 等 headless 模式 flag 的官方权威说明
  2. Hooks reference — Claude Code Docs — Stop / PostToolUse / Notification 等钩子事件、matcher 规则、JSON 输入输出与退出码语义
  3. git-bisect — Git Documentation — git bisect run 的退出码契约(0=good / 1-124=bad / 125=skip / 128=abort)与二分编排机制
  4. How to bisect a regression with Claude Code — Vibe Coding Skills — 社区 bisect + Claude 实战 recipe,可对比"AI 生成测试再 run"的变体思路
  5. How to Build AI-Powered Git Bisect Automation — bestremotetools — AI 驱动 bisect 自动化的整体工作流与脚本组织参考

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