AI の評価(AI evals)に使う
chaff は、model の出力の採点役として、model による採点の横に置けます。同じ出力にはいつも同じ結果を返し、一つ一つの指摘が行とルールを指し、どこにも何も送りません。意味は読みません。答えが正しいかどうかは、これまでどおり model の採点か人が決めます。このページでは、そのやり方を手順と実際の出力で示します。扱うのは順に、出力をまとめた採点、基準の書き方、2 つの回の比べ方、ベンチマークの採点、関数からの呼び出し、評価基盤へのつなぎ方です。
出力の何を確かめられるか
| 確かめたいこと | コマンド | 終了コード 1 になるとき |
|---|---|---|
| 要約や書き換えで事実が落ちたか、作られたか | npx chaffjs compare <元> <出力> --json | 数・日付・URL・名前・引用などが落ちたか、足された |
| RAG の回答の引用が、本当に原文にあるか | npx chaffjs cite <原文> <引用.json> --format json | 引用がその番地に無い |
| 回答が自分と矛盾していないか | npx chaffjs <出力> | 合計が内訳の和と合わない、日付と曜日が合わない |
| 生成文らしい形や、読みにくさ | npx chaffjs <出力> --sarif <path> | error の指摘だけ。ほかは数として数えます |
| 上のすべてを、たくさんの出力に | npx chaffjs grade <items.jsonl> --out <results.jsonl> | 落ちた出力がある(入力が読めなければ 2) |
| 抜き出しや計算の答えが、決まった正解と合うか | npx chaffjs grade <items.jsonl>(行に expect を書く) | 正解の事実が無いか、あってはならない事実がある |
| 作り直しの段で何を直させるか | npx chaffjs fix-plan <出力> --json | なりません。直す指示を出すだけです |
たくさんの出力には、下の chaff grade を使います。ふつうの検査には --json が無いので、指摘は SARIF で読みます(「指摘を SARIF で受け取る」)。
始める前に
Node.js 24 以上が要ります。npx chaffjs は、初めて使うときに chaff を取ってきます。何度も回す評価では、chaffjs をプロジェクトの開発用の依存に入れておきます。どの回も同じ版で動き、同じ出力には同じ結果が返るためです。実行する場所に chaff.yaml があれば、どの検査にも効きます(設定)。
chaff grade で初めての評価を回す
-
出力を JSONL に並べます。 1 行に 1 つの出力です。
idとoutputは必須です。出力の元になった文書と事実を照らすときはreferenceを足します。引用を確かめるときは、sources(原文の名前と本文)とcitationsを足します。原文が一つなら、引用のsourceは省けます。{ "id": "refund", "output": "商品が届いた日から 30 日以内なら返金を求められ、送料も戻ります(第2条第2項)。", "sources": { "policy": "# 返金の決まり\n\n第1条(対象)\n…" }, "citations": [ { "address": "2.1", "quote": "商品が届いた日から 30 日以内" }, { "address": "2.2", "quote": "送料も全額を返金する。" } ] }回答のどの文が引用なのかを、chaff は推測しません。引用は、回答と一緒に返すよう model に頼みます。
languageとgenreは出力ごとに書けます。書かなければ、ふつうのファイルと同じに決まります。 -
ファイルを採点します。
prompt-a.jsonlとprompt-b.jsonlは、同じ 3 つの題に 2 つの prompt で答えさせた出力です。中身はAI の評価の例の例 1〜3 にあります。--outは出力ごとの結果を書き出し、画面には要約が出ます。$ npx chaffjs grade prompt-b.jsonl --out b.results.jsonl 出力ごとの結果を書きました: b.results.jsonl(3 行) prompt-b.jsonl: 3 件の出力、1 件が通り、2 件が落ちた 落ちた出力 ✗ q3: facts.dropped 3 > 0, facts.added 1 > 0 ✗ refund: citations.failed 1 > 0 ルールごとの率(1,000 字あたり、指摘のあった出力の数) ai-generated-composite 3.0 1 件 ai-tell 3.0 1 件 closing-cliche 6.0 1 件 padded-intro 3.0 1 件 事実: 落ちた 3(date 1, number 2)、足された 1(date 1) 引用: 2 件を照らし、1 件が外れた AI らしさ: 低 0、中 0、高 0、測っていない 3(書いたのが AI かどうかの判定ではありません。合否には入りません) … cite 2 件の出力 citations が無い(chaff は出力から引用を推測しない) … compare 2 件の出力 reference が無い(事実は reference と照らす) … -
結果を読みます。 次のどれかがあれば、その出力は落ちます。
errorの指摘referenceに対して落ちた事実、足された事実- 原文に見つからない引用
形の指摘で落ちることはありません。1,000 字あたり(英語なら 1,000 語あたり)の率として、prompt や model を比べるのに使います。「動かなかったもの」は、何を確かめていないかとその理由です。指摘 0 件を「確かめて問題なし」と読ませないためです。
--compactは 1 出力 1 行で、CI のログ向けです。--jsonは要約を JSON で出します。$ npx chaffjs grade prompt-b.jsonl --compact q3 fail facts.dropped 3 > 0, facts.added 1 > 0 refund fail citations.failed 1 > 0 deploy pass prompt-b.jsonl: 3 件の出力、1 件が通り、2 件が落ちた -
終了コードを使います。 すべて通れば 0、落ちた出力があれば 1 です。入力か
chaff.yamlが読めなければ 2 です。JSON でない行、idの欠けや重なり、知らない言語などがこれに当たります。 CI の門は、2 を見れば「出力が悪い」のではなく「採点が動いていない」と分かります。
b.results.jsonl の 1 行には、1 つの出力の指摘、率、動かなかったルール、事実、引用が入ります。合否とその理由、再現の印も入ります。1 行目の始めはこうです。
{"id":"q3","language":"ja","genre":"blog/tech","size":{"unit":"char","value":73},"findings":[],"rates":{},"notRun":[…],
"facts":{"dropped":[{"kind":"number","key":"6 時間","text":"6 時間","line":4,"allowed":false},…],"added":[…],"reformed":1},
"citations":null,"aiScore":{"level":null,"notScored":"too-short","signs":0,"compared":22,"shown":[]},
"pass":false,"failedBecause":["facts.dropped 3 > 0","facts.added 1 > 0"],"stamp":{…}}
aiScore は AI らしさの簡易判定です(AIっぽさを直す)。
level は low・medium・high のどれかで、測らなかったときは null にし、notScored に理由(too-short か no-baseline)を入れます。
signs は数えた目印の数、compared は人の文書と比べた項目の数、shown は数えた目印です。この例の回答は短いので測っていません。書いたのが AI かどうかの判定ではなく、合否にも入りません。要約には段階ごとの出力の数が、variant の表には「AI らしさ 低/中/高」の行が出ます。prompt や model で生成文の形がどれだけ増えたかを比べるのに使います。
再現の印(stamp)には、chaff の版と、ルールのハッシュと、設定のハッシュが入ります。ハッシュが 2 つとも同じ回どうしなら比べられます。
採点の基準を chaff.yaml に書く
chaff.yaml に grade: を書くと、書いたものだけで合否を決め、出力ごとに減点を数えます。
grade:
rules:
total-mismatch: { max: 0 } # 1 件でもあれば落とす
closing-cliche: { max: 0, weight: 3 } # 落とし、1 件 3 点
ai-tell: { max_rate: 5, weight: 2 } # 1,000 字(英語は語)あたり 5 件を超えたら落とす
facts: { dropped: 0, added: 0, allow_dropped: [heading] }
citations: { failed: 0, required: true } # required: sources があって citations が無い出力を落とす
penalty: 10 # 点の和がこれを超えたら落とす
$ npx chaffjs grade prompt-b.jsonl --compact
q3 fail penalty 0 facts.dropped 3 > 0, facts.added 1 > 0
refund fail penalty 0 citations.failed 1 > 0
deploy fail penalty 8 rules.closing-cliche 2 > 0
prompt-b.jsonl: 3 件の出力、0 件が通り、3 件が落ちた
点は減点の和で、満点からの点数ではありません。一点ごとに、どの指摘から来たかが分かります(deploy の結果の行)。
"score": { "penalty": 8, "items": [
{ "points": 2, "rule": "ai-tell", "line": 3 },
{ "points": 3, "rule": "closing-cliche", "line": 12 },
{ "points": 3, "rule": "closing-cliche", "line": 12 } ] }
grade: が無ければ点は出ません。grade: に読めない値があれば、場所を示して終了コード 2 で止まります。
chaff の知らないルールの名前は、そう言ったうえで「動かなかったもの」に並べます。
max_rate の単位は出力の言語で決まります。上の例では、英語の出力は 1,000 語あたり、日本語の出力は 1,000 字あたりの 5 件です。
2 つの回を比べる(--baseline)
前の prompt の出力を --out 付きで採点し、新しい prompt の出力をそれと比べます。組は id で作ります。
$ npx chaffjs grade prompt-a.jsonl --out a.results.jsonl
$ npx chaffjs grade prompt-b.jsonl --baseline a.results.jsonl
…上と同じ要約…
a.results.jsonl と比べた: 3 件の出力が組になった
ルールごとの率(1,000 字あたり、前 → 後)
ai-generated-composite 0.0 → 3.0 (+3.0) 増えた: deploy
ai-tell 0.0 → 3.0 (+3.0) 増えた: deploy
closing-cliche 0.0 → 6.0 (+6.0) 増えた: deploy
padded-intro 0.0 → 3.0 (+3.0) 増えた: deploy
新しく落ちた: q3, refund
新しく通った: なし
新しく落ちた・足された事実と、新しく外れた引用
q3: 落ちた number 6 時間, number 2.5 時間, date 2026年7月14日、足された date 2026年7月1日
refund: 落ちた なし、足された なし、外れた引用 policy 2.2
回帰 2 件
✗ q3: passed, now fails
✗ refund: passed, now fails
回帰とは、次のどれかです。
- 前は通った出力が落ちた
grade.rulesに書いたルールの指摘が、どれかの出力で増えた- 点の和が増えた
--baseline を付けると、回帰があれば終了コード 1、無ければ 0 です。prompt や model の変更を CI で止められます。
grade.rules に無いルールの増減は表に出しますが、回帰には数えません。
2 つの回は、ルールと設定が同じでなければなりません。違えば比べずに止まります。設定の違いを model の違いと読ませないためです。
$ npx chaffjs grade prompt-b.jsonl --baseline a.results.jsonl
a.results.jsonl とは比べません: 設定が違います。ルールか設定が変わった差を、prompt や model の差として読まないため(--allow-stamp-mismatch で比べる)
終了コード 2 で終わります。前の回のあとで chaff.yaml の rules: を書き換えたので、設定が違います。
prompt や model を並べて比べる(variant)
--baseline は、前の回と今回を比べます。同じ回の中で複数の prompt や model を比べるときは、すべての出力を 1 つのファイルに入れ、行ごとにどの variant かを書きます。同じ題の行には同じ id を付けます。variant の名前は variant の欄に書くか、--variant-key で別の欄を指します。
{"id": "q3", "prompt": "prompt-a", "output": "# 要点\n\n問い合わせ 4,812 件に答え、最初の返信までの時間の中央値は 6 時間から 2.5 時間に縮みました。\n…", "reference": "# サポート窓口の四半期報告\n…"}
{"id": "q3", "prompt": "prompt-b", "output": "# 要点\n\n問い合わせ 4,812 件に答え、最初の返信は大幅に速くなりました。\n…", "reference": "# サポート窓口の四半期報告\n…"}
prompts-ja.jsonl は、例 1〜3 の 3 つの題に 2 つの prompt で答えさせた出力です。どの行も prompt の欄に名前があります。
prompt-a の返金の回答は、2 つめの引用を条文どおり「送料は返金しない。」と書いています。いつもの要約のあとに、variant を並べた表が出ます。
$ npx chaffjs grade prompts-ja.jsonl --variant-key prompt
prompts-ja.jsonl: 6 件の出力、4 件が通り、2 件が落ちた
落ちた出力
✗ q3 (prompt-b): facts.dropped 3 > 0, facts.added 1 > 0
✗ refund (prompt-b): citations.failed 1 > 0
…
2 つの variant を並べた: どの variant にもある id の出力 3 件
prompt-a prompt-b
通った 3/3(100%) 1/3(33.3%)
落ちた事実 0 3
足された事実 0 1
外れた引用 0/2 1/2
AI らしさ 低/中/高 0/0/0(3) 0/0/0(3)
ルールごとの率(1,000 字あたり)
prompt-a prompt-b
ai-generated-composite 0.0 3.0
ai-tell 0.0 3.0
closing-cliche 0.0 6.0
padded-intro 0.0 3.0
合否が分かれた出力 2 件
✗ q3: 通った prompt-a、落ちた prompt-b(facts.dropped 3 > 0, facts.added 1 > 0)
✗ refund: 通った prompt-a、落ちた prompt-b(citations.failed 1 > 0)
読み方:
- どの列も同じ題を数えます。比べるのは、すべての variant にあり、言語とジャンルが同じ
idだけです。それ以外のidは「比べなかった id」に、無かった variant と並びます。 - 「外れた引用」は、照らした数のうち外れた数です。事実は要約と同じに数え、
grade:で許した種類は数えません。 - 出力に
contextsがあれば、「一節に無い事実」の行が足されます。照らした事実のうち、検索で取ったどの一節にも無かった数です。 chaff.yamlにgrade:があれば、「減点の和」の行が足されます。- 「AI らしさ 低/中/高」は、段階ごとの出力の数で、かっこの中は測らなかった出力の数です。
- 通った割合は出力の割合で、点ではありません。満点のある点は、ここでも出しません。
--variant-key を渡すと、どの行にもその欄が要ります。variant の欄を使うときも、一行にでもあれば全行に要ります。名前の無い行や、同じ variant に同じ id が二度ある行があれば、終了コード 2 で止まります。終了コードはほかは variant が無いときと同じです。--baseline は、前の回の同じ id と同じ variant を組にします。
CI のログには、--compact が合否の分かれた題を 1 行ずつ足します。
$ npx chaffjs grade prompts-ja.jsonl --variant-key prompt --compact
…
disagree q3 pass prompt-a fail prompt-b
disagree refund pass prompt-a fail prompt-b
合否が分かれた出力 2 件
PR のコメントには、--format markdown が同じ表を Markdown で書きます。--format json は要約に variants の欄を足します。
$ npx chaffjs grade prompts-ja.jsonl --variant-key prompt --format markdown
## chaff grade: prompts-ja.jsonl
6 件の出力、4 件が通り、2 件が落ちた
…
### 2 つの variant を並べた: どの variant にもある id の出力 3 件
| | prompt-a | prompt-b |
| --- | --- | --- |
| 通った | 3/3(100%) | 1/3(33.3%) |
| 落ちた事実 | 0 | 3 |
| 足された事実 | 0 | 1 |
| 外れた引用 | 0/2 | 1/2 |
…
評価の仕組みの中からは、grade() に variant を渡し、結果を compareVariants() に渡します。返るのは、--format json が variants に入れるものと同じです。
import { compareVariants, grade } from "chaffjs/grade";
const results = [
await grade(answerA, { id: "q3", variant: "prompt-a", reference }),
await grade(answerB, { id: "q3", variant: "prompt-b", reference }),
];
const { columns, disagreements } = compareVariants(results);
console.log(columns.map((column) => `${column.variant} ${column.passed}/${column.outputs}`)); // [ 'prompt-a 1/1', 'prompt-b 0/1' ]
compareVariants({ "prompt-a": resultsA, "prompt-b": resultsB }) のように、variant ごとに分けた結果も渡せます。
RAG の回答は、検索で取った一節に拠っているか(contexts)
citations は、回答が示した引用を照らします。回答が引用を示さないことも多く、手元にあるのは検索が model に渡した一節だけ、ということがあります。その一節を contexts に入れると、chaff は回答の事実を一節の中に探します。照らすのは、数、日付、時刻、URL、コード、固有名詞、引用です。事実は compare と同じ読み方で取り出すので、1,200 円 と 1,200円 のように書き方が違っても同じ事実です。引用(「」の中)は、どれかの一節に一字一句あるときだけ支えられたとします。
rag-ja.jsonl は、同じ 2 つの一節を渡して答えさせた 2 つの回答です。
{"id": "料金", "output": "チームプランは 1 人あたり月 1,200 円で、100 GB の保存領域が付きます。サポートは 4 営業時間以内に返信します。", "contexts": ["# 料金\n\nチームプランは 1 人あたり月 1,200 円で、年払いです。\n1 人あたり 100 GB の保存領域が付きます。", "# サポート\n\nチームプランのサポートは 4 営業時間以内に返信します。\nヘルプセンターは 2026年3月3日に移りました。"]}
{"id": "料金-誤り", "output": "チームプランは 1 人あたり月 1,500 円で、100 GB の保存領域が付きます。資料には「サポートは 1 時間以内に返信します」とあります。多くのチームに向いています。", "contexts": ["…同じ 2 つの一節…"]}
$ npx chaffjs grade rag-ja.jsonl --out rag-ja.results.jsonl
出力ごとの結果を書きました: rag-ja.results.jsonl(2 行)
rag-ja.jsonl: 2 件の出力、1 件が通り、1 件が落ちた
落ちた出力
✗ 料金-誤り: contexts.unsupported 3 > 0
事実: 落ちた 0、足された 0
引用: 0 件を照らし、0 件が外れた
contexts: 2 件の出力の事実 11 件を照らし、3 件がどの一節にも無かった(number 2, quote 1)
…
contexts 1 件の出力 数・日付・名前・引用の無い文 1 件は、contexts に支えられているかを照らしていない(意味を読む必要がある)
…
料金-誤り の結果の行には、どの事実がどの一節(0 から数える)にあり、どれが無かったかが入ります。
"contexts": {"passages":2,"checked":6,
"supported":[{"kind":"number","text":"1 人","line":1,"passage":0},{"kind":"number","text":"100","line":1,"passage":0},{"kind":"name","text":"GB","line":1,"passage":0}],
"unsupported":[{"kind":"number","key":"1500 円","text":"1,500 円","line":1,"allowed":false},
{"kind":"number","key":"1 時間","text":"1 時間","line":1,"allowed":false},
{"kind":"quote","key":"サポートは 1 時間以内に返信します","text":"「サポートは 1 時間以内に返信します」","line":1,"allowed":false}],
"uncheckedSentences":1}
- 1,500 円と 1 時間はどの一節にも無く、引用は一字一句の形でどの一節にもありません。
grade:が無ければ、一つでもあれば落ちます。 contextsを空の配列[]にすると「一節が取れなかった」で、照らした事実はすべて一節に無いことになります。contextsを書かないのとは違います。書かなければ照合は動かず、「動かなかったもの」にそう出ます。- 「多くのチームに向いています。」には、chaff が照らせる事実がありません。
uncheckedSentencesに数え、「動かなかったもの」にcontextsとして出します。一節に支えられているかは、読まないと分かりません。 - 数が一節にあることは、主張が正しいことではありません。一節の数を別の物の数として書いた回答も、この照合は通ります。意味の照合は model か人に任せます。
採点の基準では、contexts に上限を書きます。
grade:
contexts:
unsupported: 0 # どの一節にも無くてよい事実の数
allow_unsupported: [name] # 数えない種類
required: true # contexts の無い出力を落とす
grade() にも contexts で一節を渡せます: await grade(answer, { contexts: passages })。
LLM のベンチマークを採点する(--rubric、expect、tasks)
ベンチマークは、要約・抜き出し・資料に拠った回答など、種類の違う問いを混ぜます。 chaff は、AI による採点の横で、機械で決まる部分を毎回同じに採点します。採点の基準はデータと一緒に持ち運べるよう、別のファイルに書きます。抜き出しや計算のように正解の決まった問いには、行ごとに正解の事実を書きます。
採点の基準を書きます。 形は chaff.yaml の grade: の中身と同じです。
tasks: の下に、問いの種類ごとの基準を書きます。
rules:
total-mismatch: { max: 0 } # どの問いでも、合計と内訳が合わなければ落とす
facts: { dropped: 0, added: 0 }
tasks:
summary:
facts: { added: 0 } # 要約は事実を省いてよい。足してはいけない
extraction: # 最上位のまま。正解は行ごとの expect で確かめる
grounded:
citations: { failed: 0, required: true }
- 種類の基準が書いたキーは、最上位のそのキーを丸ごと置き換えます。
summaryのfactsにはdroppedが無いので、省いた事実では落ちません。 - 書かなかったキーは最上位を継ぎます。
rulesだけはルールごとに重ね、種類の基準が書いたルールだけを置き換えます。 - 何も書かない種類(
extraction:)は、最上位の基準そのままです。 - JSON でも書けます。拡張子を
.jsonにし、同じ形で書きます。
問いを並べます。 行の task に種類の名前を書きます。task の無い行は、最上位の基準で採点します。正解の決まった問いには expect を書きます。facts は出力にあるべき事実、absent はあってはならない事実です。
{"id": "要約-1", "task": "summary", "output": "4 月 8 日の定例会議には 12 人が参加しました。", "reference": "# 定例会議の記録\n\n4 月の定例会議は 4 月 8 日に開き、12 人が参加しました。次の会議は 5 月 13 日です。"}
{"id": "要約-2", "task": "summary", "output": "4 月 8 日の定例会議には 15 人が参加しました。", "reference": "# 定例会議の記録\n\n4 月の定例会議は 4 月 8 日に開き、12 人が参加しました。次の会議は 5 月 13 日です。"}
{"id": "抜き出し-1", "task": "extraction", "output": "請求額は 12000円、支払いの期限は 2026-03-31 です。", "expect": {"facts": ["12,000円", "2026年3月31日"]}}
{"id": "抜き出し-2", "task": "extraction", "output": "請求額は 15,000円、支払いの期限は 2026年3月31日です。", "expect": {"facts": ["12,000円", "2026年3月31日"], "absent": ["15,000円"]}}
{"id": "返金", "task": "grounded", "output": "商品が届いてから 14 日以内なら返金を求められます。送料は戻りません。", "sources": {"policy": "# 返金の決まり\n\n第1条(期限)\n商品が届いた日から 14 日以内なら、返金を求められる。\n送料は返金しない。\n"}, "citations": [{"address": "1.1", "quote": "商品が届いた日から 14 日以内"}, {"address": "1.2", "quote": "送料も返金する。"}]}
基準のファイルを渡して採点します。 --rubric を渡した回は、chaff.yaml の grade: を使いません。
$ npx chaffjs grade bench.jsonl --rubric rubric.yaml
bench.jsonl: 5 件の出力、2 件が通り、3 件が落ちた
落ちた出力
✗ 要約-2: facts.added 1 > 0
✗ 抜き出し-2: expect.missing 12,000円, expect.present 15,000円
✗ 返金: citations.failed 1 > 0
事実: 落ちた 9(heading 2, number 7)、足された 1(number 1)
引用: 2 件を照らし、1 件が外れた
減点の和: 0
AI らしさ: 低 0、中 0、高 0、測っていない 5(書いたのが AI かどうかの判定ではありません。合否には入りません)
…
$ npx chaffjs grade bench.jsonl --rubric rubric.yaml --compact
要約-1 pass penalty 0
要約-2 fail penalty 0 facts.added 1 > 0
抜き出し-1 pass penalty 0
抜き出し-2 fail penalty 0 expect.missing 12,000円, expect.present 15,000円
返金 fail penalty 0 citations.failed 1 > 0
bench.jsonl: 5 件の出力、2 件が通り、3 件が落ちた
読み方:
要約-1は見出しと次の会議の日を省きましたが、summaryの基準は省いた事実に上限を置かないので通ります。省いた事実は「事実:」の行に数えます。要約-2は元に無い「15 人」を足したので落ちます。抜き出し-1は12000円と2026-03-31で答えています。正解の12,000円と2026年3月31日と同じ事実なので通ります。抜き出し-2は正解の金額が無く(expect.missing)、あってはならない金額がある(expect.present)ので落ちます。返金は 2 つめの引用が原文にありません。groundedの基準のcitationsで落ちます。
--out の結果の行には、task と、expect で照らした数、無かった事実、あった事実が入ります。
"task":"extraction",…,"expect":{"checked":3,"missing":["12,000円"],"present":["15,000円"]}
再現の印(stamp)に入る設定のハッシュは、基準のファイルの中身も含めて作ります。基準の違う回どうしは、--baseline が比べずに止まります。
同じ事実と読む書き方。 expect の事実は compare と同じに読みます。
12,000円 と 12000円 と 12,000円、2026年3月31日 と 2026-03-31 は同じ事実です。
¥12,000 と 1万2000円 は、12,000円 とは別の事実と読みます。正しい答えでも、言い換えると合いません。「期限は年度末です」は expect.missing 2026年3月31日 で落ち、落ちた出力に並びます。これは意味を読まないと決まらないので、並んだ出力を AI による採点か人に回します。
採点が始まらない入力。 次の入力は、出力の誤りではなく問いの誤りです。行と欄を示し、終了コード 2 で止まります。
基準の tasks に無い種類の名前です。黙って最上位の基準で採点すると、その行の基準が知らないうちに変わるので止めます。
{"id": "翻訳-1", "task": "translation", "output": "The meeting was held on April 8."}
$ npx chaffjs grade bad-task.jsonl --rubric rubric.yaml
bad-task.jsonl: 1 行目: task "translation" は採点基準の tasks にありません
ちょうど 1 つの事実でない expect の項目です。1万2000円 は 2 つの事実に読まれ、どちらを確かめるのか決まりません。事実が 1 つも無い項目も同じに止まります。確かめるものの無い項目が、何も見ずに通らないためです。
{"id": "抜き出し-3", "task": "extraction", "output": "請求額は 1万2000円です。", "expect": {"facts": ["1万2000円"]}}
$ npx chaffjs grade bad-expect.jsonl --rubric rubric.yaml
bad-expect.jsonl: 1 行目: expect.facts[0] 1万2000円 は事実を 2 つ以上含みます(compare が 1 つの事実と読む書き方で、1 項目に 1 つ書いてください)
例
文書と chaff の実際の出力で見せる例を 4 つ、別のページ「AI の評価の例」にまとめました。
- 要約は元の事実を守っているか:
compareで確かめます。 - RAG の回答の引用は原文にあるか:
citeで確かめます。 - 2 つの prompt を文章の形で比べる:作り直しに渡す計画も出します。
- 自分と矛盾する回答:合計と曜日を確かめます。
指摘を SARIF で受け取る
--sarif は、かけたファイルすべての指摘を 1 つの SARIF ファイルに書きます。画面の出力は変わりません。
$ npx chaffjs prompt-b.md --compact --sarif out/prompt-b.sarif
SARIF を書きました: out/prompt-b.sarif(5 件)
prompt-b.md blog/tech · 日本語 ジャンルは既定から
3:1 info 「重要な役割を果たす、と言えるでしょう、深く掘り下げる、本記事では、地味に効く」が揃っています(点 29、18 まで)
ai-tell
…
指摘は runs[0].results に 1 件ずつ入り、ルール・重さ・場所が付きます。次はその 1 件目です。
{
"ruleId": "chaff/ai-tell",
"level": "note",
"message": {
"text": "「重要な役割を果たす、と言えるでしょう、深く掘り下げる、本記事では、地味に効く」が揃っています(点 29、18 まで)"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "prompt-b.md"
},
"region": {
"startLine": 3,
"startColumn": 1
}
}
}
]
}
重さは error、warning、note(chaff の info)です。同じファイルで PR の行に指摘を出せます(CI)。
評価の仕組みの中から呼ぶ(grade() と採点役の形)
chaffjs/grade は 1 つの出力をその場で採点し、--out の 1 行と同じ結果を返します。
chaff.yaml を読むのは config で名指したときだけで、ファイルに書くことはありません。行に書く欄は、task と expect も含めて同じ名前で渡せます: await grade(output, { task: "extraction", expect: { facts: ["12,000円"] }, config: "chaff.yaml" })。
import { grade, toScorer } from "chaffjs/grade";
const result = await grade(output, { id: "q3", reference, config: "chaff.yaml" });
console.log(result.pass, result.failedBecause); // false [ 'facts.dropped 2 > 0' ]
const scored = toScorer(result);
console.log(scored.score, scored.reason); // 0 failed: facts.dropped 2 > 0 — no findings
toScorer() は、多くの評価基盤が受け取る形に直します。
namescore: 通れば 1、落ちれば 0passreason: 落ちた条件、ルールごとの指摘、点の和の順metadata: 結果のすべて
chaff は満点を持たないので、点の和を 0〜1 に写すことはしません。段階のある数が欲しい評価基盤は、metadata.score.penalty を読んで自分で写します。
chaff grade が終了コード 2 で断る入力では、GradeInputError を投げます。
評価基盤への組み込み
どの例も grade() か chaff grade を呼ぶだけで、chaff の中に基盤ごとの処理は持ちません。例は examples/evals にあります。基盤は自分のプロジェクトに入れてください。chaff の依存にはなりません。
| 基盤 | 例 | つなぎ方 |
|---|---|---|
| promptfoo | promptfoo/chaff-assertion.cjs | javascript の assertion が pass・score・reason を返す |
| autoevals、Braintrust | autoevals/chaff-scorer.mjs | 採点の関数 ({ output, expected }) => { name, score, metadata } |
| evalite | evalite/chaff.eval.ts | createScorer で作る採点役 |
| Langfuse | langfuse/push-score.mjs | trace に langfuse.score.create() で点を付ける |
| DeepEval | deepeval/chaff_metric.py | CLI を呼ぶ BaseMetric |
| Ragas | ragas/chaff_with_faithfulness.py | Faithfulness の横に chaff の事実の照合を置く |
| Inspect AI | inspect/chaff_scorer.py | CLI を呼ぶ @scorer |
| OpenAI Evals | openai-evals/ | samples.jsonl(input・ideal)と生成した出力を、chaff grade の入力に変える |
| GitHub Actions | README.md | workflow の一段で chaff grade --baseline を走らせる |
promptfoo の assertion、autoevals の採点役、OpenAI Evals の変換は、このリポジトリで試験しています。どの試験も、通信も API key も使いません。
# promptfooconfig.yaml
tests:
- vars:
reference: "The team answered 4,812 tickets this quarter."
assert:
- type: javascript
value: file://chaff-assertion.cjs
評価の中での chaff の持ち場
model による採点は意味を読みます。答えが正しいか、問いに合っているか、元の文書に忠実かを見ます。ただ、同じ出力でも回ごとに点が揺れることがあり、理由は文章で返ります。 chaff は同じ出力にいつも同じ結果を返し、落ちた理由はすべて行とルールを指します。答えるのは、機械で決まることだけです。
- 元の文書の事実を守っているか
- 引用が本当に原文にあるか
- 合計と曜日が合っているか
- チームの決まりを守っているか、形の指摘がどれだけあるか
model の採点や人の目の代わりにではなく、その横に置いて使います。
ここで chaff がしないこと
- model で採点しません。 採点は決まった結果を返します。model に聞く
chaff testは別のコマンドのままで、採点からは呼びません。 - 出力を書き換えません。
fix-planは指示を出すだけで、書き直すのは作り直しの段です。 - 「AI が書いた」とは言いません。
ai-generated-compositeは数えるための目印で、判定ではありません。 - 重みや満点を持ちません。 指摘 1 件が何点かはチームが決め、
chaff.yamlに書きます。