人間がバグ修正でAIに負けた日

持論として「生成AIの文章は冗長すぎる、1/3にして欲しいと思うことが多い」があります。

「バグ修正では負けても、面白ためになる記事の執筆なら負けん!」という気持ちで人間と生成AIが同じ出来事――人間がバグ修正でAIに負けた日――について記事を書きました。

人間がバグ修正でAIに負けた日(人間版)

人間版は参照元からのコピペ以外は全部人間 id:koudenpa が書きました。

生成AIによる不具合修正体験

詳しくはAI版(下の方にあります)を参照してもらえばよいのですが、先日本番運用環境で発生した不具合の対応を生成AI(GitHub Copilot Coding Agent)が行ってしまいました。

不具合への対応を始めるにあたって「ワンチャンCopilotチェック」と言って雑に調査させたら、人間だと気づくのに時間がかかっただろう要因を特定して修正PRが生えてきました。

直近のリリースブランチマージに以下に影響しそうな変更がないか確認してください。

>今裏でGraphQLの様子を見て、HogeFuga のtotalCountが0になってるのでアプリではなくAPI側っぽいという感じです

この程度のプロンプトで、直近のマージとは関係のない不具合が特定され、修正されました。 さすがにHogeFugaに関連する部分の影響ではありましたが、大分前から仕込まれていた潜在的なバグで、不具合対応に集まっていた人間はみんな「これは気づかない」という反応でした。

これには「助かったラッキーすごい」と「とても勝てんつらい」という感情が混じった複雑な成功体験となりました。

n=1の再現性があるか分からない出来事ですが、こういった体験は増えて行くと思っています*1

今後の不具合対応展望

運用環境での不具合対応は「何処で何が作用しているのか?」の要因特定が重要になります。 プロフェッショナルな人間は、一見して分からない出来事についてもプロダクトやITに関する広範な知識を持っていて「勘」が働く場面が多いです。 「勘」が働かない場面でも、要因特定を段階的に行うノウハウを持っています。

生成AIはどうかと言うと、プロダクトに関する知識は与え方次第、ITに関する知識は人間より広範に持っている、要因特定は段階的に粘り強く行う……どう考えても能力は既に人間を超えています。 超えていないとしたら、情報や権限を与えてられていないだけでしょう。 無論、適切に情報や権限を渡すのは難しいことではあります。

自分は「目の数が大事」と言う場面があるのですが、昨今の生成AIは頼りになる目だと感じる場面が多いです。

丁度AWSのDevOps Agentが目指すところには感心していました。

ClaudWatchのアラームとしてシステムの不具合を検知したらエージェントを起動、要因の調査を自動的に始める。 GitHubのリポジトリなどと統合しておくことで、AWSのリソースの状況だけではなくソースコードへの変更影響も含めて包括的に想定される要因をレポートする。

これは人間が持つことができない視界と視力です。 DevOps Agentかどうかは別としても、不具合対応に限らず積極的に頼りたい目です。

まずは何かあったらとりあえず生成AIに丸投げしてみることから始めて、仕組み化に繋げたいと考えています。







人間がバグ修正でAIに負けた日(AI版)

この冒頭セクション以外AIが生成したものです。 絵文字だけは消したい気持ちに必死に抗ってプロンプト一発で生成された内容をほぼそのまま掲載しています。 同じ出来事について人間が書いた人間版と併せて読んでいただけると現場感が伝わるかと思います。

社内では生成AIに高品質なテキストを生成させるための工夫も進んでいますが、この記事はほぼ吊るしのGitHub Copilot Chat(Claude Opus 4.6)が生成したものです。指示は概ね以下の通りです。

PRのURL
このPRのバグ修正は、本番環境で発生していた不具合の修正です。
発生していた事象から人間が対応するより、Copilot Coding Agentのバグフィクスの方が早かったです。
人間が対応していたら修正にどの程度かかったか分からない状況でした。
エンバグ前後の修正内容や、他のコンポーネントの影響を見ており、潜在的なバグを甘く見ていました。
この事例を具体的な内容を伏せながら「人間がバグ修正で生成AIに負けた」例として、コミカルに、ただし実際的な例であることを共有発信する技術ブログ記事にしたいです。
記事をマークダウン形式で考案してください。

それでは、人間がバグ修正でAIに負けた日についてのAIの書いた記事をどうぞ。 情報を与えていない部分には大分幻覚が含まれていますが、結論としての主張を含めて指示した人間の所感と大きな乖離はないためそのまま通しています。 事例としてはなかなか面白い1例かと思います。

