#デバッグ#開発手法#AIエージェント#根本解決

応急処置を禁じて根本解決に徹する開発 — バグ調査の実例集


バグに遭遇したとき、いちばん手っ取り早いのは「症状を消す」ことです。エラーが出るなら try-catch で握りつぶす。特定の値でだけ壊れるなら、その値をハードコードで避ける。精度が出ないなら、それっぽいキーワードでブーストして誤魔化す。どれも一時的には動いて見えます。

しかし、こうした応急処置は必ず後で牙をむきます。真因が生きたまま残るので、別の入り口から同じ問題が再発する。握りつぶしたエラーは、もっと分かりにくい形で下流に漏れ出す。ハードコードした条件は、次の変更で誰かが踏む地雷になる。

AIコーディングエージェント(Claude Code など)を相棒に開発を進めると、この誘惑はむしろ強まります。エージェントは「とりあえず動く修正」を高速に提案できるからです。だからこそ、「応急処置を禁じ、常に根本解決に徹する」という原則をエージェントに明示的に課すことが効いてきます。この記事では、声質変換アプリの開発中に実際に遭遇した3つのバグを題材に、症状を隠さず真因にたどり着く調査の型を紹介します。

大原則:症状ではなく、真因を消す

まず、この記事を貫く判断基準を先に置いておきます。修正案が出てきたら、次を自問します。

  • それは症状を消しているのか、真因を消しているのか
  • 同じ真因から派生する別の症状も、その修正でまとめて消えるのか
  • その修正は、なぜそれで直るのかを一文で説明できるか

三番目が特に大事です。「なぜ直るのか」を説明できない修正は、たいてい症状を隠しているだけです。「この値を避ければ落ちなくなる」は説明ではありません。「この値のとき○○という前提が崩れるから落ちる。だから前提そのものを満たすようにした」なら説明です。

以下、3つの実例で見ていきます。

実例1:変換後の音声が「ゆっくり喋る」— 比例関係が真因を指す

最初の症状は、声質変換した音声だけがなぜか間延びして再生される、というものでした。入力の録音は普通の速さなのに、出力だけが酔っ払いのようにスローになる。

応急処置なら、いくらでも思いつきます。出力にタイムストレッチをかけて無理やり縮める、とか。しかしそれは「なぜ遅いのか」を一切説明していません。

ここで効いたのが比例関係の観察でした。短い音声では起きず、長い録音でだけ発生する。しかも入力が長いほど、比例して遅くなる。131秒の録音で体感4倍以上スロー——この「長さに比例して悪化する」という手がかりが、真因の在り処を教えてくれました。

サンプリングレートの取り違えなら常に一定の倍率で遅くなるはずで、長さには比例しません。タイムストレッチのバグも同様です。「長さに比例する」という比例関係が成り立つのは、固定長で切り取られたものを全長に引き伸ばしているときだけです。

真因は、意味抽出に使っている Whisper エンコーダの「一度に30秒まで」という制限でした。131秒の録音を渡すと先頭30秒ぶんの内容しか得られず、それを131秒の長さへ引き伸ばすため、131 ÷ 30 ≒ 4.4倍スローになっていた。体感の「4倍以上」とぴたり一致します。

解決は、30秒の重複チャンクに分割してそれぞれ Whisper に通し、連結すること。応急処置的なタイムストレッチではなく、特徴抽出の段階で正しい情報を得るという真因側の修正です。この調査の詳細は別記事「音声変換が『ゆっくり喋る』バグの犯人は、Whisperの30秒制限だった」にまとめました。

ここでの学びは一つです。比例関係は真因への矢印になる。症状の大きさが何に比例して変わるのかを測れば、容疑者を機械的に絞り込めます。

実例2:長時間のML推論で502 — タイムアウトを「延ばす」のではなく「仕組みを変える」

次は、44.1kHz の広帯域モデルで長時間の音声を生成させると、フロントに502が返るというバグでした。短い音声なら通るのに、生成に時間がかかると必ず502になる。

いちばん安易な応急処置は、タイムアウト値をとにかく大きな数字に設定することです。しかしこれは「なぜ切れるのか」を理解しないまま数字をいじる、典型的な対症療法です。数字を大きくしても、それを超える入力が来ればまた落ちる。地雷を先送りしただけです。

真因を追うと、Next.js のサーバから推論バックエンドへリクエストを中継する際、内部で使われている fetch 実装(undici)に既定のタイムアウトがあり、長時間かかる応答を待ちきれずに接続を切っていたことが分かりました。502は、上流が生きているのにプロキシ層が先に諦めた結果でした。

