CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/API & SDK
API & SDK/2026-05-01上級

Claude API のプロンプトをリグレッションから守る — Golden Dataset で品質を継続検証する仕組み

モデル更新やプロンプト微修正で精度が静かに落ちる現象を、Golden Dataset と LLM-as-a-Judge で CI に組み込み継続検証する実装ガイドです。

claude-api81prompt-engineering7evaluation2regression-testing2production87

ある金曜の夜、Sonnet 4.5 から 4.6 に切り替えた直後、私のサポートチャットボットの「ご注文番号を教えてください」という確認文が、なぜか 3 割の会話で「お問い合わせ番号」に変わっていました。プロンプトは 1 文字も変えていません。バグレポートが来てから気づくまで丸 2 日。これが「プロンプトが静かに壊れる」典型的なパターンです。

ユニットテストでは検知できませんでした。出力のフォーマットは正しく、JSON もパースできていました。ただ、ビジネス上重要な「注文番号」という単語だけがすり替わっていたのです。

ここではこうした静かな品質劣化を CI で捕まえるための Golden Dataset 方式の評価パイプラインを、コード付きで丸ごと組み立てます。私が 4 つのサイトで運用している実装をベースに、月 50 ドル以下に収めるコスト設計まで含めて共有します。

なぜ「ユニットテスト」では LLM の品質を守れないのか

従来のユニットテストは「入力 X に対し出力 Y が返ること」を保証する仕組みです。LLM はそもそも非決定的に動くので、この前提が成り立ちません。

私が実際に経験した「静かに壊れる」3 つの瞬間を挙げます。

第一に、モデルバージョンを上げたとき。最新モデルは平均性能こそ向上しますが、特定タスクでは前のモデルに劣ることがあります。例えば「短く答える」指示への忠実さが微妙に下がる、といった具合です。

第二に、プロンプトの 1 文を「分かりやすく」書き換えたとき。「丁寧に答えてください」を「親切に答えてください」に変えただけで、敬語のレベルが変わることがあります。

第三に、Tool Use や RAG の周辺コードを変えたとき。プロンプトは触っていなくても、検索結果の順序やトリミング方法が変わると、最終出力の質がじわっと落ちます。

これらに共通するのは「テストは緑のまま、ユーザー体験だけが落ちる」という点です。Golden Dataset 方式は、これを定量的に検知するための仕組みです。

Golden Dataset の最小構成 — 50 件で始める

「Golden Dataset」と聞くと数千件規模を想像するかもしれませんが、実用上は 30〜100 件で十分です。私は新規プロジェクトなら 50 件から始めます。

ポイントは、プロンプトが 本番で受け取る入力の分布を縮約することです。サポートチャットなら「商品到着の遅延」「返金依頼」「使い方質問」など、過去 1 ヶ月のログから頻出する 10 カテゴリを選び、各カテゴリ 5 件をサンプリングします。

データ構造は JSONL がおすすめです。Git 管理しやすく、追記も容易です。

{"id": "support_001", "category": "shipping_delay", "input": "注文した商品がまだ届きません。", "expected_intent": "shipping_inquiry", "expected_keywords": ["注文番号", "配送状況", "確認"], "must_not_include": ["返金", "キャンセル"]}
{"id": "support_002", "category": "refund", "input": "サイズが合わないので返品したいです。", "expected_intent": "refund_request", "expected_keywords": ["返品ポリシー", "返金", "手続き"], "must_not_include": []}
{"id": "support_003", "category": "usage", "input": "アプリにログインできません。", "expected_intent": "technical_support", "expected_keywords": ["パスワード", "リセット", "アカウント"], "must_not_include": ["返金"]}

expected_keywordsmust_not_include を併用するのがコツです。前者は「これらの単語のうち N 個以上含むこと」、後者は「これらは絶対に含めないこと」を表します。完全一致ではなくキーワードベースにすることで、表現の揺れを許容しつつ、意味の劣化は捕まえられます。

ルールベース評価 — まずは決定的なチェックから

Golden Dataset を使った評価は、ルールベース → LLM-as-a-Judge の二段構えにします。コストの安いルールベースで弾けるものは、LLM 評価を回さない設計です。

# evaluator/rule_based.py
import json
from typing import TypedDict
 
class GoldenItem(TypedDict):
    id: str
    category: str
    input: str
    expected_keywords: list[str]
    must_not_include: list[str]
 
class RuleResult(TypedDict):
    id: str
    keyword_score: float  # 0.0〜1.0
    blocklist_passed: bool
    overall_passed: bool
    failure_reasons: list[str]
 
