AIが生成したコードをデバッグする方法:体系的アプローチ

Dev.to / 2026/4/19

💬 オピニオンSignals & Early TrendsIdeas & Deep AnalysisTools & Practical Usage

要点

  • AIコーディングツールは「ハッピーパス」ではうまくいきやすい一方で、引用された調査によれば約43%のAI生成プロジェクトは本番投入前に実際のデバッグが必要になる。
  • エラーをAIに「修正担当」として任せると、症状を取り繕うだけで根本原因が残り、別の場所で新たな不具合を生むことで「デバッグの罠」にはまる可能性がある。
  • 記事は、AIは制御できるデバッグ手順の中で“アシスタント”として使い、デバッグの思考や判断を丸投げしないことを主張している。
  • 体系的な5ステップのデバッグ手法を提案しており、まずはバグを再現可能にするために、トリガー手順、入力値、期待される結果/実際の結果、エラー情報やスタックトレースを正確に記録することから始める。

AIコードにおけるデバッグの罠

あなたはCursorやBoltで何かすごいものを作りました。デモではうまく動いた。ところが、あるユーザーが少し違う入力で試したら、動かなくなってしまいました。エラーをAIに貼り付けると、AIは自信満々に関数を書き換えます。そして今度は、別のものが壊れます。ようこそデバッグの罠へ。

GitClearなどの調査によると、AIが生成したプロジェクトの約43%は、本番投入の前に実際のデバッグ作業が必要だそうです。AIコーディングツールは、ハッピーパス(プロンプトで説明した流れ)には強い。一方で、語られない端(空の配列、ゼロ値、nullのユーザー、タイムゾーン境界、競合状態)には弱いのです。

そして罠はこれです。何かが壊れたとき、自然な次の行動はエラーをAIに貼り付けて「直して」と言うことです。AIは指示されたとおりに動きます。症状をつぎはぎで直す。するとバグは別の場所へ移動します。あなたはそのエラーをまた貼り付ける。繰り返す。すると、コードベースはガムテープのようなもので繋ぎ合わせられ、しかも誰も(あなたもAIも)いま実際に何が起きているのか理解できなくなります。

もっと良い方法があります。魔法ではありません。エンジニアが何十年も実践してきたのと同じデバッグの規律を、意図的にAIが書いたコードへ適用するだけです。

「AIに直してって頼めばいい」はなぜ機能しないのか

AIはすばらしいデバッグアシスタントですが、ひどいデバッグドライバーです。理由は次のとおりです:

  • AIは、どこが動いているのかを知りません。AIの視点では、ファイル内のすべてが怪しいものに見えます。うまく動いているコードに対しても「直した」つもりで直してしまい、実際のバグをそのままにすることがよくあります。
  • 再現ケースがないと、AIは推測します。「ときどき合計が間違っている」と言っても、AIにはいつなぜ間違うのか分かりません。もっともらしい見た目のコードを生成して、もっともらしい問題に対処します。もっともらしいことは正しいことではありません。
  • 「修正」ごとに新しいバグが混入し得ます。AIはあなたのアプリの実行時挙動を見られません。実際の値も観測できません。パターンに基づいてコードを書いているだけで、根拠(エビデンス)ではありません。
  • 修正の深さ(fix-depth)の問題。AIは原因ではなく症状を直す傾向があります。合計が間違っている?呼び出し側にチェックを追加。nullでクラッシュする?try/catchで包む。けれど本当のバグ—上流、つまり悪いデータが生まれた場所—は生き残り、次々に新しい症状を生み続けます。

解決策は、デバッグにおける考える部分を外注するのをやめることです。AIを、あなたが管理するプロセスの中のツールとして使ってください。

システマティックな5ステップのデバッグ手法

Step 1: 確実に再現する

再現できないものは直せません。何より先に、バグが要求どおりに発生するようにします。

次を書き出してください:

  • それを引き起こす正確な手順
  • 正確な入力値
  • 期待される出力
  • 実際の出力(エラーメッセージやスタックトレースを含む)

次に、それを縮小(shrink)してみます。これは最小再現ケース(MRC: minimal reproduction case)と呼ばれます。バグに関係ないものはすべて取り除きます。もし、20個のフィールドを持つフォームを送信するとバグが出るなら、3個で再現できますか?1個で?MRCが小さいほど、以降のすべてのステップが速く進みます。

確実に再現できないなら、「直す」に飛ばないでください。再現できるまで粘ってください。再現できない断続的なバグは、「実際に直せたか」を検証できないバグです。

Step 2: スコープを切り分ける

再現できるようになったら、バグがどこにいるのかを特定します。答えが「コードベース全体」になることはほとんどありません。

