Claude Media
promptfooでClaudeの回帰テストを組み、CIで合否を出す手順

promptfooでClaudeの回帰テストを組み、CIで合否を出す手順

システムプロンプトを直したら別の回答が崩れた、を防ぐ回帰テストの組み方です。promptfooのAnthropicプロバイダ設定、assertの3段構成、終了コード100でCIを落とす流れを示します。

プロンプトの回帰テストは、promptfooの設定ファイルに「入力と合格条件」を並べ、evalの終了コードでCIの合否を決める形で組めます。Claudeを対象にするときは、プロバイダIDにモデルIDを固定して書き、合格条件は安い順に3段(完全一致・正規表現・LLM採点)で積みます。

ただし、temperature: 0でブレを止める古典的な手は、新しいモデルでは使えません。ここを外すと、CIが赤くなったときに「プロンプトのせいか、揺らぎか」が読めなくなります。この記事は、その分かれ目と、差分の読み方まで扱います。

最小構成: プロンプト・プロバイダ・テストの3点を置く

必要なのは、プロンプトファイル、promptfooconfig.yaml、環境変数ANTHROPIC_API_KEYの3つです。プロンプトはChat形式のJSONで書くと、systemとuserを分けて渡せます。system_messageとquestionがテストのvarsから差し込まれます。

[
  { "role": "system", "content": "{{ system_message }}" },
  { "role": "user", "content": "{{ question }}" }
]

これをprompts/support.jsonとして置き、設定ファイルから読みます。例は、返金に関する問い合わせへの返信文を作るプロンプトです。

prompts:
  - file://prompts/support.json
 
providers:
  - id: anthropic:messages:claude-sonnet-5-5
    config:
      effort: medium
      max_tokens: 4096
      thinking:
        type: between_tools
 
defaultTest:
  vars:
    system_message: >-
      あなたは自社ECのサポート担当です。返金は購入から7日以内のみ受け付けます。
      返信は200字以内で、謝罪から始めないでください。
 
tests:
  - description: 返金期限を正しく案内する
    vars:
      question: 先週買った商品を返金したいのですが、まだ間に合いますか。
    assert:
      - type: contains
        value: 7日以内
      - type: not-contains
        value: 申し訳

プロバイダの書式はanthropic:messages:<モデルID>です。ANTHROPIC_API_KEYは環境変数で渡すほか、プロバイダ設定のapiKeyでも指定できます。thinkingのbetween_toolsは、Sonnet 5.5で最も低い思考設定に当たる指定です。effortとmax_tokensも、デフォルト任せにせず書いておきます。

max_tokensを書く理由は、思考がある世代では出力の上限に思考分が含まれるためです。promptfooの既定値は1024で、モデルが既定で思考するときは2048になります。回答が途中で切れるとcontainsが落ちるので、回帰テストの失敗が「プロンプトの劣化」に見えてしまいます。

temperature: 0は効かない — 何で固定するか

古い評価基盤の定石は「temperatureを0にして出力を揃える」でした。promptfooのAnthropicプロバイダでも、temperatureの既定値は0です。しかしSonnet 5.5では、promptfooがtemperature・top_p・top_kをリクエストから外します。YAMLに書いても送信されません。

Sonnet 5.5とOpus 5は、これらのサンプリング設定をAPI側が受け付けない世代です。Opus 5はpromptfooの既定のtemperature: 0も含めて送信を省きます。Opus 4.7と4.8も同じで、設定すると警告が一度出ます。素のAPIでこれを送ると400になる点は、temperatureが400になる破壊的変更の記事で扱っています。

つまり、新しいモデルの回帰テストでは「出力のランダムさを抑える」方向では固定できません。固定できるのは次の4点です。

固定項目

回帰テストで固定する4点

  • モデルID

    日付なしの固定IDを書きます。Anthropicは-latestのようなエイリアスを出していません。claude-sonnet-4-5-latestはnot_found_errorになります。

  • effortと思考

    effortの既定値はモデルで違い、Sonnet 5.5はhighです。YAMLに明記しておくと、既定値が変わっても差分に混ざりません。

  • max_tokens

    思考を含む上限なので、短すぎると回答が欠けます。十分に大きく、固定値で書きます。

  • プロンプトとvars

    テスト入力はvarsに置き、プロンプト本体はファイルで管理します。差分はgitで読めます。

ブレそのものは残ります。だから、同じテストを複数回回して「何回中何回通ったか」を見る方向に切り替えます。この読み方は後半で扱います。

assertは安い順に3段で積む

Anthropicの評価ガイドは、採点方法を選ぶ順序を示しています。コードによる採点が最も速く確実で、LLM採点は柔軟だが先に信頼性を確かめるべきもの、という整理です。promptfooのassertに当てはめると、次の3段になります。

段assert向く合格条件弱点
1assertequals / contains / not-contains向く合格条件ラベル、固定の事実(日数・金額・型番)弱点言い回しの変化で落ちる
2assertregex / is-json向く合格条件形式(日付・番号・JSONの構造)弱点意味は見られない
3assertllm-rubric向く合格条件トーン、方針の遵守、禁止事項弱点採点自体が揺れる、費用がかかる

equalsは出力が完全に一致したときだけ通ります。分類ラベルだけを返すプロンプトに向きます。返信文のように長い出力では、containsで必須の事実を、not-containsで禁止語を見ます。どのassertも、頭にnot-を付けると否定形になります。

形式の検査はregexです。たとえば注文番号を含めるルールなら、次のように書けます。

assert:
  - type: regex
    value: "ORD-\\d{6}"
  - type: is-json
    value:
      type: object
      required: [category, reply]

is-jsonは、valueにJSON Schemaを渡すと構造まで検証します。出力をJSONに固定するプロンプトでは、これが回帰の最初の砦です。

3段目のllm-rubricは、文面の方針を判定させる用途に絞ります。「謝罪から始めていないか」「保証できない約束をしていないか」といった、文字列で書けない条件です。ここを最初から全部LLM採点にすると、費用が増える上に、赤くなった理由が採点のぶれか本物の劣化か分からなくなります。段を分ければ、1・2段の失敗はそのままプロンプトの劣化として読めます。

LLM採点の判定役は、対象モデルと別に指定する

llm-rubricには判定役のモデルが要ります。何も指定しないときの選ばれ方に、CIで踏みやすい罠があります。ANTHROPIC_API_KEYだけが設定され、OPENAI_API_KEYが無いときは、判定にAnthropicが使われます。両方が設定されていると、OpenAIが既定になります。

CIのランナーにほかの用途でOPENAI_API_KEYが入っていると、Claudeの出力をOpenAIのモデルが採点する構成に静かに変わります。これを避けるには、判定役を明示します。

defaultTest:
  options:
    provider:
      id: anthropic:messages:claude-sonnet-5-5
      config:
        effort: low
 
tests:
  - description: 謝罪から始めない
    vars:
      question: 返金手続きが遅くて不満です。
    assert:
      - type: llm-rubric
        value: >-
          返信の最初の文が謝罪ではなく、状況の説明か次の手順の案内になっている。
          謝罪語で始まっていれば不合格。
        threshold: 0.8

defaultTest.options.providerは、全テストの判定役を決める指定です。assert単位のprovider、テスト単位のoptions.providerでも上書きできます。

注意が要るのはdefaultTest.providerです。これは対象モデルを固定する指定ですが、判定役が未指定のときは判定役としても使われます。対象と判定役を同じモデルにしたくないなら、options.providerを必ず書いてください。

rubricの書き方はAnthropicのガイドに沿います。基準は詳細に、結果は合否か1〜5のような数値で答えさせます。厳密さが要る採点では、判定役に思考を使わせると性能が上がるとされています。

CIで合否を出す: 終了コード100としきい値

promptfoo evalは、テストが1件でも落ちるか、合格率がPROMPTFOO_PASS_RATE_THRESHOLD(パーセント)を下回ると終了コード100を返します。ほかのエラーは終了コード1です。しきい値を設定しなければ、既定は100%、つまり1件の失敗も許しません。

終了コードが非ゼロになれば、GitHub Actionsのジョブはそのまま赤になります。ジョブを組むと次の形です。

name: Prompt regression
on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'promptfooconfig*.yaml'
jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '24'
      - name: 決定的なassert(しきい値100%)
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          npx promptfoo@latest eval \
            -c promptfooconfig.yaml \
            -o results.json -o results.junit.xml
      - name: LLM採点(しきい値90%)
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          PROMPTFOO_PASS_RATE_THRESHOLD: '90'
        run: |
          npx promptfoo@latest eval \
            -c promptfooconfig.rubric.yaml \
            -o rubric.json
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: eval-results
          path: |
            results.json
            results.junit.xml
            rubric.json

CI向けの手順に出てくる前提は、Node.js 22.22.0以上(24 LTS推奨)と、APIキーをシークレットの環境変数で渡すことです。出力形式は-oで複数指定でき、.junit.xmlで終わる名前にするとJUnit形式になります。

設定ファイルを2つに分けているのは、しきい値を段ごとに変えたいからです。全テストを1本にまとめてPROMPTFOO_PASS_RATE_THRESHOLD=90にすると、決定的なassertの失敗が1割までなら通ってしまいます。1・2段は100%、3段だけ緩める分け方なら、確実な検査を甘くせずに済みます。

jqでresults.jsonのresults.stats.successesとfailuresを数え、独自の合格率を計算する書き方もできます。ただ、終了コードに任せたほうが短く済みます。

差分を読む: ベースラインとの並置と--repeat

プロンプトを直したPRで知りたいのは「全体で何件通ったか」ではなく、「前のプロンプトで通っていたのに落ちたテスト」です。ベースラインを同じ設定に並べておくと、比較が一度の実行で済みます。

git show origin/main:prompts/support.json > prompts/baseline.json
prompts:
  - file://prompts/baseline.json
  - file://prompts/support.json

promptsに2つ書くと、同じテストに対する2つのプロンプトの結果が行列表示で並びます。ベースラインが通り、候補だけが落ちるセルが、今回の変更による回帰です。両方赤のテストは、前から落ちていた別件として分けて読めます。

ブレの扱いには--repeatを使います。各テストを指定回数ずつ実行する指定で、--repeat 3なら全テストが3回ずつ回ります。3回中2回通るテストは、プロンプトが原因で劣化したのではなく、元から不安定なテストです。そのテストのassertを緩めるか、入力を見直す材料になります。

実行する前に知っておきたいのがキャッシュです。promptfooは過去のLLMリクエストを既定でキャッシュします。同じ入力の再実行はAPIを呼ばずに結果を返すため、費用と時間は節約できます。一方で、モデル側の挙動変化を拾う定期実行では、キャッシュが古い答えを返すことになります。そのときは--no-cacheを付けます。--repeatでブレを測りたいときも、--no-cacheを併せて付けておくと、毎回モデルを呼ぶ前提で読めます。

キャッシュの保存先はPROMPTFOO_CACHE_PATHで変えられます。CIのキャッシュ機能と組み合わせれば、プロンプトを変えていないテストの再実行を省けます。

つまずきやすい点

落とし穴

モデルIDの末尾にlatestを付けて落ちる

Anthropicには-latestのエイリアスがなく、claude-sonnet-4-5-latestはnot_found_errorになります。短縮形が使えるのはclaude-sonnet-4-5のように、日付付きIDの短い別名がある世代だけです。4.6以降のIDは最初から日付が付かない形なので、省略する余地がありません。

次の3点も、CIが赤くなる前に押さえておくと原因を切り分けやすくなります。

  • 退役モデルのID: Claude 3.5 SonnetやClaude 3 Haikuなどは、Anthropic APIで404のnot_found_errorを返します。古いブログのコピー設定を持ち込むと、テスト以前に落ちます。
  • Mythos系の指定: Mythosは限定提供のモデルで、アクセスのない組織が呼ぶと、退役モデルと同じnot_found_errorになります。IDの誤りに見えるので、原因の取り違えに注意が要ります。
  • 思考の無効化: Sonnet 5.5は思考を完全には無効にできません。promptfooはdisabledをbetween_toolsに置き換えて警告を出します。effortがxhighかmaxだと、この置き換えも行われず、モデルは適応的に思考します。

テストの数は、増やす方向に倒します。Anthropicのガイドは、質は低くても自動採点できる問いを多く持つほうが、少数の手採点より良いとしています。1つの問い合わせ種別につき、正常系、境界(返金期限ちょうど)、嫌がらせや無関係な質問の3種は用意しておくと、プロンプト変更の影響が見えやすくなります。

まとめ

Claudeのプロンプト回帰テストは、temperatureではなくモデルID・effort・max_tokensの固定と、--repeatによる「通過回数」の観察で安定させます。assertは文字列・形式・LLM採点の順に積み、しきい値も段ごとに分けます。終了コード100に任せればCI側の分岐は要りません。

評価の考え方そのものを先に固めたいときは、Anthropic公式コースでのプロンプト評価が全体像をつかむ助けになります。Pythonのテストランナー側で評価ループを回したい場合は、DeepEvalプラグインの評価ループが比較対象になります。バグ修正のたびにテストを足す習慣は、再現テストを先に書かせる手順と同じ考え方です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. promptfoo.dev/docs/providers/anthropic
  2. promptfoo.dev/docs/configuration/expected-outputs
  3. promptfoo.dev/docs/integrations/ci-cd
  4. promptfoo.dev/docs/usage/command-line
  5. platform.claude.com/docs/en/test-and-evaluate/develop-tests
この記事を共有:XはてブLinkedIn