
LLMを組み込んだツールを設計する7つの原則──CAREプロジェクト11日間の意思決定ログ
このシリーズは何をやっていたか
出発点は、直近1四半期に起きた9件のインシデントでした。全部を並べて原因を分類してみたら、おかしなことに気づきます。静的解析で防げるインシデントは0件。 コードの書き間違いで起きたものがほぼありません。代わりに共通していたのは、「このコードがあそこに波及していることに、誰も気づけなかった」という形の見落としでした。
ORM のクラス名(ProjectUsage)とテーブル名(project_usage)が一致しないから、テーブル名で grep しても出ない。複数のリポジトリにまたがっているから、ひとつの検索範囲では追いきれない。DB やバッチ処理から参照されているから、アプリコード上では何も動いていないように見える──そういう類の、認知の死角。
この問題に「既製の解」はありませんでした。複数プロダクト × 複数言語 × コード外のデータソースという交差点にある課題で、どれか一つを入れれば解決する SaaS は存在しない。
だから作り始めました。CARE(Context-Aware Review Engine) という名前を付けて。ゴールは明快です。「PR の変更内容を読んで、レビュアーが見落としがちな認知の盲点を検出し、確認用のチェックリストを PR コメントとして投稿する」ツール。裏側では LLM(Azure OpenAI の gpt-5 系)を detection / generation / consolidation の3段で使って、「どこに注意を向けるべきか」のチェックリストを絞り込む構造です。
ただ、最初の設計は途中で保留になりました。9件のサンプルで筋が良さそうに見えた自律エージェント方式が、94件の全数データで ROI が borderline になった。保留した翌日、「調べた 結果」ではなく「調べる 手順(レシピ)」を返す方式、いわゆる 調査レシピ方式 にピボット。そこから Phase 0(動く)→ Phase 1(沈黙の質)→ Phase 2(トークン削減と出荷判断)を経て、「これはもう初期バージョンとして出していい」と区切るまで。この全体がシリーズ8本・11日間です。
個別の記事は dev-labo(開発ラボ)カテゴリに、試行錯誤のログとして残してあります。ただ、走り切ってみたら、8本から抽出できる原則がいくつも浮かんでいました。この記事は、それを 7つに絞って体系化した ものです。これから LLM を組み込んだツールを作る人、いま詰まっている人に、1箇所にまとめた形で届けたい内容でした。全8記事は末尾にリストしています。各原則の根拠となるエピソードをもう少し詳しく知りたい場合はそちらへ。
原則1:認知の問題と実行の問題を分ける
9件のインシデントを全部貫いていた事実はひとつでした。
「調べようと思えば調べられた。だが、調べるべきだという認識がなかった」
grep すれば見つかる依存関係。ALTER で動くカラム変更が、全然別のリポジトリのバッチ処理に波及する。技術的にはすべて、調べれば出てくる問題でした。
この区別が見えた瞬間、打ち手の設計がまったく変わります。「調査を自動化する」は 実行 の問題を解こうとするアプローチで、「調査すべきかどうかを教えてくれる仕組み」は 認識 の問題を解く。後者のほうがはるかに軽く、スケールしやすく、そして「コードだけでは分からない DB や外部連携の影響」のような認識の死角に強い。
LLM ツールを設計するとき、最初に問うべきはこうです。これは認識の問題か、実行の問題か。 両方を同時に解こうとすると、重くなりすぎて誰も使わない。実際 CARE の自律エージェント設計は、この両方を解こうとして保留になりました。一方、ピボット後の「調査レシピ方式」は認識の問題だけを解くと割り切った設計で、同じ課題に対してずっと軽く・速く動きます。
LLM は「実行」も「認識」もどちらの問題にも使えてしまうから、切り分けを意識しないとすぐ欲張った設計になる。そのとき手を止めて「本当に解いているのはどっちか」を言語化することが、最初の一手として効きました。
原則2:理論の天井と実データの床の間に線を引く
CARE の設計を詰めているとき、「そもそも影響範囲って静的解析で全部追えるのか」という問いが頭をもたげました。調べて出てきた答えは明快でした。完全自動化は原理的に不可能。
プログラムの振る舞いを静的に正確に判定する問題は、Rice の定理によって決定不能問題に帰着する。Forward Slice で網羅的に影響を追うと、動的言語では「見落としゼロ(Sound)」を目指した瞬間にプログラムのほぼ全体が影響範囲として報告されてしまう。Google でさえ完璧な手続き間解析インフラを持たず、業界標準は "Soundy"(ほぼ健全)な近似解──というのが研究的な結論でした。
この手の「完璧は無理」という理論的な天井を知ると、思考が2択に流れがちです。「諦める」か「無理を承知でやる」か。でも、どちらも違いました。
正しい問いは 「天井と床の間のどこに線を引くか」 でした。天井は Rice の定理。床は「実際に起きた9件のインシデント」。その間のどこかに「必要十分」が必ずある。線を引く材料は、理論のほうではなく、実データのほう にあります。
実際、9件を縦に並べると、「何を拾えるか/何が原理的に拾えないか」が具体的に見えてきました。PR 起因の変更から辿れる依存は拾える。ルール定義済みの観点も拾える。一方で PR 外の作業(データ移行、手動オペ)や実行時の入力依存は、構造的に対象外。この境界を明示することで、「完璧ではないが、今の課題に対しては必要十分」 という線が引けた。
LLM ツールを作るとき、論文や理論で「限界」に突き当たっても、設計を止める必要はありません。手元にある実データこそ、線引きの起点です。 理論は「これ以上は伸ばせない」を教えてくれる天井でしかなく、今の自分に必要なラインは、もっと手前にある実データの形から決まる。
原則3:LLM に判定させない、証拠を集めさせる
CARE エージェントを設計したとき、一番大事にしたのがこの境界でした。LLM の役割は 「証拠収集」 であって 「判定」 ではない。
具体的には、エージェントの入出力を次のように固定しました。
- 入口: PR の diff と、「何を調べるか」を書いたルール/プレイブック
- エージェントの動き: ツール(コード検索、DB スキーマ参照、AWS 設定参照)を呼び出して、事実を集める
- 出口: 集めた証拠のリストと、未確認の範囲の明示
- 判定: 出力を読んだ人間が行う
なぜこの境界を死守したかというと、LLM に判定まで任せると、ハルシネーションが「判定」に化ける からです。「この変更は安全です」と LLM が言い切ったあとで見落としが発生した場合、責任の所在が急に曖昧になる。「AI が安全と言ったから」は、レビューのログとして成立しません。
代わりに、LLM は「ここに証拠がある/ここは未確認」までに留める。この粒度なら、ハルシネーションは「存在しない証拠を挙げる」という形で顕在化し、Phase 1 で導入した grounding フィルタ(_is_grounded())で drop できます。matched_files が diff に実在するか、reasoning に diff 由来の具体識別子が含まれているか──このチェックを通らないものは「証拠とは見なさない」。プロンプトで『証拠を集めてくれ』と言い、コードで『証拠になっていないものは通さない』で縛る。これで「LLM の言葉だけでは判定しない」が徹底されます。
LLM の能力を過小評価しているわけではなく、これは LLM が扱う情報の性質 からくる設計判断です。不確実な情報の取り扱いを曖昧にしない。古典的な「事実と解釈を分ける」「データと判断を分ける」の、LLM 版だと思っています。
原則4:サンプルが変われば設計も変わる
9件のインシデントで CARE の設計を固めていたとき、全数94件のデータが手に入りました。同じフレームワークで分類し直したら、設計の前提がいくつも同時にひっくり返りました。
具体的には:
- 9件で「B(暗黙的な依存の見落とし)中心」と認識していたのが、94件では「D(DB の桁不足、環境変数差、Intel/ARM 差)」の比重が無視できない大きさに
- 「繰り返しパターン」は約10件のみ。残り19件は 毎回違うパターン だった
- 「仕様/実装漏れ」とラベルされた54件の中身は、3つに分解できた(影響範囲起因 ≒15件・単独バグ ≒25件・エッジケース ≒10件)
これを見て、設計思想ごと組み直しました。YAML 構造化ルール(trigger/question/scope/depth のキー付き形式)を捨てて、Markdown ナラティブ(ポストモーテム形式の自由文)に転換。理由は、毎回違うパターンを YAML で事前定義するのは無理だから。自由文で「気づきどころ」を書き留めるほうが、既存の障害対応フローと自然に重なる。事前スコープ制御もやめて、後段ガードレール(最大ツール呼び出し回数、最大トークン消費、タイムアウト)に移行しました。
最終的には、この再設計を踏まえて ROI 計算をやり直した結果、自律エージェント自体を保留 にする結論になり、翌日「調査レシピ方式」にピボットしています。つまり、94件サンプルへの拡大が、設計だけでなく プロダクト方針そのものの変更 まで引き起こした。
この経験からの原則はシンプルです。小さいサンプルで設計を完成させてはいけない。 データが増えたら、設計を作り直す覚悟で取り組む。逆に言えば、最初のサンプルで綺麗に設計しすぎないこと。あとで壊す前提のプロトタイプのほうが、データ駆動の更新が効きやすい。
「94件全部揃うまで設計するな」という話ではなく、「サンプルは増える」という前提で、設計を保留しておく領域を意図的に残す ということです。特に LLM ツールは、作るよりもデータで育てる比重が大きい。この性質に合わせた設計の「開きしろ」を用意しておくことが重要でした。
原則5:プロンプトは契約にならない、コードが契約になる
これは Phase 1 で一番効いた原則でした。
プロンプトに「チェックリストは6項目以内」と書いても、モデルはその日の機嫌で11項目返してきます。「JSON の reasoning 内に " を含めるな」と書いても、コード例を説明しようとしてバックティック内に " を埋め込んで JSON パースを破壊します。「PR 固有性のない汎用項目は禁止」と書いても、「テストで全件 pass することを確認した」のような誰でも書ける項目が混ざります。
プロンプトは お願い です。モデルが協力的な日は通るし、そうでない日は通らない。
対策は、コードが契約になる ことです。プロンプトに書いた「守ってほしい形」を、コード側で「この形でしか通さない」に変換する。
def cap_checklist_items(content: str, max_items: int) -> str:
"""チェックリスト見出し以降の - [ ] 項目を max_items 件で切り詰める。"""
def _is_grounded(match, diff_files, diff_text) -> tuple[bool, str]:
"""matched_files が diff に実在し、reasoning に具体識別子が含まれるかを検証。"""プロンプト側では「なるべくこうしてください」と書き、コード側では「これを満たさないものは drop します」と書く。両方を同じ仕様でまとめる。
重要なのは、プロンプトだけに頼らないこと。LLM 組み込みツールの品質は、このコード側のガードがどれだけ厚いかに比例します。プロンプトだけで済ませると、品質がモデルのその日の機嫌に直接支配されるし、モデルを乗り換えたときに挙動が揃いません。
逆に、コードだけで全部縛ると、モデルが生成の前段で「何を求められているか」を理解できず、効率が悪くなる(モデルが出したがる形とコードが許容する形がズレるほど、drop 率が上がる)。お願い(プロンプト) と 契約(コード) の二段構えが、実務的にちょうどいい設計でした。
原則6:ノイズと沈黙は非対称である
PR コメント系ツールを作ってみて、直感に反して最も効いた原則がこれでした。
Phase 1 の最初のコメント投稿を読んで、手が止まった瞬間がありました。7項目のチェックリストが返ってきて、その中に「法務・セキュリティの承認を取得した」「テストでカバーされているケースに過信していないことを確認した」といった、どの PR でも言えてしまう汎用警告 が混ざっていました。
読みながら、まずいな、と思いました。ツールが1回こういうノイズを出すたびに、レビュアーの頭の中では「このツールのコメントは斜め読みでいい」という 学習が不可逆に蓄積 されていきます。その後に本当に重要な指摘を出しても、もう読まれない。
一方で、ツールが 沈黙している時間 は、マイナスではなく価値です。黙っているツールは、「注意すべきことがない」というシグナルを静かに送っていて、しかもノイズで信頼を削らない。
この気づきを受けて、Phase 1 では一連の施策を入れました。
- grounding の強制:reasoning に diff 由来の具体識別子がない検出は drop
- チェックリスト上限と禁止カテゴリ:「法務承認」「過信しない」のような汎用項目は明示禁止
- 「検出0件」専用プロンプト:検出なしのときに「何か書け」で尾ひれが付くのを防ぐため、別プロンプトに分岐
- 「検出失敗」と「検出0件」の型分離:API エラーと沈黙を混ぜない(空配列には2種類ある)
すべては「沈黙の質を上げる」という一点を向いていました。
一般原則として言い換えるなら、情報を出す/出さないの判断コストを、出す側に偏らせる こと。これは Slack 通知でも、アラートでも、PR コメントでも変わらない構造です。沈黙を資産と見なす設計は、AI 時代に限らずツール全般に効きますが、ハルシネーション発信源を抱えた LLM ツールでは特に効きます。 喋らせたがる誘惑が強いぶん、沈黙を守る設計の価値が大きい。
原則7:完成と区切りは別の判断
Phase 2 で、「これはもうプロトタイプというより初期バージョンとして出していい」と判断しました。
その時点で、ツールは完成していません。メタ PR ハルシネーション という未解決の問題を抱えていました。ツール自身のプロンプトを編集する PR に対してツールを走らせると、プロンプト内の「悪い例」「良い例」を実変更と誤認して、勝手なチェックリストを生成する現象です。これは「プロンプトを書くこと」と「プロンプトで扱われる PR の対象になること」が同じリポジトリで起きている構造的問題で、潰そうとすると影響が大きい。
でも、区切る判断 は完成とは別の基準で下せる、と気づきました。出荷判断に使った5基準を言語化してみます。
- コア機能が安定している(Phase 0/1 で grounding と動詞縛りが効いた)
- 運用コストが耐えうるレベルに下がった(Phase 2 でトークン消費が元の6〜7割に)
- 他の人に見せられる体裁になった(2段構成フォーマット)
- 残っている問題は特殊ケース(通常 PR では再現しない)
- 次の改善の良い材料は「他の人が使った実 PR のフィードバック」になっている
(5) が本質でした。自分一人で詰めるフェーズから、他の人に使ってもらうフェーズに移す判断。同じ「詰める」でも、誰のループにするか という設計変更です。自分の PR だけで詰め続けると、机上の最適化に寄っていく。他の開発者の PR では再現しない違和感まで拾ってしまう可能性があるし、その修正のために設計が歪むかもしれない。
LLM ツールは、作ってから使いながら育てる部分が大きい。だから 「完成してから出す」を待っていると永遠に出せません。 区切り方を決める基準を、完成とは別に持っておくこと。これが出荷判断を詰まらせない鍵でした。
実装品質の話ではなく、誰のフィードバックを入口にするかの設計変更 として「区切る」を捉え直すこと。完成は未来の話、区切りは今の話です。これが最後の原則です。
おわりに:これはツールを作る側の内省でもあった
7つの原則を並べてみて思うのは、半分以上が 「LLM をどう扱うか」ではなく「自分がどう判断するか」の話 だということです。認識と実行を分ける、天井と床の間で線を引く、サンプルに応じて設計を書き直す、完成と区切りを切り分ける。どれも、LLM が相手ではなくても成立する原則の、LLM 版でした。
LLM を組み込んだツールを作るとき、最大の特徴は 書きながら自分の頭の中の概念が固まっていく ことでした。「認識と実行」「証拠と判定」「ノイズと沈黙」「完成と区切り」──これらの区別は、プロンプトを書きながら、コードを書きながら、少しずつ言語化されていきました。ものづくりと内省が、思っていた以上にくっついている11日間でした。
自分のループはここで一旦区切り、次は他の人のループに移ります。この7原則が、これから同じ道を歩く人の手前の景色を少し明るくできれば、それが一番嬉しい区切り方です。
参考文献
本記事で扱った原則を、技術面と設計論の両側から深めたい方向けに、執筆中に何度か戻った2冊を挙げておきます。
LLM・基盤モデル側からの体系
grounding、プロンプトとコードの二段構え、トークン経済──本記事で扱った論点が、基盤モデルを使ったアプリケーション開発全般でどう立ち現れるかを体系的に整理してくれる1冊。
ソフトウェア設計判断の側からの体系
本記事の結論は「7原則の半分以上は LLM 固有ではなく、設計判断の一般原則の LLM 版」でした。アーキテクチャ設計の判断プロセスをトレースするのに、視野を広げてくれる1冊。
シリーズ全8記事
- インシデント9件を分析したら、影響範囲調査の本質が見えてきた
- 影響範囲調査をCIに組み込む──CARE設計の思考ログ
- 「完璧」を諦めてから、設計がすっきりした──CARE エージェントの設計ログ
- 94件のインシデントを全数分析して、設計思想ごとひっくり返った話
- 自律エージェントを諦めた翌日に浮かんだ発想──「調査レシピ方式」という転換
- 「ハルシネーション対策」を実装する前に、設計変更で消えていた──Phase 0 検証で起きた3つのピボット
- 「また同じ警告か」をどう避けるか:PRコメント生成ツールの品質を、沈黙の質から問い直す
- プロトタイプに区切りをつける:PRコメント生成ツールを「初期バージョン」にするまで
この記事は役に立ちましたか?

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