ここで判断が分かれます。「undici のタイムアウトを無効化する」だけでも一応は直りますが、より確実だったのはプロキシの中継を Node 標準の http/https に置き換え、応答待ちを無制限にすることでした。長時間推論という処理の性質上、「一定時間で切る」という前提そのものがこのエンドポイントには合っていない。だから前提を持ち込まない実装に替える——真因側の対処です。

学びはこうです。タイムアウトやリトライ回数のような「数字」をいじりたくなったら、それが対症療法ではないか一度立ち止まる。多くの場合、数字を調整すべきではなく、「なぜその制限が効いてしまう構造なのか」を問い直すべきです。

実例3:巨大モデルのダウンロードが停滞 — 「無視して進む」ではなく「確実に配置する」

三つめは、HuggingFace から数GB級のモデル重みを取得する処理が、途中で停滞して先に進まなくなる問題でした。ネットワークが不安定な環境だと、ダウンロードが無言で止まったまま固まる。

ここでの応急処置の誘惑は、「ダウンロードに失敗したら握りつぶして、とりあえず起動を続ける」というものです。しかしモデル重みが揃っていない状態で推論を始めれば、もっと分かりにくいエラーが後段で噴き出すだけ。エラーを握りつぶすのは、問題を見えなくするだけで解決していません。

真因は、標準的なダウンローダが停滞(ストール)を検知できず、一度詰まると自力で回復しないことでした。落ちるわけでも失敗を返すわけでもなく、ただ黙って止まる。だから握りつぶしようもない——そもそも例外が飛んでこないのです。

解決策は、停滞検知と再開に対応したダウンローダ(curl の該当オプション)で取得し、成果物を HuggingFace のキャッシュディレクトリに確実に配置することでした。一定時間データが流れなければ失敗と見なして中断し、途中から再開する。ネットワークが不安定でも、最終的に完全なファイルがキャッシュに載ることを保証します。

学びはこうです。「無言で止まる」は「エラーで落ちる」より厄介。例外が飛ばない停滞は握りつぶしようがなく、握りつぶす発想自体が誤りです。信頼できる取得を仕組みとして用意し、成果物が正しい場所に確実に置かれることをもって「完了」とする。これが根本解決です。

3つの実例から見える共通の型

異なる3つのバグですが、真因にたどり着くまでの型は共通しています。

  • 症状の「効き方」を測る — 実例1の「長さに比例」のように、症状が何に応じて強まるかを観察する。比例関係・境界・再現条件は、そのまま真因への矢印になる。
  • 「それっぽい容疑者」を先に潰す — サンプリングレートやタイムストレッチのように、いかにも怪しい候補を観察事実で消していくと、残ったものが真因を指す。
  • 「なぜ直るのか」を一文で言えるかを関門にする — 説明できない修正は症状隠しを疑う。実例2の「タイムアウトを延ばす」は説明になっていないので不合格。
  • 数字いじりと握りつぶしを警戒する — タイムアウト値の増加(実例2)やエラーの握りつぶし(実例3)は、対症療法の典型シグナル。手が伸びたら一度止まる。

AIエージェントと組むときの実務

この原則は、AIコーディングエージェントと組むときにこそ制度化する価値があります。エージェントは応急処置を高速に量産できるので、放っておくと「動くけれど症状を隠しただけ」のコードが積み上がりがちです。

有効なのは、エージェントへの恒久的な指示として「応急処置を禁じ、根本原因の特定 → 適切な技術選定 → 設計レベルの解決策の提示 → 実装、の順で進める」と明文化しておくことです。そのうえで、提案が出てきたら人間側が「これは症状か真因か」「なぜ直るのか一文で言えるか」という関門を通す。エージェントの生産性と、根本解決の規律は両立できます。

まとめ

  • 応急処置(try-catchで握りつぶす/ハードコードで逃げる/ヒューリスティックで誤魔化す)は真因を温存し、必ず再発する
  • 修正案は「症状を消すのか、真因を消すのか」「なぜ直るのか一文で言えるか」を関門にして判定する
  • 実例1:症状の比例関係が真因(Whisperの30秒制限)を指した。比例関係は真因への矢印
  • 実例2:長時間推論の502は、タイムアウトを延ばすのではなく、時間で切らない中継の仕組みに替えて解決
  • 実例3:巨大DLの停滞は、握りつぶすのではなく、停滞検知+再開で成果物を確実に配置して解決
  • AIエージェントは応急処置を量産しやすい。「根本解決に徹する」を恒久指示として明文化し、人間が関門を通す
← 記事一覧へ