def evaluate_rule(item: GoldenItem, output: str, min_keyword_ratio: float = 0.5) -> RuleResult:
    """ルールベース評価: 期待キーワード比率 + ブロックリスト判定。"""
    output_lower = output.lower()
 
    # ① 期待キーワードのうち何割含まれているか
    matched = [kw for kw in item["expected_keywords"] if kw.lower() in output_lower]
    keyword_score = len(matched) / max(len(item["expected_keywords"]), 1)
 
    # ② ブロックリストは1つでも含めばアウト
    blocklist_violations = [kw for kw in item["must_not_include"] if kw.lower() in output_lower]
    blocklist_passed = len(blocklist_violations) == 0
 
    failure_reasons = []
    if keyword_score < min_keyword_ratio:
        missing = set(item["expected_keywords"]) - set(matched)
        failure_reasons.append(f"keyword_ratio {keyword_score:.2f} < {min_keyword_ratio} (missing: {missing})")
    if not blocklist_passed:
        failure_reasons.append(f"blocklist_violation: {blocklist_violations}")
 
    return {
        "id": item["id"],
        "keyword_score": keyword_score,
        "blocklist_passed": blocklist_passed,
        "overall_passed": keyword_score >= min_keyword_ratio and blocklist_passed,
        "failure_reasons": failure_reasons,
    }
 
# 期待出力例:
# {'id': 'support_001', 'keyword_score': 1.0, 'blocklist_passed': True, 'overall_passed': True, 'failure_reasons': []}

このコードのポイントは min_keyword_ratio=0.5 のしきい値です。100% 一致を求めるとプロンプトの自然な揺れまで失敗扱いになります。私は経験上 0.5〜0.6 が実用的だと感じています。

LLM-as-a-Judge — 揺れる出力を安定して評価する

ルールベースで弾けないのが「丁寧さ」「事実性」「文脈の維持」のような質的評価です。ここで Claude を Judge として使います。

ただし、LLM-as-a-Judge には重大な落とし穴があります。Judge モデル自身もバージョンが変わると評価が揺れるのです。私は最初これを軽視して、Judge を Sonnet 4.6 から Sonnet 4.6 に「アップデート」したら(同じバージョンに見えるが minor revision が違っていた)、評価スコアが 0.78 → 0.71 に落ちたことがあります。

対策は 2 つです。(1) Judge モデルとバージョンを評価ログに必ず記録します。(2) Judge と人手評価の合意度を Cohen's Kappa で定期測定する

# evaluator/llm_judge.py
import os
import anthropic
from typing import TypedDict
 
JUDGE_MODEL = "claude-sonnet-4-6"  # 評価ログに必ず記録すること
 
class JudgeResult(TypedDict):
    id: str
    politeness_score: int  # 1〜5
    factual_score: int     # 1〜5
    relevance_score: int   # 1〜5
    judge_reasoning: str
    judge_model: str
 
JUDGE_SYSTEM = """あなたはカスタマーサポート応答の評価者です。
3 つの軸で 1〜5 点で評価し、JSON のみを返してください。
 
- politeness: 1=失礼 / 5=非常に丁寧
- factual: 1=事実誤認あり / 5=事実誤認なし
- relevance: 1=的外れ / 5=ユーザーの意図を完全に捉えている
 
評価は厳格に。3 点を「平均的」の基準点とし、迷ったら下方に振ってください。"""
 
JUDGE_USER_TEMPLATE = """ユーザー入力:
{user_input}
 
評価対象の応答:
{model_output}
 
JSON 形式で {{"politeness": N, "factual": N, "relevance": N, "reasoning": "..."}} のみを返してください。"""
 
def judge(item_id: str, user_input: str, model_output: str) -> JudgeResult:
    client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
    try:
        msg = client.messages.create(
            model=JUDGE_MODEL,
            max_tokens=400,
            system=JUDGE_SYSTEM,
            messages=[{
                "role": "user",
                "content": JUDGE_USER_TEMPLATE.format(
                    user_input=user_input, model_output=model_output
                )
            }],
        )
        text = msg.content[0].text.strip()
        # ```json ... ``` ブロックの除去
        if text.startswith("```"):
            text = text.split("```")[1].lstrip("json").strip()
        import json
        parsed = json.loads(text)
        return {
            "id": item_id,
            "politeness_score": int(parsed["politeness"]),
            "factual_score": int(parsed["factual"]),
            "relevance_score": int(parsed["relevance"]),
            "judge_reasoning": parsed.get("reasoning", ""),
            "judge_model": JUDGE_MODEL,
        }
    except (anthropic.APIError, json.JSONDecodeError, KeyError) as e:
        # Judge 自体が失敗したら N/A 扱い(テスト全体は失敗にしない)
        return {
            "id": item_id, "politeness_score": -1, "factual_score": -1,
            "relevance_score": -1, "judge_reasoning": f"judge_error: {e}",
            "judge_model": JUDGE_MODEL,
        }
 