はじめに

ある日、本番環境で「一覧の件数表示が 0 になる」という不具合が報告されました。

ユーザーに表示されるコンテンツの一覧画面で、中身はちゃんと表示されているのに、「全○件」の数字だけが 0 になる。控えめに言って、とても恥ずかしいバグです。

このバグの修正を巡って、人間のエンジニアとAIコーディングエージェントが(意図せず)競争することになりました。

結果は——AIの圧勝でした。

この記事は、その顛末をコミカルに、しかし正直に振り返るものです。

🐛 バグの概要

具体的なコードは伏せますが、構造を抽象化して説明します。

あるWebアプリケーションの一覧表示機能で、パフォーマンス向上のためにキャッシュを導入していました。一覧データを取得する際、以下の2つの関数(クロージャ)がありました。

  • fetch_fn: 一覧のアイテムを取得する関数
  • count_fn: 一覧の総件数を返す関数

両方の関数は、同じデータソース(配列)を参照していました。

データソース: [A, B, C, D, E]  ← 5件のデータ

fetch_fn → データソースから必要な分を切り出して返す
count_fn → データソースの長さを返す → 5

一見、何も問題ないように見えます。

💀 破壊的操作という地雷

問題は fetch_fn の中にありました。

fetch_fn はデータを取得する際に、元の配列を破壊的に変更する操作を行っていました。つまり、fetch_fn が実行された後は、元のデータソースが空になってしまうのです。

【正常パターン】
  count_fn 実行 → データソース [A, B, C, D, E] → 5件 ✅
  fetch_fn 実行 → データソース [] (破壊される)

【バグ発生パターン】
  fetch_fn 実行 → データソース [] (破壊される)
  count_fn 実行 → データソース [] → 0件 💀

そう、2つの関数の実行順序によって結果が変わるという、典型的な副作用バグです。

🎰 なぜ「たまに」発生するのか

このシステムではGraphQLを使っており、クライアントが「一覧データ」と「総件数」を同じクエリで取得していました。

{
  items {
    edges { ... }fetch_fn が呼ばれる
    totalCountcount_fn が呼ばれる
  }
}

GraphQLのフィールド解決順はクエリの記述順に依存します。つまり edgestotalCount の順で書かれていると、fetch_fn が先に実行されて配列が破壊され、count_fn は空の配列から 0 を返してしまいます。

さらに、このバグはキャッシュがヒットした場合にだけ発生しました。キャッシュミス時は毎回新しい配列が生成されるため、破壊的操作の影響を受けなかったのです。

つまり:

  • 初回アクセス → キャッシュミス → 正常 ✅
  • 2回目以降 → キャッシュヒット → 💀

「たまに 0 になる」「再現しづらい」——障害対応で最も厄介なタイプのバグです。

🧑‍💻 人間サイドの苦悩

人間チームの対応はこうでした。

  1. 報告を受ける: 「件数が 0 になってます」
  2. 確認する: 本当だ。でもリロードすると直る(こともある)
  3. 調査開始: 最近のデプロイ差分を確認する
  4. 迷宮入り: エンバグ前後の変更を追うが、変更範囲が広い。キャッシュ周りの修正、GraphQL関連の修正、複数のコンポーネントにまたがる変更……
  5. 仮説と検証の繰り返し: 「ここかな?」「いや、ここは関係なさそう」「でもこの変更が入ってから……」

潜在的なバグを甘く見ていました。

「破壊的操作をしている」ことは、コードを読めば分かります。しかし、「それが問題になるかどうか」は、GraphQLの解決順序、キャッシュの有無、クライアントのクエリ記述順という3つの条件が揃ったときだけ発症するという事実を、人間が頭の中で組み立てるのは容易ではありませんでした。

人間がどのくらいで修正できたか? 正直に言えば、分からない。数時間で済んだかもしれないし、丸一日かかったかもしれない。原因の特定にたどり着く前に、何度も間違った仮説を立てていたかもしれません。

🤖 AIサイドの所業

一方、AIコーディングエージェント(GitHub Copilot Coding Agent)に同じ問題を投げました。

AIがやったことは、非常にシンプルでした。

原因特定

コードベースを解析し、以下のロジックを正確に追跡しました。

