- 公開日
低い検索スコアで回答生成を止めるなら、業務の質問と資料を使い、「根拠不足なのに通す件数」と「答えられるのに保留する件数」を比べて閾値を決めます。スコアが0.8だから回答が80%の確率で正しい、という読み方はできません。閾値は生成へ進む条件の一つであり、通過した回答の正しさは別に確かめます。
RAGは検索した資料を参照して回答を作る仕組みです。検索で似た文章が見つかっても、対象製品や適用期間が違えば、その質問の根拠には使えません。まず、根拠不足を見つける検索の判定と、作られた回答を確認する判定を分けます。検索の失敗と生成の誤りを切り分ける考え方は、RAGはハルシネーション対策の万能薬ではないで扱っています。
スコアの名前と工程を固定する
検索器の類似度と、取得後に候補を並べ替えるRerankerの関連度スコアは、同じ数値でも同じ意味とは限りません。まず、どのモデル・設定が、質問とどの文書の組に付けた値かを記録します。値が大きいほど近い方式か、小さいほど近い距離なのかも確認が必要です。
CohereのBest Practices for using Rerankは、スコアの解釈が質問と文書に依存すること、領域を代表する質問と関連性の境界にある文書を用いて閾値の参考分布を得る方法を説明しています。これは同社Rerankのガイダンスであり、すべての検索方式に共通する閾値や、回答の正解確率を与えるものではありません。
たとえば、第一段階の検索スコアで候補を落としてからRerankerを使う場合と、Rerankerの出力で回答を保留する場合では、止める位置が違います。前者で必要文書を落とせば、後者はその文書を評価できません。両方を一度に変更した比較から、どちらが誤回答を減らしたかは切り分けられません。
運用ルールには、次の事項を一緒に残します。
- 判定に使う工程、スコアの種類、モデル・索引・設定の版
- 一位のスコア、上位複数件の最小値など、質問ごとの判定値を作る方法
- 閾値と等号の扱い、候補なし・スコア欠損・検索失敗の別処理
- スコア以外に確認する対象、期間、権限、必要な根拠の条件
上位一件のスコアを使う方式は単純ですが、複数の規程を合わせないと答えられない問いでは、一件だけ高くても根拠が揃ったことになりません。一方、候補全件が閾値以上であることを求めると、末尾に不要な候補が一件加わっただけで保留になることがあります。集約方法も閾値と組み合わせて評価する対象です。
業務データに「正解資料なし」を含める
評価用の質問には、よく来る短い質問だけでなく、略語、製品番号、否定条件、過去時点、複数の根拠を必要とする問いを含めます。実データを使う場合は、評価に使える範囲を確認し、不要な個人情報を取り除きます。質問群の比率が本番と大きく違うなら、集計値をそのまま本番の見込みにしません。
質問ごとに、その時点で利用者が参照できる資料集合を固定し、担当者が必要な根拠箇所と期待回答を定めます。ここでの「正解資料あり」は、その集合の中に回答に必要な資料が揃っている状態です。一つの検索で見つからなかっただけでは、「正解資料なし」と判定できません。
「なし」とラベルを付けるには、対象とした資料の範囲、版、権限と、確認した方法を残します。別の保管先まで含む世の中全体で資料が存在しない、という意味ではありません。資料の棚卸しが終わっていない質問は「未確認」とし、なしへ押し込めないことが重要です。
同じ名前で別製品の資料だけがある質問、現行条件を尋ねたのに旧版しかない質問も役立ちます。文章が似ていても、対象の一致を確認できない例を含めるためです。権限外の資料に答えがある場合も、その利用者向けの根拠が揃ったとは扱いません。
資料の有無と、回答の正誤には別々のラベルを付けます。回答の合格条件は、対象・適用時点が合い、必要項目が揃い、根拠のない条件を足していないことなど、業務に合わせて先に決めます。採点資料が足りない場合と、資料に反する回答だと確認できた場合を区別し、判断が割れた例は担当者間で原文へ戻って確認します。
次に、閾値を選ぶ調整用データと、選んだルールを最後に確認する独立評価用データを分けます。同じ質問の言い換えや同じ規程から作ったほぼ同一の例が両方に大量に入ると、未知の質問に対する確認が弱くなります。分け方は文書群や業務領域も考慮し、調整で見た正解を独立評価へ持ち込まないようにします。
同じ質問と取得結果で、閾値だけを変える
以下は計算方法を説明する架空例で、検索やモデルを実行した結果ではありません。社内貸出の質問100問を用意し、同一の資料版と閲覧権限で確認すると、70問には必要資料があり、30問にはありません。この100問では資料の有無のラベルはすべて確認済みと仮定します。
一問の入力を「夜間に借りた機器は、返却予定日が休業日ならいつ、どこへ返すか」とします。正解資料には「次の営業日の午前10時までに総務の受付窓口へ返却」とあります。別製品の返却手順や期限だけの文書では、期待回答の条件が揃いません。
認可と明示した絞り込み条件を通った検索候補について、一位のスコアを判定値とします。この架空方式では大きいほど関連性が高いと仮定し、判定値が閾値以上なら生成候補へ進み、未満なら業務回答を保留します。候補ゼロや値の欠損は、低い数値へ変換せず別の理由で保留します。
比較では質問、資料、索引、検索器、取得件数、Reranker、入力の組み立て、生成モデルと設定、採点基準を固定します。取得結果を保存して使い回し、変えるのは閾値だけです。次の0.40と0.70は説明用の架空値で、Cohereやほかの製品への推奨値ではありません。
| 質問の資料状態 | 質問数 | 閾値0.40:通過/保留 | 閾値0.70:通過/保留 |
|---|---|---|---|
| 必要資料あり | 70 | 60/10 | 45/25 |
| 必要資料なし | 30 | 20/10 | 5/25 |
| 合計 | 100 | 80/20 | 50/50 |
閾値0.40では、資料がない30問のうち20問を通します。ここではこれを「誤通過」と呼びます。一方、必要資料がある70問のうち10問は保留です。これは資料集合を基準にした「過剰保留」で、取得に失敗した質問も含みます。取得済みの根拠だけを基準にした保留の適否とは分けます。
閾値0.70では誤通過は5問に減り、過剰保留は25問に増えるという仮定です。質問全体に対する通過率は80÷100=80%から50÷100=50%になります。必要資料ありの質問に対する過剰保留率は10÷70で約14.3%、25÷70で約35.7%です。割合を報告するときは、全質問を分母にした保留率と混ぜないようにします。
個々の質問も確認します。先の貸出質問の判定値が0.62なら、0.40では通り、0.70では止まります。必要資料の有無を人が確認したうえで、実際に必要箇所が取得されていたかを追います。資料はあるのに窓口の記述が取得されていなければ、閾値を下げる前に、検索や文書分割の欠落を調べます。
たとえば別製品の文書が0.85を得る仮定なら、どちらの閾値も通過します。対象識別の誤りを止めるには製品番号との照合が必要で、0.70を選ぶことだけでは足りません。低い値の質問だけを見ず、高い値で誤通過する質問の中身も確認します。
高い閾値を採用すれば、答えられる問い合わせが減る可能性があります。保留一件を誰が処理し、どのくらい待たせるか、誤回答がどのような手戻りを生むかを業務側と決めます。許容する誤通過と保留の負担を先に定め、成績が良く見える閾値を後から選ぶだけにしないことが大切です。
通過率、回答の正しさ、保留を別に数える
資料がある質問でも、取得や生成に失敗すれば誤回答になります。資料がない質問を通しても、生成側の別の確認で保留できる設計なら、利用者に誤回答が届くとは限りません。したがって、検索ゲートの通過と、最終的に業務回答を返したことを別の欄に記録します。
研究RAGAs: Automated Evaluation of Retrieval Augmented Generationも、関連する文脈の取得、文脈に忠実な生成、生成内容の品質を別の評価側面として扱います。以下の集計はその研究の測定結果ではなく、本例の独自の採点方法です。
全質問のうち実質的な業務回答を返す割合を、ここでは回答カバレッジと呼びます。検索ゲートを通った割合とは、生成後にも保留する設計では一致しません。
計算を単純にするため、先ほどの通過質問にはすべて実質的な業務回答を返し、生成後の保留はなかったと仮定します。同一質問の回答を固定して二つの閾値で選び分け、次の件数になったものとします。
| 最終回答の評価 | 閾値0.40 | 閾値0.70 |
|---|---|---|
| 根拠と期待回答に照合して正しい | 54 | 42 |
| 誤りを確認した | 21 | 6 |
| 未検証・判定不能 | 5 | 2 |
| 業務回答を返した合計 | 80 | 50 |
判定できた回答に占める確認済みの誤りは、0.40で21÷75=28%、0.70で6÷48=12.5%です。未検証・判定不能を分母から除いた数であり、それぞれ5問、2問が残ります。回答全体に対する確認済みの誤りは21÷80で約26.3%、6÷50=12%ですが、未確認分に誤りがあれば上がります。この表の未検証と判定不能は表示をまとめていますが、実際の記録では、照合資料が不足したのか、表現が曖昧で判定できなかったのかを分けて残します。
元の100問に対する確認済みの誤回答は21%と6%です。ただし、後者では半分を保留しています。この比率だけで「全体の正確性が94%」とは言えません。保留を正答に加えず、正しい回答の件数、誤回答、未確認、保留を並べて報告します。本例では確認済みの正しい回答も54問から42問へ減っています。
実運用で生成後にも保留するなら、ゲート通過数、実質的回答数、生成後保留数を分け直します。必要資料がない質問で、根拠不足を伝えて案内した対応は適切な保留として採点できますが、業務質問への正答と同じ件数にまとめません。部分回答を認める場合も、満たした必須項目と不足を別に評価します。
閾値を確定する前に、残る危険を確かめる
候補ごとの比較では、代表的な質問群だけでなく、誤りの影響が大きい群の結果を確認します。全体平均が良くても、同名の製品や否定条件を含む質問だけで誤通過が続くなら、単一の閾値では解決しにくい問題です。対象識別の確認や質問の聞き返しを追加する方が適切な場合があります。
スコア帯ごとに、根拠充足と回答の正誤がどの程度対応するかも調べます。もし「正答確率」として表示したいなら、何の正答を予測するかを定め、代表性のあるデータで確率への変換を学習・調整し、独立したデータで予測値と実際の正答割合の対応を検証します。これが確率の校正です。On Calibration of Modern Neural Networksは、分類モデルの確率推定を実際の正しさの見込みに対応させる問題として説明しています。ここでのRAG評価の手順は独自の設計案であり、同論文が検索スコアの変換を検証したという意味ではありません。単に0から1の範囲へ縮めるだけでは校正になりません。
小さな評価集合で誤答ゼロだったことも、将来の誤答ゼロを保証しません。質問群別の母数、反復時のばらつき、判定者が迷った例を残します。閾値調整で独立評価を何度も見て選び直したなら、その集合は調整に使ったものとして扱い、改めて未使用の評価を用意します。
採用時には、単一の数値だけでなく、データの対象期間、正解ラベル、スコアの算出方法、モデル・索引の版、同点と欠損の扱い、保留後の処理を一組で保存します。検索器やReranker、資料構成、利用者の質問分布が変わったら、以前の閾値をそのまま移す前に再評価します。新旧方式の数字が同じ範囲でも、同じ基準で答えを通せる証拠にはなりません。
保留は、次に確かめる点まで伝える
利用者には「スコア不足」とだけ表示せず、何を確認できなかったかと次の方法を示します。貸出の例なら、「返却期限と窓口を裏付ける資料を、現在の検索範囲では確認できませんでした。機器の製品番号と、適用する貸出規程の版を確認してください」のように案内できます。取得できたのが期限だけなら、その出典と適用条件を示し、窓口は未確認と分ける方法もあります。
「規程は存在しません」「返却不要です」とは言い換えません。検索障害なら資料不足と区別して、参照できなかったため判断を保留したと伝えます。権限外の資料の存在や内容を案内文で漏らさず、確認先も利用者に知らせてよい窓口に限定します。
追加検索を認める場合も、対象の特定や許可済みの資料範囲の確認から進めます。回答を出すためだけに閾値を下げ続けたり、閲覧制限を外したりはしません。保留理由と確認手段まで含めて評価すれば、誤回答を避けることと、利用者が業務を続けられることを一緒に検討できます。
検索語の書き換えでユーザーの意図を変えない方法は「Query Rewritingでユーザーの意図を変質させない」、検索結果が足りないときに結論を保留する考え方は「検索結果が十分でないとき、結論を保留する「証拠充足」の考え方」で解説しています。
追補:架空8文書で検索ゲートを実際に動かした結果
2026年10月23日、上記の架空100問表とは別に、Pythonで小さな検索実験を実行しました。LLMによる回答生成は行っていません。架空文書を使った字句検索と、検索結果を通す・保留する判定だけを確かめたものです。本文の架空例を実測値へ読み替えるものではありません。
資料は製品の返品・保証、機器の返却、旅費申請に関する架空8文書。質問18問のうち、必要な根拠がこの資料集合にあるものを10問、ないものを8問と事前にラベル付けしました。文書と質問はCodexが説明用に作成し、同じ作成過程で根拠文書を指定しています。実在する企業の規程や利用者データではありません。
何を固定して測ったか
検索は自作のBM25(k1=1.2、b=0.75)。日本語の文字2-gramと英数字の字句を使い、上位3件の正スコア文書を取得します。検索結果を一度保存し、一位のスコアが閾値以上なら通過、未満なら保留にしました。候補がない場合は数値0へ変換せず、別理由で保留します。閾値0・2・4・8は実行前に固定した比較用の値で、運用上の推奨値ではありません。
対象ID・地域・期間を照合するフィルタや、Rerankerは入れていません。たとえばP-20は字句のpと20へ分かれ、識別子全体の完全一致を強制しません。この検索器は否定や時点の意味を理解しないため、似た文書が高得点になっても適用可能な根拠とは限りません。
| 閾値 | 通過/18問 | 保留/18問 | 根拠なしの誤通過/8問 | 根拠ありの過剰保留/10問 |
|---|---|---|---|---|
| 0 | 16 | 2 | 6 | 0 |
| 2 | 16 | 2 | 6 | 0 |
| 4 | 16 | 2 | 6 | 0 |
| 8 | 14 | 4 | 5 | 1 |
閾値0・2・4では判定が変わりませんでした。4から8へ上げると、根拠なしの誤通過は6件から5件へ減りましたが、根拠がある質問の過剰保留も0件から1件へ増えました。ここでの「誤通過」は検索ゲートの判定であり、生成された誤回答数ではありません。
閾値を上げても残った取り違え
- Q10「国内出張のレシートを出す締切は?」では、正しいD07を取得していましたが、一位スコア6.522068のため閾値8で保留しました。
- Q14「M-30の返却予定日が休業日なら、いつどこへ返却する?」には、M-30の資料がありません。一位はM-20のD06で、スコア25.069183となり全閾値を通過しました。
- Q16「P-30の保証期間」にはP-30の保証資料がありませんが、P-20のD08が31.099818となり全閾値を通過しました。
この条件では、高い閾値だけで対象の違いを解消できず、答えられる質問も保留されました。対象・適用期間・必要事項の確認を、スコアとは別に設計する理由が見えます。表示スコアは小数点以下6桁に丸めていますが、判定には丸め前の値を使っています。
この実験から言えないこと
質問は、似た別製品や別時点を含む難例を意図的に入れた18問です。現実の質問分布を代表せず、調整用と独立評価用にも分割していません。正解ラベルは検索スコア計算には使っていませんが、同じ作成者が文書と質問・ラベルを設計したことによる選定バイアスは残ります。未知の質問に対する性能、本番の誤通過率、最適閾値は評価できません。
初回結果を見てデータや閾値を選び直しておらず、改善しない比較もそのまま掲載しています。必要文書が上位3件にあるという採点は、回答の正しさを採点したものではありません。LLM、埋め込み検索、権限制御、メタデータフィルタ、生成後の根拠照合を含むRAG全体の効果・安全性・速度は未測定です。BM25スコアを確率へ読み替えたり、他の検索器へそのまま移したりはできません。
再現用データ・コード・全結果
CPython 3.12.9(Windows)で実行しました。外部ライブラリやAPIは使いません。次の入力3ファイルとコードを同じフォルダーへUTF-8で保存し、python run_experiment.pyを実行すると、全結果のresults.jsonと実行環境のenvironment.jsonを出力します。OSやPythonの差で浮動小数点の末尾が変わる場合は、ハッシュだけでなく順位・判定・件数も確認してください。
config.json
{
"experiment_date_jst": "2026-10-23",
"method": "BM25 positive-IDF",
"k1": 1.2,
"b": 0.75,
"top_k": 3,
"thresholds": [
0.0,
2.0,
4.0,
8.0
],
"comparison": ">=",
"corpus_scope": "All eight fictional documents are accessible; no production data, ACL or metadata filters are implemented.",
"score_gate": "Use top-1 score; retain top-3 positive-score candidates. No candidate is a separate hold, not a score of zero.",
"tokenization": "NFKC lowercase; ASCII letters/digits sequences as whole tokens; overlapping character bigrams within each contiguous Japanese text run; punctuation discarded; no semantic understanding.",
"selection_note": "Document/query labels, k1/b and threshold grid are fixed before the first execution. This is a designed demonstration, with no held-out test split and no threshold recommendation."
}
documents.json
[
{
"id": "D01",
"title": "P-20 日本向け現行返品規程",
"text": "日本向け製品 P-20 の返品。2026年10月1日から12月31日まで適用。未開封に限り、納品後14日以内に受付窓口へ申請する。"
},
{
"id": "D02",
"title": "P-30 日本向け現行返品規程",
"text": "日本向け製品 P-30 の返品。2026年10月1日から12月31日まで適用。初期不良に限り、納品後7日以内に受付窓口へ申請する。"
},
{
"id": "D03",
"title": "P-10 旧製品返品規程",
"text": "旧製品 P-10 の返品。2026年9月30日までの販売品に適用。理由を問わず、納品後30日以内に返品を受け付ける。"
},
{
"id": "D04",
"title": "P-20 米国向け返品規程",
"text": "米国向け製品 P-20 の返品。2026年10月1日から12月31日まで適用。未開封に限り、納品後21日以内に申請する。日本向けには適用しない。"
},
{
"id": "D05",
"title": "M-10 社内貸出機器の返却",
"text": "社内貸出機器 M-10 の返却予定日が休業日の場合、次の営業日の午前10時までに総務の受付窓口へ返却する。"
},
{
"id": "D06",
"title": "M-20 社内貸出機器の返却",
"text": "社内貸出機器 M-20 の返却予定日が休業日の場合、休業日の前の営業日の午後5時までに倉庫の窓口へ返却する。"
},
{
"id": "D07",
"title": "社内出張旅費の申請期限",
"text": "国内出張旅費は帰着後10営業日以内に経理窓口へ申請する。領収書を添付する。宿泊費の上限額はこの資料では定めていない。"
},
{
"id": "D08",
"title": "P-20 日本向け保証規程",
"text": "日本向け製品 P-20 の保証期間は購入後1年間。通常使用で故障した場合の修理を対象とする。返品受付期限とは別の規程である。"
}
]
queries.json
[
{
"id": "Q01",
"query": "日本向け P-20 の2026年10月の返品期限と未開封条件は?",
"required_doc_ids": [
"D01"
],
"answerable_in_collection": true,
"label_reason": "対象・地域・時点が一致"
},
{
"id": "Q02",
"query": "日本向け P-30 の2026年10月の返品期限と初期不良条件は?",
"required_doc_ids": [
"D02"
],
"answerable_in_collection": true,
"label_reason": "対象・地域・時点が一致"
},
{
"id": "Q03",
"query": "旧製品 P-10 の2026年9月販売品は何日以内に返品できる?",
"required_doc_ids": [
"D03"
],
"answerable_in_collection": true,
"label_reason": "過去規程が正しい例"
},
{
"id": "Q04",
"query": "米国向け P-20 の2026年10月の返品期限は?",
"required_doc_ids": [
"D04"
],
"answerable_in_collection": true,
"label_reason": "日本向けとの混同に注意"
},
{
"id": "Q05",
"query": "社内貸出機器 M-10 の返却予定日が休業日なら、いつどこへ返却する?",
"required_doc_ids": [
"D05"
],
"answerable_in_collection": true,
"label_reason": "機器番号と期限・場所が必要"
},
{
"id": "Q06",
"query": "社内貸出機器 M-20 の返却予定日が休業日なら、いつどこへ返却する?",
"required_doc_ids": [
"D06"
],
"answerable_in_collection": true,
"label_reason": "M-10と異なる期限・場所"
},
{
"id": "Q07",
"query": "国内出張旅費は帰着後いつまでに申請する?",
"required_doc_ids": [
"D07"
],
"answerable_in_collection": true,
"label_reason": "上限額とは別の質問"
},
{
"id": "Q08",
"query": "日本向け P-20 の通常使用で故障した場合の保証期間は?",
"required_doc_ids": [
"D08"
],
"answerable_in_collection": true,
"label_reason": "返品とは別の規程"
},
{
"id": "Q09",
"query": "日本向け P-20 と P-30 の2026年10月の返品期限を両方知りたい",
"required_doc_ids": [
"D01",
"D02"
],
"answerable_in_collection": true,
"label_reason": "二つの根拠文書が必要"
},
{
"id": "Q10",
"query": "国内出張のレシートを出す締切は?",
"required_doc_ids": [
"D07"
],
"answerable_in_collection": true,
"label_reason": "同義語・口語の質問"
},
{
"id": "Q11",
"query": "日本向け P-40 の2026年10月の返品期限と未開封条件は?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "P-40の資料は集合内に存在しない"
},
{
"id": "Q12",
"query": "日本向け P-20 の2027年10月の返品期限と未開封条件は?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "2027年に適用する返品資料は存在しない"
},
{
"id": "Q13",
"query": "カナダ向け P-20 の2026年10月の返品期限と未開封条件は?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "カナダ向けの資料は存在しない"
},
{
"id": "Q14",
"query": "社内貸出機器 M-30 の返却予定日が休業日なら、いつどこへ返却する?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "M-30の資料は存在しない"
},
{
"id": "Q15",
"query": "国内出張旅費の宿泊費上限額はいくら?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "D07は上限額を定めていない"
},
{
"id": "Q16",
"query": "日本向け P-30 の通常使用で故障した場合の保証期間は?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "P-30の保証資料は存在しない"
},
{
"id": "Q17",
"query": "海底火山の噴出量を教えて",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "無関係な質問"
},
{
"id": "Q18",
"query": "zzqv xyzk",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "完全に未知の字句の質問"
}
]
run_experiment.py
"""Offline, synthetic BM25 retrieval-gate demonstration; no LLM or network I/O.
Usage: python run_experiment.py
Only Python's standard library is required. Input files are never modified.
"""
import collections
import hashlib
import json
import math
import platform
from pathlib import Path
import re
import unicodedata
ROOT = Path(__file__).resolve().parent
def load(name):
return json.loads((ROOT / name).read_text(encoding="utf-8"))
def tokenize(text):
text = unicodedata.normalize("NFKC", text).lower()
tokens = []
for part in re.findall(r"[a-z0-9]+|[\u3040-\u30ff\u3400-\u9fff]+", text):
if re.fullmatch(r"[a-z0-9]+", part) or len(part) == 1:
tokens.append(part)
else:
tokens.extend(part[i:i + 2] for i in range(len(part) - 1))
return tokens
def score_documents(query, documents, cfg):
counts = [collections.Counter(tokenize(d["title"] + " " + d["text"]))
for d in documents]
lengths = [sum(c.values()) for c in counts]
average = sum(lengths) / len(lengths)
document_frequency = collections.Counter(t for c in counts for t in c)
ranking = []
# Binary query term frequency: a repeated query token is counted once.
for d, c, length in zip(documents, counts, lengths):
score = 0.0
for term in sorted(set(tokenize(query))):
frequency = c[term]
if frequency == 0:
continue
df = document_frequency[term]
idf = math.log(1 + (len(documents) - df + 0.5) / (df + 0.5))
denominator = frequency + cfg["k1"] * (
1 - cfg["b"] + cfg["b"] * length / average)
score += idf * frequency * (cfg["k1"] + 1) / denominator
if score > 0:
ranking.append({"doc_id": d["id"], "score": score})
# Stable and explicitly defined tie breaker.
return sorted(ranking, key=lambda item: (-item["score"], item["doc_id"]))
def main():
documents, queries, cfg = load("documents.json"), load("queries.json"), load("config.json")
doc_ids = {d["id"] for d in documents}
assert len(doc_ids) == len(documents)
assert len({q["id"] for q in queries}) == len(queries)
assert all(set(q["required_doc_ids"]) <= doc_ids for q in queries)
# Retrieve once. Threshold changes never change the input or ranking.
rows = []
for q in queries:
ranking = score_documents(q["query"], documents, cfg)
candidates = ranking[:cfg["top_k"]]
top_score = candidates[0]["score"] if candidates else None
required = set(q["required_doc_ids"])
covered = bool(required) and required <= {c["doc_id"] for c in candidates}
rows.append({**q, "top_score": top_score, "candidates": candidates,
"required_documents_covered_in_top_k": covered})
summaries = []
for threshold in cfg["thresholds"]:
decisions = []
for q in rows:
passed = q["top_score"] is not None and q["top_score"] >= threshold
decisions.append({"query_id": q["id"], "passed": passed,
"reason": "pass" if passed else (
"no_candidates" if q["top_score"] is None else "below_threshold")})
passed_ids = {d["query_id"] for d in decisions if d["passed"]}
answerable = [q for q in rows if q["answerable_in_collection"]]
unanswerable = [q for q in rows if not q["answerable_in_collection"]]
false_pass_ids = [q["id"] for q in unanswerable if q["id"] in passed_ids]
excess_hold_ids = [q["id"] for q in answerable if q["id"] not in passed_ids]
insufficient_ids = [q["id"] for q in rows if q["id"] in passed_ids
and not q["required_documents_covered_in_top_k"]]
s = {"threshold": threshold, "query_count": len(rows),
"passed": len(passed_ids), "held": len(rows) - len(passed_ids),
"answerable_count": len(answerable), "unanswerable_count": len(unanswerable),
"false_pass_count": len(false_pass_ids), "false_pass_ids": false_pass_ids,
"excess_hold_count": len(excess_hold_ids), "excess_hold_ids": excess_hold_ids,
"passed_without_required_evidence_count": len(insufficient_ids),
"passed_without_required_evidence_ids": insufficient_ids,
"passed_with_required_evidence_count": len(passed_ids) - len(insufficient_ids),
"decisions": decisions}
assert s["passed"] + s["held"] == len(rows)
assert s["passed_without_required_evidence_count"] + s["passed_with_required_evidence_count"] == s["passed"]
summaries.append(s)
for low, high in zip(summaries, summaries[1:]):
assert high["passed"] <= low["passed"]
assert high["false_pass_count"] <= low["false_pass_count"]
assert high["excess_hold_count"] >= low["excess_hold_count"]
assert score_documents("zzqv xyzk", documents, cfg) == []
assert tokenize("P-20") == ["p", "20"]
result = {"config": cfg, "documents_count": len(documents), "queries": rows,
"summaries": summaries,
"metrics_note": "This measures retrieval and a gate only; no answers were generated or graded. Evidence coverage uses prewritten synthetic labels, not an automated semantic checker."}
(ROOT / "results.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", newline="\n")
# Environment is separate so deterministic results do not depend on timestamps.
(ROOT / "environment.json").write_text(json.dumps({"python": platform.python_version(),
"implementation": platform.python_implementation(), "system": platform.system()}, indent=2) + "\n", encoding="utf-8", newline="\n")
print("threshold | passed/18 | false_pass/8 | excess_hold/10 | passed_no_evidence")
for s in summaries:
print(s["threshold"], s["passed"], s["false_pass_count"], s["excess_hold_count"], s["passed_without_required_evidence_count"], sep=" | ")
print("results.json SHA256:", hashlib.sha256((ROOT / "results.json").read_bytes()).hexdigest())
if __name__ == "__main__":
main()
results-summary.txt
実行結果(2026-10-23 JST)
LLM/生成モデルは未使用。母数18問(根拠あり10、なし8)。
閾値 | 通過 | 保留 | 誤通過/8 | 過剰保留/10 | 通過のうち必要文書を含む
0 | 16 | 2 | 6 | 0 | 10
2 | 16 | 2 | 6 | 0 | 10
4 | 16 | 2 | 6 | 0 | 10
8 | 14 | 4 | 5 | 1 | 9
各質問の実結果(一位スコア小数点以下6桁表示。内部計算は丸め前)
Q01 | 9.811527 | D01,D04,D08 | 必要根拠=D01 | 日本向け P-20 の2026年10月の返品期限と未開封条件は?
Q02 | 13.365760 | D02,D08,D01 | 必要根拠=D02 | 日本向け P-30 の2026年10月の返品期限と初期不良条件は?
Q03 | 15.887716 | D03,D01,D02 | 必要根拠=D03 | 旧製品 P-10 の2026年9月販売品は何日以内に返品できる?
Q04 | 10.235241 | D04,D01,D02 | 必要根拠=D04 | 米国向け P-20 の2026年10月の返品期限は?
Q05 | 25.221300 | D05,D06,D07 | 必要根拠=D05 | 社内貸出機器 M-10 の返却予定日が休業日なら、いつどこへ返却する?
Q06 | 26.025947 | D06,D05,D07 | 必要根拠=D06 | 社内貸出機器 M-20 の返却予定日が休業日なら、いつどこへ返却する?
Q07 | 20.055652 | D07,D04,D01 | 必要根拠=D07 | 国内出張旅費は帰着後いつまでに申請する?
Q08 | 32.032532 | D08,D01,D04 | 必要根拠=D08 | 日本向け P-20 の通常使用で故障した場合の保証期間は?
Q09 | 8.000504 | D02,D01,D04 | 必要根拠=D01,D02 | 日本向け P-20 と P-30 の2026年10月の返品期限を両方知りたい
Q10 | 6.522068 | D07 | 必要根拠=D07 | 国内出張のレシートを出す締切は?
Q11 | 8.854764 | D01,D04,D08 | 必要根拠=集合内になし | 日本向け P-40 の2026年10月の返品期限と未開封条件は?
Q12 | 9.114474 | D01,D04,D08 | 必要根拠=集合内になし | 日本向け P-20 の2027年10月の返品期限と未開封条件は?
Q13 | 7.898000 | D01,D04,D08 | 必要根拠=集合内になし | カナダ向け P-20 の2026年10月の返品期限と未開封条件は?
Q14 | 25.069183 | D06,D05,D02 | 必要根拠=集合内になし | 社内貸出機器 M-30 の返却予定日が休業日なら、いつどこへ返却する?
Q15 | 22.339609 | D07 | 必要根拠=集合内になし | 国内出張旅費の宿泊費上限額はいくら?
Q16 | 31.099818 | D08,D02,D01 | 必要根拠=集合内になし | 日本向け P-30 の通常使用で故障した場合の保証期間は?
Q17 | 候補なし | - | 必要根拠=集合内になし | 海底火山の噴出量を教えて
Q18 | 候補なし | - | 必要根拠=集合内になし | zzqv xyzk
results.json
{
"config": {
"experiment_date_jst": "2026-10-23",
"method": "BM25 positive-IDF",
"k1": 1.2,
"b": 0.75,
"top_k": 3,
"thresholds": [
0.0,
2.0,
4.0,
8.0
],
"comparison": ">=",
"corpus_scope": "All eight fictional documents are accessible; no production data, ACL or metadata filters are implemented.",
"score_gate": "Use top-1 score; retain top-3 positive-score candidates. No candidate is a separate hold, not a score of zero.",
"tokenization": "NFKC lowercase; ASCII letters/digits sequences as whole tokens; overlapping character bigrams within each contiguous Japanese text run; punctuation discarded; no semantic understanding.",
"selection_note": "Document/query labels, k1/b and threshold grid are fixed before the first execution. This is a designed demonstration, with no held-out test split and no threshold recommendation."
},
"documents_count": 8,
"queries": [
{
"id": "Q01",
"query": "日本向け P-20 の2026年10月の返品期限と未開封条件は?",
"required_doc_ids": [
"D01"
],
"answerable_in_collection": true,
"label_reason": "対象・地域・時点が一致",
"top_score": 9.811527319154932,
"candidates": [
{
"doc_id": "D01",
"score": 9.811527319154932
},
{
"doc_id": "D04",
"score": 9.247086426472725
},
{
"doc_id": "D08",
"score": 7.848983131873796
}
],
"required_documents_covered_in_top_k": true
},
{
"id": "Q02",
"query": "日本向け P-30 の2026年10月の返品期限と初期不良条件は?",
"required_doc_ids": [
"D02"
],
"answerable_in_collection": true,
"label_reason": "対象・地域・時点が一致",
"top_score": 13.365760209738598,
"candidates": [
{
"doc_id": "D02",
"score": 13.365760209738598
},
{
"doc_id": "D08",
"score": 6.916268953424469
},
{
"doc_id": "D01",
"score": 6.2784585178676044
}
],
"required_documents_covered_in_top_k": true
},
{
"id": "Q03",
"query": "旧製品 P-10 の2026年9月販売品は何日以内に返品できる?",
"required_doc_ids": [
"D03"
],
"answerable_in_collection": true,
"label_reason": "過去規程が正しい例",
"top_score": 15.887715786152917,
"candidates": [
{
"doc_id": "D03",
"score": 15.887715786152917
},
{
"doc_id": "D01",
"score": 5.061919052041104
},
{
"doc_id": "D02",
"score": 5.02733156819426
}
],
"required_documents_covered_in_top_k": true
},
{
"id": "Q04",
"query": "米国向け P-20 の2026年10月の返品期限は?",
"required_doc_ids": [
"D04"
],
"answerable_in_collection": true,
"label_reason": "日本向けとの混同に注意",
"top_score": 10.235241247977747,
"candidates": [
{
"doc_id": "D04",
"score": 10.235241247977747
},
{
"doc_id": "D01",
"score": 5.32169498372126
},
{
"doc_id": "D02",
"score": 4.337769144529427
}
],
"required_documents_covered_in_top_k": true
},
{
"id": "Q05",
"query": "社内貸出機器 M-10 の返却予定日が休業日なら、いつどこへ返却する?",
"required_doc_ids": [
"D05"
],
"answerable_in_collection": true,
"label_reason": "機器番号と期限・場所が必要",
"top_score": 25.2213002400404,
"candidates": [
{
"doc_id": "D05",
"score": 25.2213002400404
},
{
"doc_id": "D06",
"score": 25.069183083532423
},
{
"doc_id": "D07",
"score": 2.3748691019074784
}
],
"required_documents_covered_in_top_k": true
},
{
"id": "Q06",
"query": "社内貸出機器 M-20 の返却予定日が休業日なら、いつどこへ返却する?",
"required_doc_ids": [
"D06"
],
"answerable_in_collection": true,
"label_reason": "M-10と異なる期限・場所",
"top_score": 26.025946617678766,
"candidates": [
{
"doc_id": "D06",
"score": 26.025946617678766
},
{
"doc_id": "D05",
"score": 24.704358227084896
},
{
"doc_id": "D07",
"score": 2.0617304709320363
}
],
"required_documents_covered_in_top_k": true
},
{
"id": "Q07",
"query": "国内出張旅費は帰着後いつまでに申請する?",
"required_doc_ids": [
"D07"
],
"answerable_in_collection": true,
"label_reason": "上限額とは別の質問",
"top_score": 20.05565245973181,
"candidates": [
{
"doc_id": "D07",
"score": 20.05565245973181
},
{
"doc_id": "D04",
"score": 3.6248568566123103
},
{
"doc_id": "D01",
"score": 1.9047122976002804
}
],
"required_documents_covered_in_top_k": true
},
{
"id": "Q08",
"query": "日本向け P-20 の通常使用で故障した場合の保証期間は?",
"required_doc_ids": [
"D08"
],
"answerable_in_collection": true,
"label_reason": "返品とは別の規程",
"top_score": 32.032532443123,
"candidates": [
{
"doc_id": "D08",
"score": 32.032532443123
},
{
"doc_id": "D01",
"score": 4.506828296505496
},
{
"doc_id": "D04",
"score": 4.054701602572698
}
],
"required_documents_covered_in_top_k": true
},
{
"id": "Q09",
"query": "日本向け P-20 と P-30 の2026年10月の返品期限を両方知りたい",
"required_doc_ids": [
"D01",
"D02"
],
"answerable_in_collection": true,
"label_reason": "二つの根拠文書が必要",
"top_score": 8.000504073790324,
"candidates": [
{
"doc_id": "D02",
"score": 8.000504073790324
},
{
"doc_id": "D01",
"score": 7.235222052013949
},
{
"doc_id": "D04",
"score": 6.727576621436503
}
],
"required_documents_covered_in_top_k": true
},
{
"id": "Q10",
"query": "国内出張のレシートを出す締切は?",
"required_doc_ids": [
"D07"
],
"answerable_in_collection": true,
"label_reason": "同義語・口語の質問",
"top_score": 6.522067637102344,
"candidates": [
{
"doc_id": "D07",
"score": 6.522067637102344
}
],
"required_documents_covered_in_top_k": true
},
{
"id": "Q11",
"query": "日本向け P-40 の2026年10月の返品期限と未開封条件は?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "P-40の資料は集合内に存在しない",
"top_score": 8.854763785008588,
"candidates": [
{
"doc_id": "D01",
"score": 8.854763785008588
},
{
"doc_id": "D04",
"score": 8.304899061276558
},
{
"doc_id": "D08",
"score": 6.916268953424469
}
],
"required_documents_covered_in_top_k": false
},
{
"id": "Q12",
"query": "日本向け P-20 の2027年10月の返品期限と未開封条件は?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "2027年に適用する返品資料は存在しない",
"top_score": 9.11447386143466,
"candidates": [
{
"doc_id": "D01",
"score": 9.11447386143466
},
{
"doc_id": "D04",
"score": 8.565399732057983
},
{
"doc_id": "D08",
"score": 7.848983131873796
}
],
"required_documents_covered_in_top_k": false
},
{
"id": "Q13",
"query": "カナダ向け P-20 の2026年10月の返品期限と未開封条件は?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "カナダ向けの資料は存在しない",
"top_score": 7.898000250862243,
"candidates": [
{
"doc_id": "D01",
"score": 7.898000250862243
},
{
"doc_id": "D04",
"score": 7.883713037643243
},
{
"doc_id": "D08",
"score": 5.983554774975142
}
],
"required_documents_covered_in_top_k": false
},
{
"id": "Q14",
"query": "社内貸出機器 M-30 の返却予定日が休業日なら、いつどこへ返却する?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "M-30の資料は存在しない",
"top_score": 25.069183083532423,
"candidates": [
{
"doc_id": "D06",
"score": 25.069183083532423
},
{
"doc_id": "D05",
"score": 24.704358227084896
},
{
"doc_id": "D02",
"score": 2.2658222781248516
}
],
"required_documents_covered_in_top_k": false
},
{
"id": "Q15",
"query": "国内出張旅費の宿泊費上限額はいくら?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "D07は上限額を定めていない",
"top_score": 22.33960886299606,
"candidates": [
{
"doc_id": "D07",
"score": 22.33960886299606
}
],
"required_documents_covered_in_top_k": false
},
{
"id": "Q16",
"query": "日本向け P-30 の通常使用で故障した場合の保証期間は?",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "P-30の保証資料は存在しない",
"top_score": 31.099818264673672,
"candidates": [
{
"doc_id": "D08",
"score": 31.099818264673672
},
{
"doc_id": "D02",
"score": 5.290876534259898
},
{
"doc_id": "D01",
"score": 3.5500647623591517
}
],
"required_documents_covered_in_top_k": false
},
{
"id": "Q17",
"query": "海底火山の噴出量を教えて",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "無関係な質問",
"top_score": null,
"candidates": [],
"required_documents_covered_in_top_k": false
},
{
"id": "Q18",
"query": "zzqv xyzk",
"required_doc_ids": [],
"answerable_in_collection": false,
"label_reason": "完全に未知の字句の質問",
"top_score": null,
"candidates": [],
"required_documents_covered_in_top_k": false
}
],
"summaries": [
{
"threshold": 0.0,
"query_count": 18,
"passed": 16,
"held": 2,
"answerable_count": 10,
"unanswerable_count": 8,
"false_pass_count": 6,
"false_pass_ids": [
"Q11",
"Q12",
"Q13",
"Q14",
"Q15",
"Q16"
],
"excess_hold_count": 0,
"excess_hold_ids": [],
"passed_without_required_evidence_count": 6,
"passed_without_required_evidence_ids": [
"Q11",
"Q12",
"Q13",
"Q14",
"Q15",
"Q16"
],
"passed_with_required_evidence_count": 10,
"decisions": [
{
"query_id": "Q01",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q02",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q03",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q04",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q05",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q06",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q07",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q08",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q09",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q10",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q11",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q12",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q13",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q14",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q15",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q16",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q17",
"passed": false,
"reason": "no_candidates"
},
{
"query_id": "Q18",
"passed": false,
"reason": "no_candidates"
}
]
},
{
"threshold": 2.0,
"query_count": 18,
"passed": 16,
"held": 2,
"answerable_count": 10,
"unanswerable_count": 8,
"false_pass_count": 6,
"false_pass_ids": [
"Q11",
"Q12",
"Q13",
"Q14",
"Q15",
"Q16"
],
"excess_hold_count": 0,
"excess_hold_ids": [],
"passed_without_required_evidence_count": 6,
"passed_without_required_evidence_ids": [
"Q11",
"Q12",
"Q13",
"Q14",
"Q15",
"Q16"
],
"passed_with_required_evidence_count": 10,
"decisions": [
{
"query_id": "Q01",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q02",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q03",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q04",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q05",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q06",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q07",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q08",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q09",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q10",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q11",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q12",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q13",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q14",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q15",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q16",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q17",
"passed": false,
"reason": "no_candidates"
},
{
"query_id": "Q18",
"passed": false,
"reason": "no_candidates"
}
]
},
{
"threshold": 4.0,
"query_count": 18,
"passed": 16,
"held": 2,
"answerable_count": 10,
"unanswerable_count": 8,
"false_pass_count": 6,
"false_pass_ids": [
"Q11",
"Q12",
"Q13",
"Q14",
"Q15",
"Q16"
],
"excess_hold_count": 0,
"excess_hold_ids": [],
"passed_without_required_evidence_count": 6,
"passed_without_required_evidence_ids": [
"Q11",
"Q12",
"Q13",
"Q14",
"Q15",
"Q16"
],
"passed_with_required_evidence_count": 10,
"decisions": [
{
"query_id": "Q01",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q02",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q03",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q04",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q05",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q06",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q07",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q08",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q09",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q10",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q11",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q12",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q13",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q14",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q15",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q16",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q17",
"passed": false,
"reason": "no_candidates"
},
{
"query_id": "Q18",
"passed": false,
"reason": "no_candidates"
}
]
},
{
"threshold": 8.0,
"query_count": 18,
"passed": 14,
"held": 4,
"answerable_count": 10,
"unanswerable_count": 8,
"false_pass_count": 5,
"false_pass_ids": [
"Q11",
"Q12",
"Q14",
"Q15",
"Q16"
],
"excess_hold_count": 1,
"excess_hold_ids": [
"Q10"
],
"passed_without_required_evidence_count": 5,
"passed_without_required_evidence_ids": [
"Q11",
"Q12",
"Q14",
"Q15",
"Q16"
],
"passed_with_required_evidence_count": 9,
"decisions": [
{
"query_id": "Q01",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q02",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q03",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q04",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q05",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q06",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q07",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q08",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q09",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q10",
"passed": false,
"reason": "below_threshold"
},
{
"query_id": "Q11",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q12",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q13",
"passed": false,
"reason": "below_threshold"
},
{
"query_id": "Q14",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q15",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q16",
"passed": true,
"reason": "pass"
},
{
"query_id": "Q17",
"passed": false,
"reason": "no_candidates"
},
{
"query_id": "Q18",
"passed": false,
"reason": "no_candidates"
}
]
}
],
"metrics_note": "This measures retrieval and a gate only; no answers were generated or graded. Evidence coverage uses prewritten synthetic labels, not an automated semantic checker."
}
今回のresults.jsonのSHA256:967e0a756586325a811e12ee192f8499aba2e085167085c79d398897f4e9e9cb