うまくいく手法:

  • 二分探索。コードパスの半分をコメントアウトします。バグはまだ起きますか?起きるなら、残った半分のどこかにあります。起きないなら、コメントアウトした半分のどこかにあります。見つかるまで半分ずつ切り詰めます。
  • 境界でログを取る。疑わしいパスの中で、各関数の入口と出口にconsole.logを置きます。入力と出力を表示してください。バグは「正しそうに見える最後のログ」と「間違って見える最初のログ」の間にあります。
  • Git bisect。以前は動いていたのに今は動かないなら、git bisectがバグを導入したコミットを特定します。
// 疑わしいパス: submit -> validate -> calculateTotal -> persist
function handleSubmit(order) {
  console.log('[submit] input:', order);
  const valid = validate(order);
  console.log('[submit] validated:', valid);
  const total = calculateTotal(valid);
  console.log('[submit] total:', total);
  return persist(valid, total);
}

つまらない?はい。でも効果は抜群です。ログ4つで、検索空間はだいたい半分になります。

Step 3: 仮説を立てる(推測ではない)

スコープを絞り込んだら、そこで一度止まって考えます。コードを変更する前に、次を書き出してください:

  • 何が起きていると思う? 1文で。
  • それが正しいことを証明するには? 特定のログ値、特定の状態、特定のコードパスなど。
  • それが間違っていることを証明するには? あなたが間違っている可能性があるから、早く知りたいはずです。

なんだか細かい話に聞こえます。ですが違います。仮説と推測の違いは、仮説が反証可能だという点です。「割引が二重に適用されているかも」は仮説です—確認できます。「価格のどこかが間違ってる」は雰囲気で、雰囲気の修正に繋がります。

Step 4: 雰囲気ではなくエビデンスで検証する

仮説を現実に照らしてテストします。実際の値を読みます。決めつけず、観測してください。

  • console.logで、特定の変数を特定の行で出す
  • debugger;ステートメントを挿入して、DevToolsでステップ実行する
  • エディタのデバッガでブレークポイントを設定する

これもAIの出力に当てはまります。AIが「バグはこの行でdiscountがundefinedになっていることだ」と言ったなら、鵜呑みにせず—console.log(discount)して確認してください。AIはだいたい合っていることが多いものの、正確に一致しているとは限りません。確信に満ちた誤診に基づいて動くのは、検証して確かめるよりも時間を浪費します。

信じるが検証するは、AIに対してもあなた自身の前提に対しても同じです。

Step 5: 症状ではなく根本を直す

仮説が正しいことを確認したら、いちばん近くに見える症状をつぎはぎで直したくなる衝動をこらえます。上流まで追跡しましょう。

関数が間違った数を返す場合、呼び出し側に if (total < 0) total = 0 を追加しないでください。次のように問いかけましょう:なぜ間違った数を返したのですか?不正な入力はどこから来ていますか?そこを直してください。

よくあるアンチパターンは、症状を if 文と try/catch で包み込むことです。エラーが消えるので進歩したように感じます。ですが悪いデータはまだシステムを流れ続けています。あなたはその叫び声を小さくしただけです。1か月後、同じ根本原因が別の形で再び現れます。

デバッグ中にAIを効果的に使う方法

AIは、このプロセスの「上に」置くのではなく「中に」入れるべきです。特定の役割を持つ鋭い道具として使いましょう:

  • AIにMRCを渡す。ファイル全部は渡さない。 10行の再現は、文脈400行よりもあなたもAIも考えやすいです。
  • 実際の証拠を共有する。 エラーメッセージ、スタックトレース、入力値、期待される出力と実際の出力。「動かない」で終わらせないでください。
  • コードを書く前に、AIに仮説を説明させる。 「何が起きていると思う?なぜそう思う?」説明があなたが観測した内容と一致しないなら、修正を書かせてはいけません。
  • 修正が機能しなければ、ステップ1に戻る。 AIに「もう一度試して」とは頼まないでください。そうすると、コツコツと10ラウンドの手裏剣(的当て)ゲームみたいになります。修正が失敗したなら、仮説が間違っていました。再現し直し、切り分け直し、再度仮説を立て直してください。

加えて:良いテストはデバッグを劇的に速くします。すでに「動くことが証明されている」コードの部分を教えてくれるからです。テストはデバッグ中に回帰(リグレッション)を検出するので、いま追っているバグを追いかけながら他のものを偶然壊してしまうことを防げます。

実際のデバッグ例

具体的にしてみましょう。ここに、割引を使って注文の合計金額を計算するための関数があります。これはAIが生成したものです:

// pricing.ts
export function calculateTotal(
  price: number,
  quantity: number,
  discountPercent: number
): number {
  const subtotal = price * quantity;
  const discount = subtotal * (discountPercent / 100);
  return subtotal - discount;
}

ユーザーから、「割引を適用しないと合計が NaN になる」という報告があります。では5つのステップで見ていきましょう。

