背景
最近、業務データとLLMを繋ぐためにMCPサーバーを自作していました。ツールを実装しながら気になったのが、「ユーザーがAI Agentに投げる質問に対して、MCPツール経由で正しい回答を返せるのか」という点です。
そこでLLM-as-a-Judge(LLMの出力を別のLLMで評価する手法)を試してみることにしました。評価用のLLMには安価なモデルを使う手もありますが、Claude Codeの定額プランに加入していたので、追加コストなしで使えるClaude Codeのカスタムスキルを評価に活用してみました。
この記事でやること
自作MCPサーバーに対して用意した質問と正解のペアをAI Agentに回答させ、その結果をClaude Codeのカスタムスキルで自動評価する仕組みを作ります。サンプルとして社内図書館の書籍管理システムを使います。
検証環境
- Python 3.12+ / uv
- MCPサーバー: FastMCP + PostgreSQL(蔵書検索・貸出管理など5つのツールを提供)
- AI Agent: OpenAI GPT-5.4(MCP経由でツールを呼び出すエージェントループ)
- 評価: Claude Code カスタムスキル
/checkAnswer
./
├── .claude/skills/checkAnswer/
│ └── SKILL.md # 評価スキル定義
└── src/
├── server.py # MCPサーバー
├── answer_qa.py # QA回答エージェント
├── qa.csv # 質問と正解のペア
├── answers.jsonl # answer_qa.py の出力(AI回答)
└── report.md # /checkAnswer の出力(評価レポート)
仕組み
質問と正解のペア(qa.csv)
質問と正解を以下の形式で用意します(抜粋)。
プロンプト例,答え
蔵書は全部で何冊ありますか?,30冊
エンジニアリング部の利用者は何人ですか?,6人(田中太郎・鈴木花子・佐藤一郎・高橋美咲・木村裕子・林大輔)
最も多く本を借りている利用者は誰ですか?,田中太郎(14回)
AI Agentによる回答生成(answer_qa.py)
qa.csv から質問だけを読み込み、LLMがMCPツール経由で回答を生成し、answers.jsonl に記録します。正解はLLMには渡さず、あとから照合用に使います。エージェントループ自体は OpenAI API の tool calling を使った実装です。
answer_qa.py の主要部分(クリックで展開)
async def run_agent(session: ClientSession, client: OpenAI, tools: list[dict], question: str) -> str:
"""1つの質問に対してMCPツールを使いながら回答を生成する"""
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": question},
]
for _ in range(MAX_TURNS):
response = client.chat.completions.create(
model=MODEL, messages=messages, tools=tools,
)
choice = response.choices[0]
if choice.finish_reason == "stop":
return choice.message.content or ""
if choice.finish_reason == "tool_calls":
messages.append(choice.message)
for tool_call in choice.message.tool_calls:
args = json.loads(tool_call.function.arguments)
result = await session.call_tool(tool_call.function.name, arguments=args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result.content),
})
continue
return choice.message.content or ""
return "N/A: ターン数上限に達しました"
async def answer_qa(session: ClientSession, client: OpenAI, tools: list[dict]):
"""qa.csv の全質問に回答し answers.jsonl に記録する"""
qa_path = Path(__file__).parent / "qa.csv"
output_path = Path(__file__).parent / "answers.jsonl"
with open(qa_path, encoding="utf-8") as f:
questions = list(csv.DictReader(f))
with open(output_path, "w", encoding="utf-8") as out:
for i, row in enumerate(questions, 1):
question = row["プロンプト例"] # 質問のみ取り出す
answer = await run_agent(session, client, tools, question)
record = {**row, "AI回答": answer} # 正解も含めてJSONLに記録
out.write(json.dumps(record, ensure_ascii=False) + "\n")
out.flush()
async def main():
"""MCPサーバーを起動してQA回答を実行する"""
server_params = StdioServerParameters(
command="uv",
args=["run", str(Path(__file__).parent / "server.py")],
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = mcp_tools_to_openai((await session.list_tools()).tools)
await answer_qa(session, OpenAI(), tools)
評価スキルの定義(SKILL.md)
Claude Code のカスタムスキルで /checkAnswer を作り、以下の内容で .claude/skills/checkAnswer/SKILL.md に配置しました。
---
name: checkAnswer
description: answers.jsonl を読み込み、AI回答と正解を照合してレポートを出力する
---
# checkAnswer スキル
answer_qa.pyが出力した answers.jsonl を読み込み、AI回答と正解を照合してレポートを出力する。
## 手順
### 1. AI Agentの実行
まず、answer_qa.py を実行して answers.jsonl を生成する。
```bash
cd src && uv run answer_qa.py
```
### 2. answers.jsonl の読み込みと照合
`src/answers.jsonl` を1行ずつ読み込む。各行は以下の形式のJSONである:
```json
{"プロンプト例": "質問文", "答え": "正解", "AI回答": "AIの回答"}
```
1行ずつ「答え」と「AI回答」を比較し、以下の基準で判定する:
- **OK**: 数値・割合がすべて正解と一致(丸め誤差は許容)
- **PARTIAL**: 一部の数値は正しいが、欠落や不一致がある
- **NG**: 数値が大きく異なる、または回答が間違っている
- **N/A**: 回答不能だった(ツールで取得できない等)
- **SKIP**: 正解が空欄の質問
### 3. レポート出力
以下の形式でレポートを `src/report.md` に出力する:
(レポートのテンプレート — サマリーテーブル + 各問の詳細)
スキルの実行結果
/checkAnswer を実行すると、src/report.md に以下のようなレポートが出力されます。
# checkAnswer レポート
実行日時: 2026-03-24 11:22
## サマリー
| 判定 | 件数 |
|------|------|
| OK | 13 |
| PARTIAL | 3 |
| NG | 1 |
| N/A | 3 |
| SKIP | 0 |
| **合計** | **20** |
正答率: 65.0%(OK 13 / (OK 13 + PARTIAL 3 + NG 1 + N/A 3))
各問の詳細(抜粋)
### Q2: ジャンル別の蔵書数と比率を教えてください
- 判定: OK
- AI回答: 技術書11冊(36.7%) ビジネス5冊(16.7%) 文学5冊(16.7%) 科学5冊(16.7%) 自己啓発4冊(13.3%)
- 正解: 技術書11冊(36.7%) ビジネス5冊(16.7%) 文学5冊(16.7%) 科学5冊(16.7%) 自己啓発4冊(13.3%)
- 備考: 全数値・比率が完全一致
### Q8: エンジニアリング部の利用者は何人ですか?
- 判定: NG
- AI回答: 0人
- 正解: 6人(田中太郎・鈴木花子・佐藤一郎・高橋美咲・木村裕子・林大輔)
- 備考: AIが0人と回答しており正解の6人と大きく異なる。部署名の検索条件が正しく機能しなかった可能性がある
### Q17: 最も多く本を借りている利用者は誰ですか?
- 判定: PARTIAL
- AI回答: 田中太郎(8回)
- 正解: 田中太郎(14回)
- 備考: 利用者名は正解だが、貸出回数が8回 vs 14回で不一致。貸出履歴100件の制限により全件を集計できなかった可能性がある
失敗した7問を分析すると、大半はMCPツール側の設計不足(件数上限・パラメータ不足・出力形式)に起因しており、評価の結果がそのままツール改善のTODOリストになることがわかります。
この手法のメリット・デメリット
ここまでの内容をふまえ、この手法には以下のメリット・デメリットがあると感じました。
メリット
- 評価用のプログラムを作らなくても
SKILL.mdを書くだけで評価可能 - 定額プランなら評価時に追加料金がかからない
- 安価なモデルを使う場合に比べて、評価の品質が高いと期待できる
デメリット
- Claude Codeの対話的な実行が前提のため、LLMのAPIを直接叩く方法と比べるとCIへの組み込みが難しい
終わりに
今回はClaude Codeを使ったLLM-as-a-Judgeを試してみました。
ちょっとした検証をしたいけどプログラムを作るモチベーションがない、または極力評価のコストを削りたい場合に有効かもしれません。