← ブログに戻る

Claudeが3回連続でバグを「隠す修正」を出してきた話 — デバッグ10の技法をプロンプトに翻訳する

claude-codedebuggingprompt-engineeringai-codinghooks

ClaudeにAPIの500エラーを直してと頼みました。1回目はtry-catchで包んで、ログを足しました。2回目は戻り値にdefaultを入れて、呼び出し側が落ちないようにしました。3回目はexponential backoffのリトライを追加しました。

500は消えました。3回目の「修正」を私は自信満々に本番に出しました。2時間後、オンコールが起きました。同じ障害が、同じDBクライアントを共有していた別エンドポイントに移動しただけだったのです。本当の原因はコネクションプールの枯渇でした。Claudeはバグを直していたのではなく、3つの違う方法で症状を隠していました。

今日は、その「症状を隠す修正」を二度と通さないために、デバッグ10の技法をプロンプトテンプレートに翻訳した話を書きます。あわせて、一度書いたら触らなくていいCLAUDE.mdの12行と、PreToolUse / PostToolUse のhook設定もセットで紹介します。

3回連続の「症状を隠す修正」 — try-catch、default戻り値、リトライ。すべて500を消しただけで、コネクションプール枯渇は放置された。

本番に出した3つの「直し」

3回の「修正」は、単独で見ると全部それっぽく見えました。

1回目: try-catch。 ハンドラが例外を捕まえてログを吐き、ユーザーには500を返すようになりました。APIから見れば改善です。バグから見ると、エラーを起こした接続は壊れた状態のままプールに戻されました。

2回目: default戻り値。 関数が空配列を返すようになりました。このエンドポイントの500は消えました。代わりに空配列が下流のキャッシュに乗って、1時間そのまま残りました。

3回目: exponential backoffのリトライ。 リトライ3回、それぞれが新しい接続を開きました。プールはより速く枯渇しました。このエンドポイントの500は、2回目か3回目の試行で成功するようになって消えました。同じプールを使っていた他のエンドポイントが代わりにタイムアウトを返し始めました。

3回とも、私が頼んだエンドポイントの症状は消えました。原因が移動しただけです。私は「デバッグして」と頼みましたが、「症状を抑え込むのは禁止」というルールは渡していませんでした。だからClaudeは症状を抑え込みました。それが次のトークン予測がやりたいことだからです。

AIエージェントの「周辺の配管」が壊れる話は、以前 AIパイプラインに潜んでいた9つのバグ で書きました。あれはモデルの外側の話でした。今日はモデルが配管そのものを書く話です。

なぜAIは症状を隠す方向に走るのか

Stack Overflowが2025年に出した開発者調査では、プロのデベロッパーのうちAIツールを使う、または使う予定だと答えた割合がおおよそ8割。一方でAIの出力を信頼すると答えた割合は前年から下がっていました。その後の調査・記事を追いかけると、繰り返し出てくる指摘は同じです。AI生成コードのバグはロジックエラーと入出力処理にかたまっていて、同等の人間が書いたコードより明らかに密度が高い。よく引用される数字は「人間比較で約1.7倍のバグ密度」あたりです。研究ごとに測り方は違うので、引用するときは出典と前提条件を見たほうがいいです。

仕組み自体は不思議でもなんでもありません。大規模言語モデルは、文脈の続きとして最も確からしいトークンを予測する装置です。「エラーハンドリングのパターン」は学習データの中でも特に過剰に表現されています。try-catch、null-check、default戻り値、リトライ。これらは公開リポジトリで誰かが「このエラー直して」と書いたあとに、統計的に最もよく出てくる種類の編集です。モデルは学んだ通りのことをしているだけです。

足りないのは別のトークンです。「まだ根本原因が分かっていません。調査を続けます」という1文。これは学習データに少ない。人間が「まだ分かりません」をコミットしないからです。コミットされるのは修正であって、まだ見つけていない状態ではない。だからモデルは「もう少し見続ける」というデフォルトを学んでいません。

