AI

git diff × LLMでコードレビューを自動化する方法

ローカル環境でgit diffをLLMに渡し、構造化されたレビュー結果をJSONで受け取る仕組みの作り方を、実装例とともに解説します。

Papapapapa
コードレビューLLM自動化Node.jsGit
git diff × LLMでコードレビューを自動化する方法

なぜローカルLLMレビューなのか

GitHub CopilotやClaude Codeなど、AIコードレビューのSaaS製品は増えています。しかし、以下のようなケースでは「ローカルで自前のレビューパイプライン」を持つメリットがあります。

  • CI/CDに組み込みたい(GitHub Actionsやローカルのpre-pushフックで自動実行)
  • レビュー観点をプロジェクト固有にカスタマイズしたい(DynamoDBのインデックス設計、IAM権限チェックなど)
  • レビュー結果を機械処理したい(JSONで受け取り、Slack通知やダッシュボード連携)

本記事では、git diff → LLM → レビュー結果(JSON) という最小パイプラインの実装方法を解説します。

全体像(最小構成)

外部プログラム(Node.js / Python等)で以下の4ステップを実行します。

  1. 差分生成git diff(例: baseブランチとの差分)を取得
  2. 入力整形 — diffが大きい場合はファイル単位・サイズ単位で分割(chunking)
  3. LLM呼び出し — OpenAI / Anthropic等に「コードレビュー用プロンプト+diff」を送信
  4. 結果取得 — JSONスキーマ固定で返させて保存(例: review.json
git diff origin/main...HEAD


  ┌─────────────┐
  │   chunking   │  ← ファイル単位 or サイズ単位で分割
  └──────┬──────┘


  ┌─────────────┐
  │  LLM API    │  ← プロンプト+diff → JSON
  └──────┬──────┘


   review.json       ← 構造化されたレビュー結果

これで「/review 相当」の処理をローカル・自動で実行できます。

出力JSONのスキーマ設計

まず「機械処理できる形」を決めるのがコツです。自由文で返されると後処理が大変なので、スキーマを固定します。

{
  "summary": "全体所見...",
  "findings": [
    {
      "severity": "critical|high|medium|low",
      "title": "問題の短い要約",
      "file": "path/to/file",
      "lineHint": "optional: around line 123",
      "evidence": "diff中の根拠引用",
      "impact": "何が壊れるか",
      "recommendation": "どう直すか"
    }
  ],
  "meta": {
    "base": "origin/main",
    "head": "HEAD",
    "model": "gpt-4.1-mini"
  }
}

各フィールドの設計意図は以下の通りです。

フィールド目的
severity4段階に限定することで、機械的なフィルタリングや通知の振り分けが可能になる
evidencediff中の根拠を引用させることで、LLMの「憶測」を抑制する
lineHint正確な行番号は保証できないため、あくまで「目安」として扱う
metaどのdiff・どのモデルで生成されたかを記録し、再現性を担保する

プロンプトのポイント

レビュー品質を上げるために、プロンプトには以下の制約を明示します。

  • 憶測禁止 — 根拠がdiffにある指摘だけを出力させる
  • 重要度分類 — critical / high / medium / low の4段階
  • 観点の明示 — 契約違反、エッジケース、Null/undefined、権限・IAM、インデックスなど
  • JSON強制 — 自由文ではなくJSONで返させる(response_format: { type: "json_object" } が使える場合はAPIオプションでも指定)

プロンプトの品質がレビューの品質を直接左右します。「何を見てほしいか」を具体的に書くほど、的確な指摘が返ってきます。

実装例(Node.js / OpenAI)

以下は git diff origin/main...HEAD を取り、チャンクに分け、JSONで返させる最小実装です。

前提

  • 環境変数 OPENAI_API_KEY が設定済み
  • Node.js 18以上
import { execSync } from "node:child_process";
import OpenAI from "openai";

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// ── Step 1: 差分を取得 ──────────────────────────
function getDiff(baseRef = "origin/main", headRef = "HEAD") {
  return execSync(`git diff ${baseRef}...${headRef}`, { encoding: "utf8" });
}

// ── Step 2: チャンク分割 ─────────────────────────
// 簡易的なサイズ分割(実運用では「ファイル単位→大ファイルはさらに分割」がおすすめ)
function chunkText(text, maxChars = 12000) {
  const chunks = [];
  for (let i = 0; i < text.length; i += maxChars) {
    chunks.push(text.slice(i, i + maxChars));
  }
  return chunks;
}

// ── Step 3: LLMにレビューを依頼 ──────────────────
const schemaHint = `
Return JSON ONLY with this structure:
{
  "summary": string,
  "findings": [
    {
      "severity": "critical"|"high"|"medium"|"low",
      "title": string,
      "file": string|null,
      "lineHint": string|null,
      "evidence": string,
      "impact": string,
      "recommendation": string
    }
  ]
}
`;

async function reviewDiffChunk(diffChunk) {
  const prompt = `
You are a senior software engineer performing a thorough code review.
Focus on: logic errors, edge cases, null/undefined, race conditions,
security, API contract violations, DynamoDB/index correctness, IAM/permissions.
Do NOT speculate. Only report issues you can justify from the diff.

${schemaHint}

DIFF:
${diffChunk}
`;

  const res = await openai.chat.completions.create({
    model: "gpt-4.1-mini",
    temperature: 0.2,
    messages: [{ role: "user", content: prompt }],
    response_format: { type: "json_object" },
  });

  return JSON.parse(res.choices[0].message.content);
}

// ── Step 4: 結果を統合 ──────────────────────────
function mergeResults(results) {
  const merged = { summary: "", findings: [] };
  merged.findings = results.flatMap((r) => r.findings || []);
  merged.summary = `Chunks: ${results.length}. Findings: ${merged.findings.length}`;
  return merged;
}

// ── メイン処理 ──────────────────────────────────
async function main() {
  const diff = getDiff(
    process.argv[2] ?? "origin/main",
    process.argv[3] ?? "HEAD"
  );

  if (!diff.trim()) {
    console.log(
      JSON.stringify({ summary: "No diff", findings: [] }, null, 2)
    );
    return;
  }

  const chunks = chunkText(diff);
  const results = [];
  for (const c of chunks) {
    results.push(await reviewDiffChunk(c));
  }
  const merged = mergeResults(results);

  console.log(JSON.stringify(merged, null, 2));
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

実行方法

# デフォルト(origin/main との差分)
node review.mjs

# 特定のブランチ間の差分
node review.mjs origin/develop HEAD

# 結果をファイルに保存
node review.mjs > review.json

実用レベルに引き上げるための改良ポイント

チャンキングの改善

上記の実装では単純な文字数分割ですが、実運用では diff --git の区切りでファイル単位に分割するのがベターです。

function chunkByFile(diff) {
  return diff
    .split(/^diff --git /m)
    .filter(Boolean)
    .map((chunk) => "diff --git " + chunk);
}

これにより、1つのファイルの差分が途中で切れてコンテキストが失われる問題を回避できます。大きなファイルだけさらに分割するハイブリッド方式がおすすめです。

2段階レビュー

チャンク数が多い場合、1回目の各チャンクの findings を結合した後、2回目のLLM呼び出しで重複統合・優先度調整・全体要約を生成させると、精度が上がります。

1回目: 各chunkごとに findings 抽出

2回目: findings全体をLLMに渡して
       重複統合 + 優先度再調整 + summary生成

ノイズの除外

バイナリ差分やlockfile(package-lock.json, yarn.lock)はレビュー対象から除外しましょう。トークンの無駄遣いを防ぎ、レビュー精度が向上します。

function filterDiff(diff) {
  return chunkByFile(diff).filter((chunk) => {
    const file = chunk.match(/^diff --git a\/(.+?) /)?.[1] ?? "";
    const excludes = [
      /\.lock$/,
      /\.min\./,
      /dist\//,
      /\.zip$/,
      /\.png$/,
      /\.jpg$/,
    ];
    return !excludes.some((re) => re.test(file));
  });
}

運用上の注意点

トークンとコスト

diffが大きいとトークン消費が増えます。ファイルフィルタやチャンキングでの入力量削減は必須です。目安として、1回のレビューを数百円以内に収めるには、diffを1万〜2万トークン程度に抑えるのが現実的です。

秘匿情報の取り扱い

社内コードを外部APIへ送ることになるため、以下の対策を検討してください。

  • 社内規約・利用許可の確認
  • .envや認証情報を含むファイルの除外
  • APIプロバイダのデータ保持ポリシーの確認(OpenAI APIはトレーニングに使わない等)

再現性の確保

どの base / head でdiffを取ったかを meta フィールドに入れることで、後から同じレビューを再現できます。CIで使う場合は、コミットハッシュも記録しておくと有用です。

CI/CD連携

GitHub Actionsなら、PRの差分をそのまま diff として投げられます。

- name: AI Code Review
  run: |
    DIFF=$(gh pr diff ${{ github.event.pull_request.number }})
    echo "$DIFF" | node review.mjs --stdin

結果をPRコメントとして投稿すれば、チーム全員がレビュー結果を確認できます。

まとめ

git diff → LLM → JSON という最小パイプラインは、わずか数十行のコードで構築できます。ポイントは以下の3つです。

  1. 出力スキーマを先に決める — 自由文ではなくJSONで固定し、機械処理可能にする
  2. プロンプトで観点と制約を明示する — 憶測禁止、重要度分類、根拠引用
  3. チャンキングとフィルタでコストを制御する — ファイル単位分割、lockfile除外

この仕組みは、既存のCI/CDパイプラインに組み込むことも、ローカルのpre-pushフックとして使うことも可能です。当社でもPR作成時の自動レビューとして活用しており、人間のレビュアーが本質的な設計判断に集中できる環境づくりに貢献しています。

コードレビューの自動化に興味がある方は、ぜひお問い合わせください。

この記事をシェア

関連記事