# 期待出力例:
# {'id': 'support_001', 'politeness_score': 5, 'factual_score': 5, 'relevance_score': 4, 'judge_reasoning': '丁寧で事実誤認なし。注文番号確認の手順も明確。', 'judge_model': 'claude-sonnet-4-6'}

なぜ judge_error を「テスト全体の失敗」にしないのか。Judge の API 失敗は評価対象モデルの品質と無関係です。これを混同すると、Anthropic 側の一時的な障害でビルドが落ちます。-1 をマーカーにして、サマリで「N/A 件数」として別カウントするのが正解です。

Cohen's Kappa で Judge の信頼度を担保する

LLM-as-a-Judge の評価が「人間の評価とどれくらい一致するか」を定量化する指標が Cohen's Kappa です。月に 1 回、Golden Dataset の 20 件をランダムサンプリングして、Judge と人間(自分)の評価を比較します。

# evaluator/kappa.py
from sklearn.metrics import cohen_kappa_score
from typing import Sequence
 
def compute_kappa(judge_scores: Sequence[int], human_scores: Sequence[int]) -> dict:
    """politeness などの順序尺度に対する一致度を測る。"""
    # 1〜5 の順序尺度なので weights="quadratic" を使う
    kappa = cohen_kappa_score(human_scores, judge_scores, weights="quadratic")
    diffs = [abs(j - h) for j, h in zip(judge_scores, human_scores)]
    return {
        "kappa": kappa,
        "mean_abs_diff": sum(diffs) / len(diffs),
        "exact_agreement_rate": sum(1 for d in diffs if d == 0) / len(diffs),
        "n_samples": len(diffs),
    }
 
# 期待出力例:
# {'kappa': 0.72, 'mean_abs_diff': 0.4, 'exact_agreement_rate': 0.65, 'n_samples': 20}
# kappa >= 0.6 を「実用ライン」、>= 0.8 を「強い一致」と判断する

判断基準: kappa が 0.6 を切ったら Judge プロンプトを見直すサインです。私の経験では、評価軸を曖昧にしている(「全体的な品質」のような単一スコアにしている)と kappa が下がります。politeness factual relevance のように分解すると安定します。

評価コストを月 50 ドル以下に抑える設計

「Golden Dataset を CI で毎回回したらコストが大変では」という心配が必ず出ます。私の実装ではこう抑えています。

第一に、評価をフルに回すのは main へのマージ時のみにしています。PR ビルドではルールベースのみ走らせ、LLM-as-a-Judge は main マージ時にバッチで実行します。

第二に、Judge には Haiku 4.5 を使うことも検討してください。私は精度比較を実施した上で、サポートチャット評価では Sonnet 4.6 と Haiku 4.5 の Kappa 差が 0.05 以内だったので Haiku に切り替えました。コストは約 1/5 です。

第三に、プロンプトキャッシュを必ず使うことです。Judge の system プロンプトは固定なので、キャッシュ効率は 90% 超になります。

具体的な月額試算は以下の通りです。

  • Golden Dataset 50 件 × 評価軸 3 = 150 評価コール / 1 ラン
  • main マージ 1 日 5 回 × 30 日 = 150 ラン
  • 合計 22,500 評価コール / 月
  • Haiku 4.5 (input $0.80 / output $4.00 per 1M, キャッシュ後実効 input $0.10) × 1 評価あたり ~600 input + 80 output トークン
  • 月額: ($0.10 × 0.6 + $4.00 × 0.08) / 1000 × 22500 ≒ 約 8 ドル

これに、月 1 回の Cohen's Kappa 用 20 件評価 × Sonnet 4.6 を加えても、月 15 ドル以内に収まります。

CI への組み込み — GitHub Actions の最小構成

# .github/workflows/prompt-eval.yml
name: Prompt Regression Eval
 
on:
  push:
    branches: [main]
  pull_request:
    paths:
      - 'prompts/**'
      - 'evaluator/**'
 
jobs:
  eval:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'
      - run: pip install -r evaluator/requirements.txt
 
      # PR は安価なルールベースのみ(高速・無料)
      - name: Rule-based eval (PR)
        if: github.event_name == 'pull_request'
        run: python evaluator/run.py --mode rule_only --golden golden_dataset.jsonl
 
      # main マージ時はフル評価(LLM-as-a-Judge 含む)
      - name: Full eval (main)
        if: github.event_name == 'push'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: python evaluator/run.py --mode full --golden golden_dataset.jsonl --output eval_results.json
 
      - name: Upload results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: eval-results
          path: eval_results.json
 
      # 失敗時に Slack 通知(任意)
      - name: Notify on regression
        if: failure() && github.event_name == 'push'
        run: |
          curl -X POST -H 'Content-type: application/json' \
            --data "{\"text\":\"⚠️ Prompt eval regression detected on main. See: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
            ${{ secrets.SLACK_WEBHOOK_URL }}