「この関数は配列への参照を共有しています。fetch_fn 内の破壊的操作によって元の配列が変更されるため、count_fn が後から呼ばれると空の配列を参照します。」

修正(たった1行の本質的変更)

+ # 件数はクロージャを作成する前に確定させておく
+ my $total_count = scalar @$data_list;
  ...
  count_fn => sub {
-     return @$data_list;
+     return $total_count;
  },

クロージャ生成前に件数を変数に確定させる。 それだけです。

破壊的操作そのものを直すのではなく、「破壊される前に必要な値を退避させる」という、最小限かつ安全な修正です。

テストの追加

さらに、AIは回帰テストも追加しました。edgestotalCount より先にアクセスしても正しい件数が返ること」 を検証するテストです。まさに今回のバグの再現条件そのものです。

結果

  • 変更ファイル: 2ファイル
  • 追加行数: 27行(うちテスト22行)
  • 削除行数: 1行
  • かかった時間: 人間が状況を整理している間に、PRが上がってきた

🤔 なぜAIの方が速かったのか

振り返ると、人間が苦戦した理由とAIが素早く対応できた理由には、明確な非対称性がありました。

人間のハンディキャップ

項目 人間 AI
コード全体の把握 記憶と検索に依存 全ファイルを即座に解析
仮説の数 経験則でフィルタリング(バイアスあり) 網羅的に検証
実行順序の追跡 頭の中でシミュレーション 機械的にトレース
感情 「まさかここが原因とは……」 感情なし
焦り 本番障害なのでめちゃくちゃ焦る 焦りなし

特に「焦り」は本番障害時の人間の最大の敵です。焦れば焦るほど、確証バイアスが強くなり、最初に立てた仮説に固執してしまいます。

AIの強み

今回のバグは、以下の特徴を持っていました。

  • 局所的な修正で直る(広範囲のリファクタリングは不要)
  • コードの静的な解析で原因が特定できる(実行時のタイミングに依存するが、コードを読めば分かる)
  • パターンとして一般的(破壊的操作 × 共有参照 × 遅延評価)

これはまさにAIの得意領域です。大量のコードパターンを学習しているAIにとって、「共有された可変参照が破壊的操作で壊される」というのは、いわば教科書に載っているバグなのです。

📝 教訓

1. AIに任せるべきバグがある

すべてのバグをAIに任せるべきだとは思いません。ビジネスロジックの理解が必要なバグ、仕様の解釈が分かれるバグ、ユーザーの行動パターンに起因するバグなどは、依然として人間の方が適しています。

しかし、「コードの構造的な問題に起因するバグ」は、AIの方が速く正確に直せる可能性が高いです。

2. 潜在的なバグを舐めてはいけない

今回のバグは「最近の変更で入った」のではなく、以前から潜在していたものでした。特定の条件の組み合わせでしか発症しないバグは、コードレビューでも見逃されがちです。

3. AIは「恥ずかしい」を知らない

人間は「こんな単純なバグに時間をかけてしまった」と恥ずかしくなります。AIはそんなことを一切気にせず、淡々と修正します。この感情の不在が、障害対応では大きなアドバンテージになることを痛感しました。

まとめ

人間がバグ修正でAIに負けました。 完敗です。

でも、負けたことは恥ずかしくありません(少し恥ずかしいですが)。

大事なのは、「AIの方が速いケースがある」ことを認め、それを武器として使うことです。本番障害の対応中に「まずAIに投げてみる」という選択肢が増えたことは、チームにとって大きな前進でした。

次に本番障害が起きたとき、人間チームはきっとこう言うでしょう。

「とりあえずAIにも投げておいて」

それは敗北宣言ではなく、チームの新しい戦術です。


本記事は実際に発生したバグ修正の事例をもとに、具体的な実装の詳細を伏せて執筆しました。







おまけ

本文比でAI版4200文字、人間版1800文字くらいでした。 1/3になっていない……詳細はAI版参照というチートを使ったのにも関わらず、無念。

尚、執筆時間は指示1分未満に対して軽く100倍以上の時間がかかっている模様。

内容がどうかは読者の判断に委ねます。

面白お気持ち記事*2になっていて、記事でも負けている気がしています。 現実は厳しいですね。

*1:サンプル数は増やしたくないですが……障害は発生しないに越したことはないし、発生させないのが仕事なので。

*2:事実や生成AIの特性、当面の使い方の考え方は書かれてしまったのでお気持ちに寄せている側面はあります。