AIコードレビュアーをCIに組み込んで満足していたら、忙しい1週間のPRラッシュ後の請求書を見てびっくりした。プッシュのたび、コミットのたびに、完全な差分を大型のフロンティアモデルに送信していた。思ったより早く金額が膨らみ、その大半が天才が読む必要のないフォーマット変更やREADME編集だった。そこで2段階構成に作り直したところ、大事な検知を失うことなくコストが大幅に下がった。
考え方はシンプルだ。すべてを高額モデルに送らない。安価なモデルを門番役にして、何をエスカレーションすべきかを判断させ、本当に危険を及ぼし得るファイルだけを大きなモデル(キャッシュ有効)に渡すようにする。
第1段階: 安価なトリアージパス
第1段階では差分を見て1つの質問に答える。「ここに本物のレビューを正当化できるほどのリスクはあるか?」。これは分類タスクであり、フロンティアモデルは必要ない。
この段階は小型のローカルモデルで実行する。自分のマシンではOllama + qwen2.5-coderを使っているが、CIではセルフホストの小型モデルや、プロバイダーが提供する最安のクラウドティアで十分だ。仕事は判断ではなく振り分けである。
トリアージ用プロンプトは意図的に狭くしている。レビューはさせず、ルーティングだけをさせる:
You are a triage filter for code review. For the diff below, output JSON only:
{ "risk": "low" | "high", "reason": "<one short phrase>" }
Mark "high" if the diff touches: auth, access control, money/token math,
cryptography, SQL or shell string building, deserialization, file paths,
network calls, or anything that changes who can do what.
Mark "low" for formatting, comments, docs, tests, renames, and config bumps.
DIFF:
<diff here>
Enter fullscreen mode Exit fullscreen mode
小型モデルでも「これがauthや金銭に関わるか」は十分判断できる。「low」と判定された場合、CIは一行の「高リスク変更は検出されませんでした」を投稿して終了する。高額なAPI呼び出しは発生しない。実際のところ、ほとんどのPRはドキュメント、テスト、配管作業なので、この段階で大部分をショートカットできる。
第2段階: 大型モデルをリスクのあるファイルにのみ使用し、キャッシュを活用
第1段階が「high」と判定したとき、リスクのあるファイルだけを強力なモデルにエスカレーションする。ここで本当のレビューが行われ、ここでプロンプトキャッシュが威力を発揮する。
コツは、大型モデルに送信する内容のほとんどがレビュー間で変わらないことだ。システムプロンプト(レビュールーブリック、重大度の定義、社内ルール)は毎回同一であり、リポジトリコンテキスト(周辺ファイル、規約、変更されたコードが依存するインターフェース)も同じ領域に触れる小さなPR群では安定している。この安定したプレフィックスをキャッシュすれば、初回にフル価格を支払い、以後のレビューではわずかな割合しか課金されない。
そこでリクエストを次のように構成する: キャッシュ済みシステムプロンプト、キャッシュ済みリポジトリコンテキスト、そして最後に最新の差分。Anthropicスタイルのキャッシュは安定したプレフィックスをキーとするため、順序が重要になる。安定したものを先、変動するものを後に置く。
# pseudocode, provider-agnostic shape
response = client.messages.create(
model="big-model",
system=[
{
"type": "text",
"text": REVIEW_RUBRIC, # never changes
"cache_control": {"type": "ephemeral"},
},
{
"type": "text",
"text": repo_context_for(files), # changes rarely
"cache_control": {"type": "ephemeral"},
},
],
messages=[
{"role": "user", "content": build_review_prompt(diff)}, # changes every time
],
)
Enter fullscreen mode Exit fullscreen mode
ルーブリックとリポジトリコンテキストはキャッシュに乗る。差分だけが毎回新しくなる。同じモジュールに小さなPRが頻繁に当たるリポジトリではキャッシュヒット率が高く、大型モデルの1レビューあたりのコストは主に新規トークン分だけになる。
GitHub Actionsのスケッチ
これらを繋ぐのは特別なことではない。1つのジョブ、2つのステップ、条件付きエスカレーション:
name: ai-review
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
persist-credentials: false
- name: Triage (cheap model)
id: triage
run: |
git diff origin/${{ github.base_ref }}...HEAD > diff.patch
python scripts/triage.py diff.patch > triage.json
echo "risk=$(jq -r .risk triage.json)" >> "$GITHUB_OUTPUT"
- name: Deep review (big model, cached)
if: steps.triage.outputs.risk == 'high'
env:
MODEL_API_KEY: ${{ secrets.MODEL_API_KEY }}
run: python scripts/deep_review.py diff.patch
Enter fullscreen mode Exit fullscreen mode
2番目のステップのif:がコスト全体の鍵だ。低リスクのPRは高額モデルに到達しない。高リスクのPRだけが完全な処理を受ける。
diff-onlyレビューで見逃す落とし穴
ここが人々が間違えやすい部分であり、素朴なAIレビュアーが誤った自信を与える理由だ。差分だけを送ると、モデルはファイル横断のバグを見ることができない。
あるPRが関数の戻り値型やパラメータの意味を変更したとする。差分は局所的には問題ないように見える。レビュアーはその行だけを見て承認する。しかし、その関数を呼び出している3つの別ファイルは古い前提のまま呼び続けていて、それらのファイルは差分に一切含まれていない。バグは実在し、深刻で、diff-onlyレビューは構造的にそれを見逃す。
安価な解決策は、変更された関数の呼び出し元を送信コンテキストに含めることだ。リポジトリ全体を送る必要はない。変更されたシンボルと、それに触れるすべてのファイルを送ればよい:
# find who calls the functions changed in this PR
changed_funcs=$(git diff origin/main...HEAD \
| grep -oP '^\+.*function \K\w+' | sort -u)
for fn in $changed_funcs; do
grep -rl "$fn(" src/ >> caller_files.txt
done
sort -u caller_files.txt
Enter fullscreen mode Exit fullscreen mode
これらの呼び出し元ファイルを第2段階のコンテキスト(キャッシュされる部分)に投入する。呼び出し元はゆっくりとしか変わらないため、キャッシュに乗る。インターフェースを変更する修正があったとき、モデルは旧来の動作に依存していたコードを確認でき、不整合を指摘できる。この1つの追加で、純粋なdiffレビューでは決して見つからないクラスのバグを捉えられる。そして呼び出し元は安定しているため、初回のキャッシュ以降はほぼコストがかからない。
これで実際に得られるもの
経済性は安価なモデルに難しい仕事をさせるのではなく、ルーティングから生まれる。小型モデルはフィルタであり、フィルタリングは安価で簡単だ。大型モデルは本当の仕事をするが、それはそれに値するわずかな部分に限られ、ルーブリックとコンテキストはキャッシュに乗るため、変動する差分に対してのみフル価格を支払う。
私はこれらの手法をspectr-aiのコスト推論にも一部取り入れている: 高速なローカルパスで慎重かつ高額な検証に値するかを判断し、実際に価値のある部分だけを高額検証する。トリアージ段階にローカルでqwen2.5-coderを使うことで、その段階はすでに所有しているハードウェアで事実上無料になる。
避けるべき失敗モードは、安価な段階をレビュアー扱いすることだ。そうではない。それは門番だ。トリアージモデルがレビューコメントを書き始めていたら、難しい仕事を間違ったツールに与えていることになる。第1段階は狭く保ち、第2段階には呼び出し元とキャッシュ済みコンテキストを十分に与えれば、請求額は小さく保ちつつ、本物の検知を維持できる。
AIレビューをCIで運用している場合、差分だけを送っていますか、それとも変更されたものの呼び出し元も取り込んでいますか?
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.