ステップ1 — 再現する。 テストかREPLで:

calculateTotal(100, 2, 0);    // 期待: 200、実際: NaN
calculateTotal(100, 2, null); // NaN
calculateTotal(100, 2);       // NaN

確認できました:discountPercent0null、または未指定(missing)のときに壊れます。

ステップ2 — 切り分ける。 ログを追加:

console.log('price:', price, 'quantity:', quantity, 'discount%:', discountPercent);
const subtotal = price * quantity;
console.log('subtotal:', subtotal);
const discount = subtotal * (discountPercent / 100);
console.log('discount:', discount);

ゼロのケースの出力は subtotal: 200discount: 0、total: 200 でした。つまりゼロのケースは実際には問題ありません。問題は未指定(missing)のケースで、discount: NaN が表示されます。なぜなら undefined / 100NaN だからです。

ステップ3 — 仮説。 discountPercent が未指定(引数として渡されない)である場合、undefined / 100NaN を生成し、それが次に subtotal - discount を汚染する、ということです。

ステップ4 — 確認する。 DevToolsで console.log(undefined / 100) を実行すると NaN が出ます。確認できました。

ステップ5 — 根本を直す。 根本原因は、この関数が「割引なし」のケースを扱っていないことです。呼び出し側をパッチしないで、シグネチャを直してください:

// pricing.ts
export function calculateTotal(
  price: number,
  quantity: number,
  discountPercent: number = 0
): number {
  const subtotal = price * quantity;
  const discount = subtotal * (discountPercent / 100);
  return subtotal - discount;
}

たった1文字でバグが直りました。でも、その5つのステップがなければ、どの1文字かは分からなかったはずです。AIに「my total is NaN, fix it(合計がNaNになるので直して)」と貼り付けるより、どれだけ役に立つか分かるでしょう。AIは関数全体を書き直したり、try/catchを追加したり、念のためすべての入力を数値に強制したりするかもしれません。

デバッグツール:知っておくべき「バイブ」

小さなツールキットでも、あれば十分です:

  • ブラウザの開発者ツール(DevTools)。 コンソール、Network、Sources。特にSourcesタブを学びましょう——ブレークポイント、ウォッチ式、コールスタックが、どんなAIのプロンプトよりもあなたを助けてくれます。
  • 戦略的なconsole.log 境界でログを出し、変数名と値を表示します(console.log('user:', user))。終わったら片付けることも忘れずに。
  • debuggerステートメント。 コードのどこかにdebugger;を置けば、DevToolsを開いているとそこで実行が停止します。1行ずつステップ実行して進めましょう。
  • エラートラッキング(Sentryの無料プラン)。 本番環境で、実際のユーザーから発生したエラーを、完全なスタックトレースと失敗時の状態付きでキャプチャします。暗闇の中でデバッグすることがなくなります。

正直なところ、日常的な多くのバグに対しては、DevToolsを知っている開発者のほうが、どんなAIよりも速いです。AIは可能性を推論しないといけません。あなたは実際の値を見られます。

デバッグが設計上の問題を明らかにするとき

問題が、目の前のバグそのものではないこともあります。バグの周りにあるコードの「形」こそが問題です。

次のサインに気をつけてください:

  • 同じファイルやモジュールで、何度もバグ修正をしている
  • どの修正も、ほかの2つの問題を壊してしまう
  • 入力から出力へデータがどう流れるのか説明できない
  • 同じロジックが3か所に重複していて、それぞれ直さないといけない

こうなったとき、デバッグはもっと大きなことを教えてくれます。コードが強く結合されていて、抽象化が欠けていて、データフローが不明確だということです。必要なのは——追加のパッチではなく——リファクタリングです。

そして、どんなバグ修正も何か別のものを壊しているように見えるなら、デバッグで切り抜けられる地点を過ぎているのかもしれません。そうなったら、上にどんどん載せて出荷を続ける前に、コードベースを安定させるために助けを呼ぶタイミングです。

まとめ

デバッグはスキルであって、魔法のAIトリックではありません。適切にデバッグできるたびに、次のバグに対して確実にあなたは良くなります——バグが隠れる場所、手を伸ばすべきツール、そしてどのAIの修正を信用すべきかについて、直感が育っていきます。

AIは、現実のプロセスの中で使うとデバッグを加速します。代わりに使うと、デバッグの役割を置き換えてしまいます。違いは、あなたが主導権を持っているか、それとも「直して」というループに閉じ込められてコードが判別できなくなってもバグがまだ残っているのか、です。

次に何かが壊れたら:再現する、切り分ける、仮説を立てる、検証する、根本を直す。必ずこの順番です。毎回。

もともとは bivecode.com に掲載されました。AIによって作られたコードベースが手に負えなくなっていて困っているなら、BiveCode Rescue が助けになります。