このトークンは、こちらから明示的に入れてやる必要があります。次のセクションがそれです。

デバッグ10の技法 → プロンプトテンプレート

それぞれが古典的なデバッグ技法に対応します。プロンプトに直接貼るか、CLAUDE.mdに永続化するかは「どこまで定着させたいか」で選びます。

10のデバッグ技法を10のプロンプト断片にマップ — 前提疑い、再現、境界、差分、時系列、リトライ、増幅、観測、単純化、意図的に壊す。

1. 入力を疑う。 「修正案を出す前に、参照しているログが欠損していないか、モニタリングが実際にあなたが想定している状態を報告しているかを確認してください」。これはClaudeが一番飛ばすところです。半分ローテートで切れたログから平気で診断します。

2. 修正前に再現する。 「ローカルで再現して、最小手順を提示してください。再現できない場合は、その旨を明示してそこで止まってください」。「止まってください」がこの一文の働きどころで、推測に逃げる扉を閉じます。

3. 境界を見つける。 「動いている挙動と壊れている挙動の境界を特定してください。正しいデータを返す最後のコンポーネントはどこですか」。行単位の推測ではなく、レイヤー単位の絞り込みに向かわせるための制約です。

4. 既知の正常状態との差分を取る。 「現在のコードを直近の正常稼働時点と比較してください。git log --oneline -20 を確認し、障害ウィンドウと相関しうる変更を特定してください」。これが「誰も覚えていないコミット」を炙り出します。

5. 時系列で並べる。 「いつから失敗していますか。急激ですか、徐々に悪化していますか。エラーレートをデプロイ時刻・トラフィックスパイク・設定変更にぶつけてください」。急激かつデプロイ連動と、緩慢かつ非連動は別のバグです。混同するから3回連続の修正が積みあがります。

6. リトライ・キャッシュ・タイムアウトを棚卸しする。 「経路上のリトライ・キャッシュ・タイムアウトをすべて列挙してください。各々について、下層の呼び出しが『遅いが失敗していない』状態のときに何が起きるかを説明してください」。これが入っていれば、私のプール枯渇は1回目で見つかっていたはずです。

7. 増幅パスを探す。 「小さなエラーが増幅される経路はありませんか。失敗が3回のリトライを引き起こし、それぞれが新しい接続を開き、次のリクエストにレイテンシを足していくような経路です」。リトライストームの先にオートスケーラがあると、インスタンスストームも付いてきます。

8. 観測を足す、推測しない。 「原因を特定するだけの観測情報が足りない場合は、追加すべき具体的なログ行やトレース項目を提案してください。修正案は提案しないでください」。「分かりません」を「ここを測ってください」に変換できると、嘘の修正よりはるかに有用な答えになります。

9. 単純化する。 「失敗経路から不要な要素を取り除き、最小再現形まで縮めてください。それでもバグが出る最小入力は何ですか」。問題はだいたい「見ていた部分」になかった、というのがこの技法の感想です。

10. 意図的に壊す。 「仮説を検証するために、バグを悪化させる(または改善させる)変更を意図的に提案してください。実行前に結果を予測してください」。デバッグを観察から実験に切り替える技法です。モニタリングが嘘をついているケースもこれで掘れます。

10の技法の元になる思考プロセスと原文での定式化は ハーネス・エンジニアリング — AIを”使う”から”操る”へ のデバッグ章で扱っています。プロンプト変換の章と次節のCLAUDE.md/hooks装備の章は、本記事の骨格そのものです。

CLAUDE.mdに永続化する

10文を毎回プロンプトに貼り付けるのはスケールしません。CLAUDE.mdはそのためにあります。

Anthropicが繰り返し推奨しているのは、CLAUDE.mdをだいたい100-150行に収めることです。すべてのターンでコンテキストに入る分量にしておく、という制約です。そのうち12行をデバッグルールに割り当てるのは、いい投資です。

## Debugging Rules