timeout-minutes: 15 を必ず設定してください。Judge API のハングや無限リトライで CI が止まると、開発フローを大きく妨げます。

よくある間違い・落とし穴 5 つ

実運用に持ち込んで初めて気づく落とし穴を、私が実際にハマった順に紹介します。

(1) Golden Dataset を 1 度作って終わりにしてしまう。本番の入力分布は刻々と変わります。月 1 回、本番ログから 5〜10 件を追加し、半年以上前の古いケースは見直すサイクルを入れてください。

(2) しきい値を「初期スコア = 合格ライン」にしてしまう。最初の評価で総合スコア 0.85 が出たから「0.85 を下回ったら失敗」とするのは危険です。Judge の揺れだけで 0.83 になることがあり、全レポを赤くします。初期スコア × 0.95 を初期しきい値にし、3 ヶ月運用して経験的に調整するのが安全です。

(3) Judge プロンプトに「1〜5 点」と書いて 4.7 が返ってくる。Claude は柔軟に解釈するので、整数指定でも小数を返すことがあります。int(parsed["politeness"]) で明示変換するか、Tool Use で構造化出力を強制してください。

(4) 失敗時のレポートが「平均 0.78」だけ。これでは何が悪化したか分かりません。個別ケース ID 単位で前回スコアと diff を出す仕組みを必ず入れてください。eval_results.json を S3 に履歴保存しておけば、previous_run.json と当日結果の差分を出せます。

(5) Judge の評価ログを保存しない。後から「なぜ 3 点だったのか」を遡れないと、Judge プロンプトの改善ができません。judge_reasoning フィールドは必ず保存してください。月数百 KB です。

月次の評価レビュー会 — 仕組みを腐らせない運用

最後に、最も重要なのは「仕組みを作って終わりにしない」ことです。私は月初に 30 分、自分一人でも以下のレビューを必ずします。

評価結果のトレンド(ダッシュボードかスプレッドシート)を見ながら、(a) スコアが下がっているケースの原因を仮説立て、(b) Cohen's Kappa が 0.6 を切っていないか、(c) Golden Dataset に追加すべき新しい本番ケースはないか、(d) Judge プロンプトを直すべきか、をチェックします。

このレビューを欠かすと、Golden Dataset は 3 ヶ月で陳腐化します。私の運用ログでは、レビュー月とそうでない月で「実本番でのユーザー指摘件数」が約 2 倍違っていました。仕組みは育てるものです。

評価設計をさらに体系的に

次の一歩

まず Golden Dataset を 30 件作るところから始めてください。完璧なデータセットを目指すと永遠に始まりません。本番ログから「ユーザーがリトライしている会話」と「サポートが介入した会話」を 15 件ずつ拾えば、それだけで実用的な出発点になります。明日の朝 30 分でできる作業です。

そしてルールベース評価だけでも CI に入れてみてください。LLM-as-a-Judge は、ルールベースが回り始めてからで十分です。「いきなり全部入り」を狙うと挫折します。

関連記事として Claude API の LLM 評価パイプライン構築ガイドClaude API のテスト戦略 — Unit / Integration / E2E で AI 品質を守る、さらに Claude API プロンプトレジストリと A/B テストの本番運用ガイド も合わせて読むと、評価・バージョニング・テストの三位一体が見えてきます。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API & SDK2026-03-27
Claude API LLM評価パイプライン構築ガイド — Claude-as-Judge・プロンプトA/Bテスト・品質スコアリングの実装パターン
Claude API を使った LLM アプリケーション評価パイプラインの設計と実装。Claude-as-Judge、プロンプト A/B テスト、品質スコアリング、回帰テストの本番実装パターンを扱います。
API & SDK2026-07-13
Claude API の同時実行を束ねる — シングルフライトで重複推論とキャッシュ・スタンピードを防ぐ
同一プロンプトが同時に何度も Claude へ飛ぶ重複推論を、シングルフライト(request coalescing)で束ねる設計です。プロセス内・分散環境の実装、ジッター付きリトライ、負のキャッシュまで実測付きでまとめました。
API & SDK2026-07-09
出力は同じ、道筋は違う — エージェントの軌跡を不変条件で守る
既定モデルが入れ替わっても最終出力は正しいまま、ツール呼び出しの道筋だけが静かに変わることがあります。実行トレースを記録し、不変条件で機械的にアサートする軌跡回帰ハーネスを実装コードと実測値で整理しました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →