Notebook
Promptfooで同一provider idを使った評価結果の衝突バグを修正した
Promptfooで同一provider idを利用した評価をした際に、結果が衝突し適切に表示されないバグがあったため修正しました。
Guide
目次
Promptfoo を使っていて見つけたバグを修正したので、その内容をまとめます。
PRはこちらです。
promptfoo PR #10208
背景
以下のように、json で出力する場合と通常のテキストで出力する場合でどちらが品質がよくなりそうかを評価していました。
prompts:
- |
次の季節におすすめのB'zの曲を100文字以内で教えてください。
季節: {{season}}
providers:
- id: openai:chat:gpt-5-mini
config:
max_tokens: 256
- id: openai:chat:gpt-5-mini
config:
max_tokens: 256
response_format:
type: json_schema
json_schema:
name: custom_name
strict: true
schema:
type: object
additionalProperties: false
required:
- message
properties:
message:
type: string
maxLength: 100
description: message
tests:
- vars:
season: 夏
assert:
- type: latency
threshold: 5000
- vars:
season: 冬
assert:
- type: latency
threshold: 5000結果を確認すると、以下のように本来はテキストで出力するパターンとjsonで出力するパターンが出てほしいのに、片方の結果しか出ないという問題がありました。

今回のケースのように LLM 評価で、同じモデルを設定違いで複数回並べて比較することがあります。たとえば temperature などを変えて比較するケースもありえると思います。promptfooのドキュメントにもこれは記載があります。
Promptfoo evaluate-llm-temperature
ただしこれには回避する方法があり、 label を指定することで適切に表示されました。
providers:
- id: openai:chat:gpt-5-mini
label: "テキストで出力"
config:
max_tokens: 128
- id: openai:chat:gpt-5-mini
label: "jsonで出力"
config:
max_tokens: 128
response_format:
# response_format の詳細は省略
しかしこれに気付くまで時間がかかってしまったので、同一provider idを指定している場合は ワーニングを出すか、表示出来るようにする必要があると感じました。
そこでこれを直したのが今回のPRです。
起きていた問題
Promptfoo では、評価結果をテーブル形式で保持するために、各 provider/prompt の組み合わせに prompt index を割り当てています。
例えば、同じ provider id を返す provider を2つ指定し、prompt が1つだけあるケースを考えます。
| providerの出現順 | provider id | prompt | 本来の列index |
|---|---|---|---|
| 1つ目 | duplicate-provider | Test prompt | 0 |
| 2つ目 | duplicate-provider | Test prompt | 1 |
本来は、同じ provider id と同じ prompt であっても、provider が2つあるので評価結果の列は2つ必要です。
しかし以前の実装では、評価結果をどの prompt/provider 列に入れるかを探すために、次のような key で index を管理していました。
`${provider}:${promptId}` -> indexこのケースでは、2つの provider が同じ provider id を返し、prompt も同じなので、Map key が完全に同じになります。
| providerの出現順 | Map key | 保存されるindex |
|---|---|---|
| 1つ目 | duplicate-provider:<promptId> | 0 |
| 2つ目 | duplicate-provider:<promptId> | 1 |
JavaScript の Map は同じ key を複数持てないため、2つ目の値が1つ目を上書きします。
結果として、Map の中身は次のようになります。
| Map key | index |
|---|---|
duplicate-provider:<promptId> | 1 |
該当箇所は evaluator.ts#L2044 になります。
本来必要だった index = 0 の情報が消えてしまい、1つ目の provider の評価結果も2つ目の列に紐づいてしまう問題がありました。
修正内容
修正後は、同じ key に対して単一の index ではなく、index の配列を持つようにしました。commit 4043eca
`${provider}:${promptId}` -> [index1, index2, ...]先ほどの例では、Map の中身は次のようになります。
| Map key | indices |
|---|---|
duplicate-provider:<promptId> | [0, 1] |
これで、同じ provider id と同じ prompt の組み合わせが複数あっても、対応する列 index をすべて保持できます。
ただし、配列にしただけでは「今実行している provider は [0, 1] のどちらを使うべきか」がまだ分かりません。
そこで evaluation 実行時に、同じ provider key が何回目に出てきたかを数えるようにしました。
| providerの出現順 | provider id | occurrence index | 使用するprompt index |
|---|---|---|---|
| 1つ目 | duplicate-provider | 0 | [0, 1][0] = 0 |
| 2つ目 | duplicate-provider | 1 | [0, 1][1] = 1 |
実装としては、provider ごとの出現回数を Map で管理し、その occurrence index に対応する prompt index を使います。
const promptIndices = promptIndexMap.get(`${providerKey}:${promptId}`);
const promptIdx = promptIndices?.[providerOccurrenceIndex];これにより、同じ provider id と同じ prompt の組み合わせでも、それぞれの評価結果を別々の列に保存できるようになりました。
メンテナの修正
上記でPRを出したのですが、promptfooメンテナのMichaelさんに追加で修正していただきました。
最初の修正では、providerKey:promptId という key は残したまま、値を単一の index から index の配列に変えていました。
`${provider}:${promptId}` -> [index1, index2, ...]一方で修正いただいたのは、評価結果の列を providerKey:promptId から検索すること自体をやめています。
代わりに、prompt column を作るタイミングで「この provider が持つ列」をそのまま保持するようになりました。
概念的には次のような構造です。
| providerの出現順 | provider | columns |
|---|---|---|
| 1つ目 | firstProvider | [{ promptIdx: 0, prompt: Test prompt }] |
| 2つ目 | secondProvider | [{ promptIdx: 1, prompt: Test prompt }] |
つまり、列をあとから key で探すのではなく、列を作った時点で provider ごとに promptIdx を持たせています。
type ProviderColumns = {
provider: ApiProvider;
columns: { promptIdx: number; prompt: Prompt }[];
};この形にすると、実行時には columnsByProvider を順番に処理すればよくなります。
for (const { provider, columns } of columnsByProvider) {
for (const { promptIdx, prompt } of columns) {
// この provider/prompt の結果は promptIdx の列に保存する
}
}この修正のポイントは、provider id や prompt id のような「表示・識別用の文字列」ではなく、実際に作成した列そのものを実行計画に渡していることです。
そのため、次のようなケースにも強くなっています。
| ケース | なぜ問題になりうるか | メンテナ修正での扱い |
|---|---|---|
同じ provider id の provider が複数ある | providerKey が衝突する | provider ごとに別の columns を持つ |
異なる provider が同じ label を持つ | label を provider key として使うと衝突する | provider オブジェクト単位で列を保持する |
| 異なる prompt が同じ prompt id になる | prompt label 由来の id が衝突する | prompt ごとの列を作成時点で保持する |
| test 側で provider/prompt を filter する | filter 後に列 index がずれる可能性がある | 作成済みの promptIdx をそのまま使う |
また、resume 時の metrics 共有にも手が入っています。
既存の評価結果から metrics を復元する際、重複 provider が同じ stored prompt に解決されることがあります。その metrics object をそのまま複数列で共有すると、片方の結果更新がもう片方にも影響してしまいます。
そこで、既存 metrics を使う場合は structuredClone で複製し、各 column が独立した metrics を持つようにしています。
metrics: existingPrompt?.metrics
? structuredClone(existingPrompt.metrics)
: createDefaultPromptMetrics()最初の修正は「同じ key に複数 index を持たせる」ことで衝突を避けるものでした。Michaelさんの修正はさらに一歩進んで、「そもそも重複しうる文字列 key から列を復元しない」設計に変えています。
まとめ
以下のように正しく表示されるようになりました。

こちらの記事の趣旨とは異なりますが、あわせてこの修正中にドキュメントの誤りも見つけたので修正しています。 promptfoo PR #10209
Promptfoo はプロンプトの評価でとても便利なので最近はかなり使っています。
そのため今回のような小さい修正ではありますが、少しでも貢献出来たので嬉しく思います。