
「ハルシネーション対策」を実装する前に、設計変更で消えていた──Phase 0 検証で起きた3つのピボット
前回の記事で、影響範囲調査レシピジェネレーターの「調査レシピ方式」に発想を転換した話を書いた。記事の最後で「次は Phase 0 の遡及テスト設計について書く」と予告していた。
今回はその続きなのだが、実際にやってみたら「遡及テスト設計」の話をするつもりが、テストの途中で設計そのものを3回作り直していたという記録になった。
そして最後には、Phase 1 で対応予定だった「ハルシネーション対策」の Issue を、一行もコードを書かずに close することになった。この記事はその顛末です。
前提:このツールが何をするか
簡単に振り返っておくと、影響範囲調査レシピジェネレーターは、PR の diff から「この変更で見落としやすい盲点」を検出し、調査の手がかりを生成する CLI です。
プロトタイプの動作フローはこうなっている。
Phase 0 の目的は、この流れが実運用に耐えるかを遡及テストで確かめることでした。想定していた評価軸は大きく3つ。
- P0-1 捕捉率: 過去のインシデント PR に対して、該当パターンを検出できるか
- P0-3 ノイズ率: 無関係な PR で「該当なし」を正しく返せるか
- P0-4 事実性: 生成されたレシピが実在するコードやファイルを参照しているか
最初は単純に、過去に影響範囲の見落としが起きた PR をいくつか流して、パターン検出と生成レシピの質を定量的に測ればいい、と考えていました。
ピボット1:プロダクト固有の前提が混じっていた
最初のテストは拍子抜けするほど順調だった。新旧テーブル同期パターンの PR を流したら、該当パターンをピンポイントで検出し、関連する過去インシデントも正しく紐付けてきた。
プロンプト側のバグ(JSON 例のブレースが str.format のプレースホルダと誤認される)や、モデル名の取り違え(使っているデプロイ名が微妙にズレていた)などはあったものの、大筋では動いている。「悪くない」と思って次の PR に進もうとした、その時でした。
生成されたレシピを読み直していて、ふと手が止まった。
うーん、提示された影響範囲調査のやり方、正しいのか判断できないな……
具体的に言うと、こんな感じだった。
- 存在するか怪しいディレクトリへの言及
checksumなどのユーティリティ実行コマンド → 実在するか確認できない- SQL 例のテーブル名が、実テーブルではなく「それっぽい雛形」
- バッチが特定のフレームワークで書かれている前提の grep
要するに、それっぽく具体的なのだけれど、LLM が「知っている形式」で手順を埋めているだけで、実在するかの保証がない。いわゆるハルシネーションでした。
ここでいったん、「検証モード(生成したコマンドを実行して空振りを注記)」と「L1 grounding(リポジトリのファイルツリーをプロンプトに含める)」という2つの対応策を Issue に起票し、Phase 1 で対応する、としました。この Issue が後で効いてくる。
ただ、この時点でもう一つ、もっと根本的な違和感があった。
プレイブックを覗くと、特定のカラム名、特定のフラグ名、特定の内部テーブルの命名規則など、自社プロダクト固有のナレッジが滲み込んでいる。これだと、レシピを見てもそれが「汎用的に正しい手順」なのか「自社の文脈を当て推量で埋めただけ」なのか判別できない。Phase 0 の事実性指標を測ろうにも、判断軸そのものがブレる。
「プロトタイプなのに固有ナレッジが混じっているのは辛い。ゼロベースで作り直したい」と Claude Code に相談した。
返ってきたのは、8個の汎用パターン(スキーマ変更、フラグ固定化、二重データパス……)でした。一見もっともらしいんだけど、何か引っかかった。
いいんですけど、既存のファイルの情報に引っ張られてない? ゼロベースで考えた?
この問いに対する Claude Code の返しは、正直、少し刺さった。
正直に言うと、引っ張られていました。
既存カテゴリの言い換えが半分で、新規は半分だけ。「ゼロベース」と言いながら半分は既存の再ラベリングです。
そこから出てきた新案が、「技術カテゴリ」ではなく「盲点の軸(認知軸)」で分類し直すというものでした。
| ID | 盲点 | 問い |
|---|---|---|
| B1 | 呼び出し元の網羅 | この変更は誰から呼ばれている? 全呼出元は新しい挙動に耐えるか? |
| B2 | 永続状態への影響 | この変更は何を DB/キャッシュ/ファイル/キューに残すか? |
| B3 | 環境間の差分 | 本番と dev で状態/設定が異なるとき、同じ挙動をするか? |
| B4 | 時系列上の一貫性 | デプロイ中・ロールバック時・移行期間中でも破綻しないか? |
| B5 | 並行性・冪等性 | 同時に複数走ったら? 途中で失敗したら? 再実行したら? |
| B6 | 外部契約 | 契約を保っている相手を全員把握できているか? |
| B7 | 失敗時の可観測性 | 失敗が "静かに" 起きるようにならないか? |
DB / API / env という「技術の分類」ではなく、「レビュアーが見落としやすい認知の軸」をパターンにする。こう並べると、このツールが本来埋めたかった「認識の問題」に、ようやく語彙が追いついたという感覚がありました。前回の記事で書いた「調べようと思えば調べられたが、調べるべきだという認識がなかった」を直接問い直す形になっている。
ついでに、インシデント事例も固有案件の ID から、「E-B1-01: デフォルト引数の反転による silent な挙動変更」のような匿名化された汎用的な失敗の原型(アーキタイプ)に差し替えました。実インシデントではないので「過去に発生した」という表現は禁じ、「盲点の典型例」として位置づける、という運用も一緒に整理した。
ここで最初のピボットが入った。「パターン体系をゼロベースで作り直す」。
ピボット2:詳細手順は重すぎる。チェックリストに変える
B1-B7 の再構築が終わり、もう一度同じ PR でレシピを生成してみた。パターン検出の精度は上がり、PR 固有の識別子を使った具体的な手順が返ってくるようになった。技術的には改善された。
改善されたのだけれど、今度は別の違和感が湧いてきました。
生成されたレシピは12ステップ。各ステップが2〜3行の解説付きで、合計すると読むのに5〜10分かかる。真面目にやれば調査としては網羅的でしょう。ただ、こんなに丁寧に書かれたものを、全員が PR ごとに実行してくれるだろうか?
形骸化するときの典型は、「やることが正しすぎて重い」です。正論すぎるチェックリストはだいたい読み飛ばされる。これは前回の記事で「静的なチェックリストが形骸化する」と書いたのと同じ轍を、動的生成でも踏もうとしている気配がしました。せっかく動的にしたのに、内容が重すぎて同じ結果になってしまう。
そこで、レシピのフォーマットそのものを変えることにした。
| 項目 | 旧(詳細手順) | 新(チェックリスト) |
|---|---|---|
| 長さ | 10〜12ステップ × 2〜3行 | 4〜6項目 × 1行 |
| 内容 | 「X を実行し、Y を確認し、Z を比較せよ」 | 「〜した / していない」の二値確認 |
| コマンド | 各ステップに必須 | 1項目に1個だけ、任意で末尾に添える |
| 期待読了 | 5〜10分 | 1〜2分 |
原則は3つに絞った。
- 4〜6項目に収める。10項目を超えるとレビュアーは消化しない
- 二値で答えられる形にする(「〜した」「〜していない」)
- PR 固有の識別子(ファイル名、関数名、カラム名)を含める
この変更を入れて再実行したら、同じ PR に対するレシピが一気に軽くなった。「これなら読む」という手触りが、ようやく出てきた感じでした。
ここで2つ目のピボット。「詳細手順 → チェックリスト」。
ピボット3:パターン別に並べると同じ話を3回読まされる
これで行けるかと思ったのだけれど、実案件で CI 経由で走らせてみたら、次の問題が出てきました。
ある PR に対して、B6(外部契約)、B4(時系列一貫性)、B3(環境間差分)の3つの盲点が同時に検出された。それぞれのパターンごとにレシピが生成され、それが縦に3つ並んで PR コメントに投稿された。
結果として、全チェック項目のうちおよそ半分が重複していた。
同じ変更内容の説明が3回、同じチェック項目が複数回、微妙に言い回しを変えて登場する。読んでいる開発者からすると、「さっき読んだぞ」の連続です。
ここでの判断で少し迷ったのが、どこで統合すべきかでした。
最初に浮かんだのは「チェックリストだけ重複排除する」案。でも、よく見たら変更概要の説明と盲点の言及も重複している。部分的な修正では追いつかない。そこで、レポート全体を再構成する方針に切り替えた。
最終形はこうなっている。
# 影響範囲調査レシピ
## この PR の変更概要
(diff の要点を1回だけ記述)
## 検出された盲点
- B6 外部契約: 〜
- B4 時系列: 〜
- B3 環境差: 〜
## チェックリスト
(統合・重複排除された項目)実装としては、パターン別のレシピ生成は残したまま、最後に LLM への追加呼び出しを1回挟んで、全レシピを統合・重複排除させる構成にした。コストは +1 API call、およそ3,000トークン増。個別レシピは成果物として残すので、詳しく追いたいときは辿れる。
ここで3つ目のピボット。「パターン別表示 → 集約レポート」。
ハルシネーション対策の Issue が、実装せずに close になった
ここまでの3ピボットを終えて、もう一度 Issue 一覧を見返した。
Phase 1 で対応予定だった「ハルシネーション対策」Issue は、ピボット1の時点で起票したものでした。「検証モードの追加」と「L1 grounding への昇格」でハルシネーションを抑え込む、という内容。
ところが当時の問題を整理し直してみると、こうなっていた。
| 当時の問題 | 現在の状況 |
|---|---|
| 架空ディレクトリへの言及 | チェックリスト形式では1行で完結し、架空パスを展開する余地がほぼない |
| 架空ユーティリティの呼び出し | 汎用プレイブックから固有識別子を排除したため、LLM は diff 由来の識別子だけを使うようになった |
| 雛形テーブル名の混入 | プレイブック側からプロダクト固有命名を削除済み |
| 命名規則の想定不一致 | プロダクト固有ナレッジを全排除 |
当時ハルシネーションに見えていたものの大部分は、設計変更の副作用で消えていた。
当時、私は「検証モード(生成コマンドを実際に実行して空振りを可視化)」と「L1 grounding(ファイルツリーをプロンプトに入れて実在ファイルのみ参照させる)」という実装策で解こうとしていた。どちらも実装コストは小さくなく、特に検証モードはサンドボックス実行環境が絡むので結構な工数になる。
それが、以下の3つの変更だけで、大部分が解消されていた。
- プレイブックから固有識別子を削除(ピボット1の副産物)
- 出力をチェックリストに制約(ピボット2)
- 「diff に現れる識別子だけを使え」「プレイブックを複写するな」というプロンプト制約
結果として、該当 Issue は close しました。運用で再びハルシネーションが問題になったら再検討する、という形で棚上げ。
実装する前に、実装が要らなくなっていた。これは今回、一番嬉しかった瞬間でした。
Phase 0 の手触りと、次に向かう場所
駆け足で振り返ると、この Phase 0 検証で起きたことはこうなる。
- ピボット1: プロダクト固有 → 汎用認知軸(B1-B7)
- ピボット2: 詳細手順 → チェックリスト
- ピボット3: パターン別 → 集約レポート
- 副産物: 「ハルシネーション対策」の Issue が実装前に陳腐化
最初に想定していた「遡及テストで捕捉率を測って Phase 0 合格判定する」という綺麗な流れとは、だいぶ違う着地になりました。正直、途中で何度か「これ、進んでいるのか戻っているのかわからない」と感じる瞬間があった。
ただ、振り返ると、今回手を動かしていて効いたのは「テストしながら設計を問い直し続ける」という姿勢だったように思います。特にピボット1で「ゼロベースで考えた?」と一度立ち止まったことが、その後の2つのピボットをまとめて引き出した感触がある。あれを聞かずに進めていたら、プロダクト固有ナレッジの上に詳細手順形式を積み上げて、ハルシネーション対策の実装まで突っ走っていたはずです。作業量は何倍にもなっていたでしょう。
Phase 0 は、当初想定していた「数字を取る」という目的は半分しか果たしていません。捕捉率やノイズ率の定量測定はまだ十分にやれていない。ただ、設計レベルの粗が一通り洗い出され、レシピの形も落ち着いた。「数字を取れる設計にたどり着いた」、というのが今の手触りです。
次は、今のチェックリスト形式が実運用でどう消化されるか。開発者が実際にチェックを付けてくれるか、それとも読み飛ばされるか。この「人間側の受容度」こそが、本来の Phase 0 で測りたかったものでしょう。
一度設計を作り直して整理しきる、という遠回りがあってから、ようやく本題のテストが始められる。プロトタイプの手触りとは、だいたいこういうものなのかもしれない、と思っています。
参考:Claude Code と協働する
今回のピボットは、Claude Code に「ゼロベースで考えた?」と一度問い直したところから動き始めました。AI と協働しながら設計を作っていく進め方については、以下の書籍が参考になります。
この記事は役に立ちましたか?

この記事が、何かの整理につながったら
コーヒー1杯分の応援をもらえると嬉しいです。
あなたへのおすすめ