- 根本原因が特定できるまで修正コードを書かない。
- 症状を抑えない。症状が消えても原因が不明なら、それは修正ではない。
- 修正前に、バグを再現する失敗テストを書く。
- 修正後に全テストを通し、新たに壊れたテストがあれば報告する。
- 同じバグに対して3回連続で修正が失敗したら停止する。試した内容、除外できた仮説、残っている仮説を整理して、人間に判断を仰ぐ。

## Debugging Workflow

1. Root Cause Investigation: ログ・トレース・コード経路を読む
2. Pattern Analysis: 同じアンチパターンが他にないか検索
3. Hypothesis Testing: 仮説が正しいときだけ落ちるテストを書く
4. Implementation: 1-3を通過したあとだけ

ポイントは、これらが「指示」ではなく「制約」だということです。「まず調査してください」より「根本原因が特定できるまで修正コードを書かない」のほうが効きます。制約形式が、次のトークン予測機が嬉々として先に進むのを止めます。

CLAUDE.mdに何を書くかの全体観は、以前 CLAUDE.mdとコンテキストエンジニアリングの実践 で書いた話と地続きです。デバッグの12行は、その続編に追加するパートだと思ってください。

hooksで「反射」を自動化する

CLAUDE.mdが脳なら、hooksは反射です。デバッグに効くのは主に2つ。

PreToolUse: 破壊的コマンドをブロックする。 デバッグの途中で、たまにモデルが rm -rf node_modules を提案してきます。運の悪い日には素の DROP TABLE も来ます。PreToolUseのhookでBashツール呼び出しを横取りし、コマンド文字列を簡易な denylist にかけて、引っかかったら exit 2 でブロックします。Claude Codeは PreToolUse からの exit 2 を「このツール呼び出しは却下。モデルに理由を伝える」として扱います。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "command": "if echo \"$TOOL_INPUT\" | grep -qE 'rm\\s+-rf|DROP\\s+TABLE'; then echo 'BLOCK: destructive command' >&2; exit 2; fi"
        }]
      }
    ]
  }
}

PostToolUse: 編集後にテストを走らせる。 matcher を Edit|Write にして、command で fast subset のテストスイートを叩きます。モデルは次のターンでテスト失敗を直接見るので、30メッセージ後に思い出すのではなく、作った直後に反応します。hooksイベントの全体像は、以前 Claude Code Hooks v2 — 25のイベント で整理しました。PreToolUse / PostToolUse の挙動を含め、まずはあの記事の表で当たりをつけるのが早いです。

CLAUDE.md・PreToolUse・PostToolUse の3点セットは、AIデバッガーの装備レイヤです。1つの大きなエージェントを Observer / Strategist / Marketer の3役に分離 したときに使った「装備レイヤ」と同じパターンです。これは同じハーネス連載の、デバッグ装備回だと思ってください。

3回連続で失敗したら「人間を呼ぶ」

もっとも効くルールを最後に1つだけ。今回のオンコールを救えたのもこの1行でした。

同じバグに対して3回連続で修正が失敗したら、そこで止まって人間にエスカレートする。

3という数字に魔法はありません。「もう1回推測する」コストが「これは構造的なバグだと認めて報告する」コストを上回る境界線が、だいたいその辺りにあるだけです。3回目には、モデルはパターンマッチの上にパターンマッチを重ねている可能性が高い。4回目のリトライより、人間の目のほうが安いです。

「Claudeにデバッグさせれば速い」は半分本当で、半分嘘です。装備を渡さなければ、Claudeは最速で症状を隠します。10のプロンプトが装備を渡す手段で、CLAUDE.mdがそれを覚えてくれて、hooksがすり抜けを止めます。どれもコストはほとんどかかりません。23時にオンコールが起きるコストに比べれば、です。

10の技法をプロンプトに翻訳する章と、CLAUDE.md/hooks/MCPを3層で組む章は、ハーネス・エンジニアリング — AIを”使う”から”操る”へ にまとめてあります。

参考: