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 | 向く合格条件 | 弱点 |
|---|---|---|---|
| 1 | assertequals / contains / not-contains | 向く合格条件ラベル、固定の事実(日数・金額・型番) | 弱点言い回しの変化で落ちる |
| 2 | assertregex / is-json | 向く合格条件形式(日付・番号・JSONの構造) | 弱点意味は見られない |
| 3 | assertllm-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.8defaultTest.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.jsonCI向けの手順に出てくる前提は、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.jsonprompts:
- file://prompts/baseline.json
- file://prompts/support.jsonpromptsに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プラグインの評価ループが比較対象になります。バグ修正のたびにテストを足す習慣は、再現テストを先に書かせる手順と同じ考え方です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。