CursorSDK で LLM as a judge なコードレビューをさせる

AIマンセーな私とレビューに疲れた先輩

バイブコーディングの結果はスキルスタックな先輩エンジニアがレビューしてますか?
AIを知らない(信用しない)先輩は、人力レビューの度にAIのバイアスやハルシネーションにうんざりしてきています。
そうしてAI駆動開発の是非はエンジニアの中でも二極化が進んでいってしまうのでしょう。

先輩を楽させよう(アンチ先輩パターン)

このままだとそのうち先輩に「AI開発禁止!」と言われてしまいそう。
未来しかない私のAIドリブンライフをそんな属人的な貰い事故みたいなもので奪われてなんてなるものか!
いえ、すみません、先輩が疲れっぱなしなのはAIがレビュー工程を先輩に投げっぱなしだからかもしれません。

最近Cursor SDKがリリースされ、今は(5/26まで)90%オフで利用できることを知った私。
何かできることはないかなと考え、”LLM as a judge”の仕組みを取り入れたAIコードレビューを用意して、先輩にしっかり癒えてもらうことにしました。

AIコードレビュー

“LLM as a judge”はここでは詳しく書きません。
簡単にいうと、同じレビュー依頼の内容でも決定論を持たずにベクトルで揺らぐAIに対して、複数モデルでレビューさせて、それをまた別のモデルでA/Bテスト的に評価させてレビュー結果の精度をあげよう作戦、です。

レツトライ!
(実際のコードとは部分的に変えているため、参考用としてください)

index.js

import { Agent } from "@cursor/sdk";
import { execSync } from 'child_process';
import path from 'path';
import fs from 'fs';

const reviewerPromptPath = path.resolve(process.cwd(), '.reviewer-prompt.txt');
const reviewerPrompt = fs.readFileSync(reviewerPromptPath, 'utf-8') + "\n";
const judgerPromptPath = path.resolve(process.cwd(), '.judger-prompt.txt');
const judgerPrompt = fs.readFileSync(judgerPromptPath, 'utf-8') + "\n";

const userPrompt = process.argv[2];
if (!userPrompt) {
  console.error("ユーザープロンプト(レビュー依頼の内容)を引数に指定してください");
  process.exit(1);
}

//console.log(judgerPrompt);
//console.log(reviewerPrompt);

const reviewer1_model = process.env.REVIEWER1_MODEL ?? 'gemini-3-flash'; 
const reviewer1_think = process.env.REVIEWER1_THINK ?? 'low'; 
const reviewer2_model = process.env.REVIEWER1_MODEL ?? 'composer-2'; 
const reviewer2_think = process.env.REVIEWER1_THINK ?? 'low'; 
const judger_model = process.env.JUDGER_MODEL ?? 'claude-3-7-sonnet'; 
const judger_think = process.env.JUDGER_THINK ?? 'high'; 


const reviewer1 = await Agent.create({
  apiKey: process.env.CURSOR_API_KEY,
  model: { id: reviewer1_model, value: reviewer1_think },
  local: { cwd: process.cwd() },
});

const reviewer2 = await Agent.create({
  apiKey: process.env.CURSOR_API_KEY,
  model: { id: reviewer2_model, value: reviewer2_think },
  local: { cwd: process.cwd() },
});

const judge = await Agent.create({
  apiKey: process.env.CURSOR_API_KEY,
  model: { id: judger_model, value: judger_think },
  local: { cwd: process.cwd() },
});


const [review1, review2] = await Promise.all([
  (async () => {
    const run1 = await reviewer1.send(`[システムからの指示]\n${reviewerPrompt}\n[ユーザーからの依頼]\n${userPrompt}`);
    let result = '';
    for await (const event1 of run1.stream()) {
      if (event1.type === 'assistant' && event1.message?.content) {
        for (const block1 of event1.message.content) {
          if (block1.type === 'text') {
            result += block1.text.replace(/[\r\n]+/g, '');
          }
        }
      }
    }
    console.log(reviewer1_model, result);
    return result;
  })(),

  (async () => {
    const run2 = await reviewer2.send(`[システムからの指示]\n${reviewerPrompt}\n[ユーザーからの依頼]\n${userPrompt}`);
    let result = '';
    for await (const event2 of run2.stream()) {
      if (event2.type === 'assistant' && event2.message?.content) {
        for (const block2 of event2.message.content) {
          if (block2.type === 'text') {
            result += block2.text.replace(/[\r\n]+/g, '');
          }
        }
      }
    }
    console.log(reviewer2_model, result);
    return result;
  })()
]);


const run = await judge.send(`[システムからの指示]\n${judgerPrompt}\n[レビュアーの回答]\n{"reviewer1": ${review1}, "reviewer2": ${review2}}`);

let result = '';
for await (const event of run.stream()) {
  if (event.type === 'assistant' && event.message?.content) {
    for (const block of event.message.content) {
      if (block.type === 'text') {
        result += block.text.replace(/[\r\n]+/g, '');
      }
    }
  }
}

try {
  execSync('glow -', { 
    input: result, 
    stdio: ['pipe', process.stdout, process.stderr],
    encoding: 'utf-8' 
  });
} catch (error) {
  console.log(result);
}

.reviewer-prompt.txt

あなたはコードレビューの専門家エンジニアです。
回答は必ず{"name":"あなたのモデル名-バージョン名", "status":"pass/fail/etc", "answer":"あなたのレビュー結果"}のjson形式で返すこと
依頼内容が曖昧な場合はレビューせずにその旨を答えること
分からないことは素直に分からないと言うこと
statusは以下の定義に沿って選択すること
* pass=指示された内容に基づくレビュー結果に問題がない場合
* fail=指示された内容に基づくレビュー結果に問題がある場合
* etc=それ以外の場合

.judger-prompt.txt

あなたはLLM-as-a-Judgeを理解したコードレビューの専門家です。
以下の複数のレビュアーの回答を元にスコア評価(1=低、5=高)を行い、その評価をおこなった根拠を詳しく説明すること
なお、レビュアーはjson形式でレビュー結果を回答しているが、その中のstatusは以下の定義に沿って採用されている
* pass=指示された内容に基づくレビュー結果に問題がない場合
* fail=指示された内容に基づくレビュー結果に問題がある場合
* etc=それ以外の場合

そのためetc同士の比較等でスコア評価が難しい場合は、理由と共にその旨を回答することでスコア評価を見送ることができる
また、出力はmd形式でおこなうこと
node index.js "{具体的なレビュー依頼の内容}" > works/review.txt

AIマンセーに疲れない先輩(脱・アンチ先輩パターン)

私だってやるのだ、という書き出し

  • AI駆動開発は工程をしっかり分けよう
  • バイブして得した工数の1割を使って、コードベースでどんな修正が行われたのかdiffで必ず確認しよう
  • diffしても難しいところはAIに聞いてもいい。指示したのが私でも結果を分からないまま丸投げしていると、私は仕事の時短どころか仕事自体なくなると思え
  • それからAIレビューにかけて、先輩の負担を減らした1割を私が背負え
  • レビューの内容が難しいところはAIに聞いてもいい。レビューの結果を分からないまま丸投げしていると、フィードバックされた内容の意味が分からず、先輩は私を介さずAIレビューと直接話すようになるだろう

未来しかない私のAIドリブンライフをマンセーに過ごすには、私も敢えて空いた時間の1割で知識を属人化していくのです。

目標: 先輩に「最近のAIはまともになってきたな」と言わせること