一覧に戻る
    「完璧」を諦めてから、設計がすっきりした──CARE エージェントの設計ログ
    開発ラボ
    PRこの記事には広告が含まれています

    「完璧」を諦めてから、設計がすっきりした──CARE エージェントの設計ログ

    14 分で読める

    前回の記事では、CARE(Context-Aware Review Engine)というコンセプトを設計した。要点をまとめると、こういう仕組みだった。

    • PR の変更内容と「レビュー観点ルール」を照合する
    • ルールに合致したら、LLM が実際にコードや DB・AWS を調査してレポートを出す
    • 使うほどルールが育つ自己強化ループ

    設計の骨格は決まったものの、「この仕組みは本当に機能するのか」という根本的な問いにちゃんと答えられていなかった。

    今回は、その問いを考えるヒントが思わぬところから見つかった話と、それを踏まえてエージェントの設計を具体化した話。

    静的解析では防げないのか、防げないのか

    ドキュメントを整理しながら、別の問いが頭に浮かんできた。「そもそも、コードの変更が何に影響するかを静的解析で完全に追えないのか?」

    その問いを調べていたときに見つけたのが、@rairaii さんが書いた Qiita の記事「コード変更の影響範囲、完全に自動で追えるのか? Forward Slice による影響範囲調査の自動化の技術的限界と現実的選択」だった。

    読んでみると、結論が明快だった。

    完全自動化は原理的に不可能。

    プログラムの振る舞いの正確な静的判定は決定不能問題に帰着する(Rice の定理)。変更点から全影響を網羅的に追跡しようとすると、特に動的な言語やポインタ演算を含む場合、「見落としゼロ(Sound)」を目指した瞬間にプログラムのほぼ全体が影響範囲として報告されてしまう。Google でさえ完璧な手続き間解析インフラは保有しておらず、業界標準は「Soundy(ほぼ健全)」な近似解だという。

    正直、最初に読んだときは少し複雑な気持ちになった。「そんなに難しいなら、やっぱり無理なのか」という諦めと、「でも待って、この理論はそのまま今の問題に当てはまるのか?」という疑問が混在していた。

    天井と床の間に線を引く

    少し立ち止まって考えてみると、この記事が示しているのは「理論的な天井」だった。完璧は無理、という天井。

    一方、前回の分析で手元にあるのは「実際に起きた9件のインシデント」という床だ。

    天井は高いところにある。でも床は手元にある。「必要十分」の線は、その天井と床の間のどこかに引けるはずだ。

    9件の分析データを見直すと、拾えるもの・拾えないものの境界がかなり具体的に見えてきた。

    拾える拾えない
    PR 起因の変更から辿れる依存PR 外の作業(データ移行、手動オペ)
    ルールが定義済みの観点未知の観点(初めて遭遇するパターン)
    コード・DB・AWS から読み取れる事実実行時の入力依存で初めて顕在化するもの

    「未知の観点」については、インシデント駆動・PR 駆動でルールが育つ設計になっているので、時間軸で改善される領域だ。一方、「PR 外の作業」や「実行時の入力依存」は、構造的に対象外と割り切るしかない。

    このフレーミングが決まったとき、設計全体がすっと腑に落ちる感覚があった。

    Forward Slice の爆発問題と CARE の構造的な違い

    もう一歩踏み込んで考えると、CARE はそもそも Forward Slice とは設計思想が違うことがわかった。

    Forward Slice の爆発問題は「変更点から全影響を網羅的に追跡する」アプローチで起きる。CARE はそうじゃない。diff に対してルールがマッチしたら、決められた問いを調べに行くという形をとっている。

    つまり、網羅的な探索ではなく、症状に応じた検査に近い。医師が全身スキャンを毎回やるのではなく、症状からどの検査をすべきか判断するのと同じ構造だ。

    この整理から、CARE は意図せず "Soundy" なアプローチになっていたと言える。完全な網羅性を諦めたことが、実用性を担保している。

    設計に3つの境界を引いた

    理論的な根拠が整理できたので、エージェント設計の核になる3つの境界をドキュメントに追加した。

    境界1: ルールが調査のスコープを制御する

    ルールが「何を」「どこまで」調べるかを制御する。ルールのないところは LLM も調べない。これが Forward Slice の爆発を防ぐ壁になる。

    ルールの構造はこのイメージ。

    trigger: どんな diff で発火するか(テーブル定義の変更、特定ディレクトリの変更など)
    question: 何を調べるか(このテーブルを参照している他サービスはあるか)
    scope: どこまで調べるか(対象リポジトリ一覧、DB スキーマなど)
    depth: 何ホップ先まで追うか
    origin: なぜこのルールが存在するのか(インシデントとの紐付け)

    depth を設けているのがミソで、1ホップ先まで追うのか、2ホップ先まで追うのかでコストが大きく変わる。最初は depth: 1 を基本にして、必要に応じて広げる設計にした。

    境界2: LLM の役割は「証拠収集」であり「判定」ではない

    これは設計の中で一番大事にしたポイントかもしれない。

    LLM に「安全 / 危険」の判定をさせると、誤判定がそのまま事故になる。人間が「LLM が大丈夫と言っていたから確認しなかった」という状態になるのが最も怖い。

    だから、LLM には事実の提示だけをさせる。

    良い出力: 「table_x は外部サービス B の UserService.java:142 で JOIN されています」
    悪い出力: 「この変更は安全です」

    判断は文脈を知っている開発者がする。LLM が見つけてきた「事実」をレビュアーが評価する、という役割分担にする。誤判定のリスクはレビュアーの目で吸収できるし、LLM が見落としたとしても、「確認観点をリストアップしてくれた」だけで十分に価値がある。

    境界3: 「拾えないもの」を明示する

    必要十分の線を引くなら、拾わないものを宣言しておく方が健全だ。

    これがないと、「CARE が何も言わなかったから大丈夫だろう」という過信が生まれる。仕組みへの信頼と、仕組みの限界への理解は、セットで持ってほしい。

    エージェントの処理フロー

    3つの境界を踏まえて、エージェントの処理フローを設計した。

    ポイントは「合致なしの場合もリスクシグナルを拾う」分岐がある点だ。ルールが存在しないからといって何もしないのでは、ルールの盲点が永遠に盲点のままになってしまう。

    エージェントが持つツール

    調査に使うツールは3種類。すべて読み取り専用に限定している。

    コード調査ツール

    • リポジトリ横断検索(特定のテーブル名・関数名・モジュールを参照しているコードを探す)
    • ファイル読み取り(コンテキストを把握するために周辺コードを読む)
    • Git 履歴参照(最近の変更履歴、関連する PR を確認する)

    DB 調査ツール

    • スキーマ参照(テーブル定義、カラム定義、制約を確認する)
    • インデックス情報(クエリの対象カラムにインデックスがあるか確認する)
    • テーブル統計(行数、更新頻度などパフォーマンス影響の推定に使う)

    読み取り専用に限定しているのは当然として、DB 調査ツールへのアクセスは本番 DB を直接参照しない設計にする予定だ。本番 DB のメタ情報(スキーマ・統計情報)を別途エクスポートして参照する形か、読み取り専用のレプリカを使う形か、Phase 1 の実装で詰める。

    AWS 調査ツール

    • リソース構成の参照(Lambda・EC2 のランタイム、アーキテクチャ、メモリ設定)
    • 環境変数・設定の参照(デプロイ先の実行環境の確認)
    • メトリクスの参照(リソース使用率の傾向確認)

    レポートの設計

    エージェントが出力するレポートには、品質基準を設けることにした。

    • 具体性: 「参照あり」ではなく「リポジトリ B の UserService.java:142 で参照あり」
    • 追跡可能性: なぜその情報を取得したか(どのルールが起動したか)を明示する
    • 簡潔性: 1つのルールに対して長くても5〜10行。読まれないレポートに意味はない
    • 透明性: 「このルールは調査済み / 未調査」「調査に失敗したもの」を明示する

    レポートの形式は、PR コメントとして投稿されることを前提にしたマークダウン形式。

    ## CARE 影響範囲調査レポート
    
    ### 適用されたルール: 共有テーブルのスキーマ変更
    
    **調査内容**: table_x を参照している他のコード・サービスを確認しました。
    
    **発見した事実**:
    - サービス B (UserService.java:142): SELECT 文で JOIN しています
    - バッチジョブ C (batch/daily.py:89): 全件参照して CSV 出力しています
    
    **調査範囲外**:
    - 本番 DB でのインデックス有無(Phase 2 以降で対応予定)
    
    ---
    このレポートは CARE が自動生成しました。判断は開発者が行ってください。

    最後の一文が大事だ。「判断は開発者が行ってください」と明示することで、仕組みへの過信を防ぐ。

    スコープ制御とコスト管理

    Phase 1 では手動トリガーから始めるので、PR ごとのコストを実際に測りながら判断できる。ただし、自動化フェーズに備えていくつかの制御項目を設計段階から決めておく。

    • トークン上限: ルールごとに上限を設ける。予算超過の場合は「調査途中で打ち切り」と明示する
    • タイムアウト: 1ルールあたりの調査時間の上限
    • ツール呼び出し回数: depth 設定に連動させる(depth: 1 であれば、各スコープへの参照は最大1回)

    コストで一番気になっているのは、ルールが増えてくると「1PR あたりの調査ツール数」が増えること。ルールが10個あって全部発火したら10種類の調査が走る。Phase 1 の検証でこの数感を掴んで、自動化フェーズの投資判断につなげる。

    Phase 1 で確認すること

    設計はできた。でも、これはまだ机上の設計だ。

    Phase 1(手動検証フェーズ)で確認すべきことを整理した。

    調査精度: 発見した事実が実際に関係しているか(ノイズ量)

    見落とし量: 本来発見すべき依存を見落としていないか

    コスト: 1ルールあたりのトークン消費・調査時間

    有用性: レビュアーが「このレポートで判断できた」と感じるか

    ツールアクセスの実現性: DB・AWS への読み取り専用アクセスが実際に設定できるか

    特に、ノイズ量が定着の鍵になると思っている。レポートが「見なくていい情報の羅列」になった瞬間、レビュアーはレポートを読まなくなる。そうなると自己強化ループが崩れる。最初の数週間でノイズ率のベースラインを測定して、ルールの粒度調整に使う。

    今の率直な感触

    理論的な根拠が整理できたことで、CARE の設計がなぜこの形なのかを説明できるようになった。

    「Soundy なアプローチ」という言葉は、後付けで見つけた言語化だ。最初からその言葉があったわけじゃない。でも、理論的な裏付けが見つかったことで、「これはトレードオフを意識して選んだ設計だ」と自信を持って言えるようになった。

    完璧を諦めることを、諦めた。それからやっと、現実的な線が引けた。

    引き続き、Phase 1 の実装とその結果を書いていく。

    参考

    影響範囲の設計判断や監視・観測の体系的な考え方を深めたい方には、以下の書籍が参考になります。

    この記事は役に立ちましたか?

    Coffee cup

    この記事が、何かの整理につながったら

    コーヒー1杯分の応援をもらえると嬉しいです。

    ※ これは応援とは別の話ですが、

    同じようなテーマを自分の文脈で整理したい場合は、 (文章だけだと詰まりやすい人向けに) 思考整理の壁打ちという形で対話の時間も取っています。

    対話の時間について