Step 0
環境を用意する
なぜ
一度だけです。仮想環境は labs/rag-lab/.venv に作り、システムの Python と分けます。
pip install -e . の行は、このあと毎回 PYTHONPATH を設定せずに python -m rag_lab と書けるようにするためです。
お使いの環境が 3.11 でなければ、2 行目の python3.11 を自分のものに替えてください(3.10 以上ならどれでもよく、たいていは python3 です)。以下の出力はすべて 3.11.15 のものです。
コマンド
cd labs/rag-lab
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -e .ここを読む
- 依存関係のバージョンは
requirements.txtにすべて固定してあります。半年後にもう一度インストールしても、同じ wheel が入り、同じベクトルが計算されます。 - モデルはまだダウンロードされていません。それを必要とする最初のコマンドの中で、約 470 MB をダウンロードします。
自分で変えてみる
このあとのコマンドはすべて、この仮想環境が有効になっている前提です。新しいターミナルを開いたら、まず source labs/rag-lab/.venv/bin/activate を実行してください。
Step 1
Knowledge Base を読み込む
なぜ
まず、ナレッジベースと評価用の質問そのものが正しいことを確かめます。セクション番号が重複していないこと、どの質問の Ground Truth も実在するセクションを指していること。
このステップはモデルを読み込まず、ネットワークも使わず、1 秒以内に終わります。
コマンド
python -m rag_lab validate見えるはずのもの
knowledge base zh 7 documents, 61 sections
allergens.md 7 sections 1296 chars 过敏原与交叉接触说明
delivery.md 9 sections 1243 chars 外卖配送规则
membership.md 10 sections 1154 chars 会员制度问答
menu.md 8 sections 1430 chars 菜单与可调整项
promotion-policy.md 8 sections 1252 chars 优惠与券规则
refund-policy.md 11 sections 1777 chars 退款与补偿政策
store-operations.md 8 sections 1407 chars 门店操作手册
query set 11 queries, schema rag-lab.queries.v1
every evidence section resolves; ids unique; concepts agree. OKここを読む
- 7 件の文書、61 セクション。「セクション」とは stable semantic section のことで、ナレッジベースの各セクションには、切り方が変わっても変わらない番号があります。たとえば
refund.missing.combo-core-item。 - Ground Truth が結びついているのはこの番号で、チャンクではありません。だからこのあと切り方をどう変えても、判定の基準は動きません。
自分で変えてみる
content/rag-lab/evaluation/queries.json のどれかの evidence_sections を、存在しない番号に変えてもう一度走らせてみてください。名指しで教えてくれます。
content/rag-lab/knowledge/zh/中国語の文書 7 件。セクション番号は見えない HTML コメントに書いてあるcontent/rag-lab/evaluation/queries.json11 問、期待される答え、根拠のセクション、紛らわしいセクション
Step 2
文書を測る:chunk size はどこから来るのか
なぜ
「どれくらいの大きさで切るか」は勘で決めるものではありません。まずこのモデル自身の tokenizerで実際の文書を測ってから、候補の値を決めます。
このステップではモデルをダウンロードします(初回は 1 分ほど)。以降はすべてローカルです。
コマンド
python -m rag_lab stats見えるはずのもの
tokenizer: XLMRobertaTokenizer (intfloat/multilingual-e5-small)
model window: 512 tokens, 384 dimensions
document chars tokens sections section tokens (min/med/max)
allergens.md 1296 976 7 73 / 131 / 183
delivery.md 1243 864 9 63 / 102 / 120
membership.md 1154 803 10 65 / 76 / 108
menu.md 1430 1102 8 90 / 125 / 256
promotion-policy.md 1252 896 8 75 / 116 / 144
refund-policy.md 1777 1308 11 86 / 116 / 156
store-operations.md 1407 1025 8 92 / 128 / 160
61 sections, 6974 tokens total
section tokens: min 63, median 111, mean 114, max 256
chars per token: 1.37ここを読む
- ルール 1 件の中央値は 111 tokens、最短は 63、最長は 256(メニューの表)です。
- だからこのあとの候補値はこう選んでいます。64 はルール 1 件よりはっきり小さく、192 は 1 件半ほど、448 は 4 件ほどでモデルの 512 のウィンドウに近い値です。「よく見る 500」ではありません。
- 1 token あたり 1.37 文字 —— 中国語はおおよそ 1 文字 1 token ですが、「おおよそ」は単位ではないので、全体を通して token で数えます。
自分で変えてみる
このナレッジベースは小さめです。自分の文書で stats を走らせてみると、中央値はまったく違い、候補の値も変わります。
Step 3
Fixed chunking:文書を切る
なぜ
まずは切るだけで、検索はしません。固定長の物差しがどこに落ちるかを見ます。
--document は表示を絞るだけで、インデックスはナレッジベース全体のままです。
コマンド
python -m rag_lab chunk --strategy fixed --chunk-size 64 --overlap 0 --document refund見えるはずのもの
strategy fixed-64-0 {"id": "fixed-64-0", "splitter": "fixed", "unit": "tokens", "chunk_size": 64, "overlap": 0}
113 chunks, tokens min 2 / median 65 / max 66
chunks holding more than one section: 52
fixed-64-0:refund-policy#000 64t ## 适用范围
sections: refund.scope
fixed-64-0:refund-policy#001 65t 规定"退多少钱、由谁发起"。配送环节本身的延误、取消与补偿,按《外卖配送规则》处理;因原
sections: refund.scope
fixed-64-0:refund-policy#002 65t 对应规则判断是否成立,再按本政策计算金额,不重复退款。
sections: refund.scope, refund.missing.standard
fixed-64-0:refund-policy#003 65t 付金额。实付金额指该商品在本单中实际承担的价格,已含平摊后的优惠。顾客可以选择退款,也可
sections: refund.missing.standard
fixed-64-0:refund-policy#004 65t ,订单中其余已送达的商品不退。顾客要求把整单退掉时,按下文"整单退款"判断是否成立。ここを読む
- ナレッジベース全体が 113 チャンクになり、そのうち 52 チャンクは複数のルールにまたがっています。
- 3 番目のチャンク(
#002)を見てください。始まりは前のルールの半分の文で、終わりは次のルールに入り込んでいます。切れ目はルールを知りません。 --fullを付けると、チャンクを全文で出力します。
自分で変えてみる
--chunk-size を 448 にすると、同じファイルが 21 チャンクから 3 チャンクになります。
python -m rag_lab chunk --strategy fixed --chunk-size 448 --overlap 0 --document refundlabs/rag-lab/src/rag_lab/chunking.py分割器が二つ、合わせて二百行足らず
Step 4
Embedding + 検索:一つのコマンドの中の四つの仕事
なぜ
次のコマンドは四つのことを順にやります。コードの中でも四つの部分に分かれています。
① 各チャンクに passage: のプレフィックスを付けてモデルに通し、384 次元のベクトルを得る(embedding.py)。② 質問に query: のプレフィックスを付けて、同じようにベクトルにする。③ ベクトルはすべて L2 正規化してあるので、コサイン類似度は内積一回で済む(retrieval.py)。④ 並べ替えて、上位 3 件を取る。
プレフィックスは飾りではなく、e5 ファミリーのモデルが求める入力の形式です。ベクトルは results/.cache/ にキャッシュされるので、chunk size を変えても、変わっていない文章は計算し直しません。
labs/rag-lab/src/rag_lab/embedding.pyモデル、tokenizer、プレフィックス、キャッシュ、provenancelabs/rag-lab/src/rag_lab/retrieval.pynumpy の行列一つと内積一回で、ベクトルデータベースはなしlabs/rag-lab/configs/experiment.json固定している側:モデル、revision、top_k
Step 5
最初の Top-K:根拠が切られた姿
なぜ
質問はゲームのシナリオ 1 です。セットのメインドリンクが入っていなかったとき、注文全体を返金できるか。答えるには二つのルールが要ります —— 通常の場合と、セットの例外です。
コマンド
python -m rag_lab retrieve --strategy fixed --chunk-size 64 --overlap 0 --query-id Q01-zh見えるはずのもの
strategy fixed-64-0 {"id": "fixed-64-0", "splitter": "fixed", "unit": "tokens", "chunk_size": 64, "overlap": 0}
index 113 chunks
query Q01-zh 套餐里的主饮没送到,小吃都收到了,能不能把整个套餐退掉?
top_k 3
#1 0.9393 fixed-64-0:refund-policy#005 (65 tokens)
sections: refund.missing.combo-core-item
主饮漏送时,不按单件漏送处理。主饮是套餐的核心商品,缺少主饮后剩余附件不构成一份可交付的套餐,因此顾客有权要求退还整个套餐的实付金额,已送达的附件无需退回。顾客也可以选择只补做
#2 0.9278 fixed-64-0:store-operations#007 (65 tokens)
sections: ops.stockout.combo, ops.stockout.no-confirm
差价。顾客不接受任何方案的,整份套餐按《退款与补偿政策》的整单情形退款,已制作完成的附件不要求退回。
套餐主饮本身售罄的,不做替换,直接进入退款流程。
## 联系不上顾客时
#3 0.9275 fixed-64-0:refund-policy#004 (65 tokens)
sections: refund.missing.standard, refund.missing.combo-core-item
,订单中其余已送达的商品不退。顾客要求把整单退掉时,按下文"整单退款"判断是否成立。
## 套餐主饮漏送
套餐按整体定价,其中的饮品称为主饮,小吃、配料杯等称为附件。
套餐
evidence needed: refund.missing.standard, refund.missing.combo-core-item
✗ refund.missing.standard 22% in top-3 (not complete in any chunk)
✓ refund.missing.combo-core-item 61% in top-3 (not complete in any chunk)
evidence coverage@3 50% complete False retrieved 195 tokens redundancy 0% signal 66%ここを読む
- 1 位は「顾客也可以选择只补做」(「お客さまは作り直しだけを選ぶこともできる」)まで読んで途切れています —— これは 64 tokens の終わりで、文の終わりではありません。
- 最後の三行が判定です。基本ルールは 22% しか取れず、例外は 61%、
complete False。 signalは取れた文章のうち根拠のセクションに属する割合、redundancyはそのうち重複している割合です。今回は重なりがないので 0% です。
自分で変えてみる
別の質問を試しましょう。--query-id Q06-zh(ピーナッツアレルギーの質問で、根拠が三つ要ります)。自分の質問を直接聞くこともできます:--query "生日券过期了还能补发吗"(「期限切れの誕生日クーポンは再発行できますか」)。ただしその場合、判定に使える Ground Truth はありません。
Step 6
chunk size を変える
なぜ
数字を一つだけ変え、ほかはすべてそのまま —— 同じ質問、同じモデル、同じ top-k です。
コマンド
python -m rag_lab retrieve --strategy fixed --chunk-size 192 --overlap 0 --query-id Q01-zh見えるはずのもの
前に上位 3 件の原文がありますが、ここでは最後の判定の部分だけを載せています。
evidence needed: refund.missing.standard, refund.missing.combo-core-item
✓ refund.missing.standard 69% in top-3 (first complete at rank 2)
✓ refund.missing.combo-core-item 100% in top-3 (first complete at rank 2)
evidence coverage@3 100% complete True retrieved 579 tokens redundancy 0% signal 45%ここを読む
- 今回は二つのルールが同じチャンク(2 位)に入り、
complete Trueになりました。 - その代償は同じ行にあります。コンテキストは 195 tokens から 579 になり、signal は 66% から 45% に下がりました。
自分で変えてみる
448 も試してください。complete は True のまま、コンテキストはさらに増え、signal はさらに下がります。どんな質問にも最良の数字はありません —— ステップ 10 で 11 問をまとめて走らせます。
python -m rag_lab retrieve --strategy fixed --chunk-size 448 --overlap 0 --query-id Q01-zhStep 7
overlap を入れる
なぜ
別の質問にします。デリバリーが 40 分遅れていて、配達員がまだ受け取りに来ていないとき、キャンセルできるか。答えは二文で、定義(30 分を超えたら重大な遅延)と結論(受け取り前ならキャンセルできる)です。
まず重なりなしで一度、次に 32 tokens の重なりを足してもう一度走らせます。
コマンド
python -m rag_lab retrieve --strategy fixed --chunk-size 96 --overlap 0 --query-id Q03-zh見えるはずのもの
前に上位 3 件の原文がありますが、ここでは最後の判定の部分だけを載せています。
evidence needed: delivery.delay.severe, delivery.delay.cancel-before-pickup
✗ delivery.delay.severe 45% in top-3 (not complete in any chunk)
✓ delivery.delay.cancel-before-pickup 100% in top-3 (not complete in any chunk)
evidence coverage@3 50% complete False retrieved 291 tokens redundancy 0% signal 50%ここを読む
complete False:定義のセクションは 45% しか取れていません。切れ目がその途中に落ちているからです。
自分で変えてみる
--overlap を 32 にして、もう一度走らせます。
python -m rag_lab retrieve --strategy fixed --chunk-size 96 --overlap 32 --query-id Q03-zhevidence needed: delivery.delay.severe, delivery.delay.cancel-before-pickup
✓ delivery.delay.severe 84% in top-3 (first complete at rank 1)
✓ delivery.delay.cancel-before-pickup 68% in top-3 (first complete at rank 4)
evidence coverage@3 100% complete True retrieved 291 tokens redundancy 0% signal 49%
also retrieved (listed as not-the-answer): delivery.delay.minorcomplete True。1 位のチャンクに定義と結論の両方が入り、類似度も 0.9361 から 0.9399 に上がりました。コンテキストは長くなっていません —— 今も 291 tokens で、中身が正しくなっただけです。
Step 8
overlap をやりすぎる
なぜ
前のステップの結論は「重なりは大きいほどよい」のように聞こえます。もう少し増やして、32 から 48 にし、ほかは変えません。
コマンド
python -m rag_lab retrieve --strategy fixed --chunk-size 96 --overlap 48 --query-id Q03-zh見えるはずのもの
前に上位 3 件の原文がありますが、ここでは最後の判定の部分だけを載せています。
evidence needed: delivery.delay.severe, delivery.delay.cancel-before-pickup
✗ delivery.delay.severe 0% in top-3 (first complete at rank 5)
✓ delivery.delay.cancel-before-pickup 92% in top-3 (first complete at rank 3)
evidence coverage@3 50% complete False retrieved 292 tokens redundancy 18% signal 41%ここを読む
complete False—— 同じ質問に、また答えられなくなりました。redundancy 18%:1 位と 3 位は隣り合う二つのチャンクで、半分以上が同じ文章です。上位 3 件には、違う内容が二つしか入っていません。- 定義のセクションは 5 位まで落ちました。ずっとナレッジベースにあったのに、上位 3 件に入らなかっただけです。
- インデックスも増えます。同じ 61 セクションの知識で、重なり 0 は 76 チャンク、32 は 108 チャンク、48 は 141 チャンクです。
自分で変えてみる
続けて 64 を試してください。この質問ではまた complete True に戻ります —— overlap は単調なつまみではありません。0 / 32 / 48 / 64 の四つの値をすべて走らせれば、どんな一言の結論よりはっきりします。
python -m rag_lab retrieve --strategy fixed --chunk-size 96 --overlap 64 --query-id Q03-zhStep 9
structure-aware:切れ目を文書自身に決めさせる
なぜ
別の切り方です。token を数えず、Markdown の見出しで切り、表とそのすぐ後ろの注記を同じところに残します。
この分割器はセクション番号を読めません。固定長の分割器とまったく同じ入力、つまり番号コメントを除いた本文を受け取ります。そうでなければ、答えに沿って切ることになってしまいます。
コマンド
python -m rag_lab chunk --preset structure-aware --document menu見えるはずのもの
strategy structure-aware {"id": "structure-aware", "splitter": "structure", "unit": "tokens", "label": "按结构切", "role": "boundary", "max_tokens": 480, "min_tokens": 40, "note": "Boundaries come from the Markdown — headings, paragraphs, a table and its note. The ceiling is the model's window, not a target size."}
60 chunks, tokens min 63 / median 111 / max 402
chunks holding more than one section: 1
structure-aware:menu#000 90t ## 适用范围
sections: menu.scope
structure-aware:menu#001 402t ## 主要饮品
sections: menu.drinks.table, menu.drinks.table-notes
structure-aware:menu#002 123t ## 甜度档位ここを読む
- メニューは 7 チャンクに切られ、2 番目のチャンクは 402 tokens です —— 価格表の全体と、その下の注記が同じチャンクに入っています。
自分で変えてみる
この切り方で「黒糖タピオカミルクを砂糖ゼロにできるか」を聞いてみます。
python -m rag_lab retrieve --preset structure-aware --query-id Q09-zhevidence needed: menu.drinks.table, menu.drinks.table-notes
✓ menu.drinks.table 100% in top-3 (first complete at rank 1)
✓ menu.drinks.table-notes 100% in top-3 (first complete at rank 1)
evidence coverage@3 100% complete True retrieved 653 tokens redundancy 0% signal 63%
also retrieved (listed as not-the-answer): menu.sugar-levelscomplete True、表も注記も 100% 取れました。同じ質問は、192 tokens の固定の切り方では complete False でした —— 切れ目が表の真ん中に落ちたからです。
labs/rag-lab/configs/strategies.json切り方は八通りで、三つの統制された比較に分けてある
Step 10
11 問をすべて走らせる
なぜ
一つの質問の結果は、偶然に騙されやすいものです。8 通りの切り方 × 11 問をすべて走らせます。約十秒です。
コマンド
python -m rag_lab evaluate見えるはずのもの
fixed-64-0 113 chunks complete 1/11 coverage 27% 194 tokens redundancy 0% signal 42%
fixed-192-0 40 chunks complete 6/11 coverage 71% 578 tokens redundancy 0% signal 27%
fixed-448-0 18 chunks complete 10/11 coverage 91% 1209 tokens redundancy 0% signal 18%
fixed-96-0 76 chunks complete 2/11 coverage 41% 280 tokens redundancy 0% signal 43%
fixed-96-32 108 chunks complete 4/11 coverage 55% 290 tokens redundancy 10% signal 41%
fixed-96-48 141 chunks complete 2/11 coverage 36% 290 tokens redundancy 18% signal 40%
fixed-96-64 207 chunks complete 5/11 coverage 59% 290 tokens redundancy 22% signal 45%
structure-aware 60 chunks complete 6/11 coverage 71% 396 tokens redundancy 0% signal 42%
wrote labs/rag-lab/results/manifest.json
wrote labs/rag-lab/results/evaluation.json
wrote labs/rag-lab/results/chunks.json
wrote labs/rag-lab/results/retrieval.json
wrote labs/rag-lab/results/evaluation.mdここを読む
- 総合スコアはありません。各列が別々の方向に動いていて、それこそが見たいものです。大きいチャンクが最も多く取り切れ(10/11)、signal は最も低く(18%)、重なりのグループは単調ではありません(2 → 4 → 2 → 5)。
- 読みやすい版は
labs/rag-lab/results/evaluation.mdに、質問ごとに表一つで書かれます。 - すべての成果物に同じ manifest が付きます。ナレッジベースの sha256、質問セットの sha256、モデルの id と revision、依存関係のバージョン、時刻。ナレッジベースを一文字変えると、ハッシュが変わります。
自分で変えてみる
一部だけ走らせます。--preset は繰り返せます。そうすると results/ にはそれだけが残り、次のステップの export は名指しでエラーになります(ゲームが使うすべての切り方が要るため)—— 完全な evaluate を一度走らせれば済みます。--top-k を変えることもできますが、変えたあとは上の数字と比べられません。それは別の変数だからです。
python -m rag_lab evaluate --preset fixed-192-0 --preset structure-awarelabs/rag-lab/results/evaluation.md質問ごと・切り方ごとの、読みやすいレポートlabs/rag-lab/results/manifest.jsonprovenance だけ:ハッシュ、モデル、バージョン、時刻
Step 11
サイト上の結果を作り直す
なぜ
ゲームのページが読むのは静的な成果物で、ブラウザの中でモデルは動きません。このコマンドが、results/ からサイトへの唯一の道です。
コマンド
python -m rag_lab export見えるはずのもの
wrote src/data/rag02/generated/manifest.json
wrote src/data/rag02/generated/game.json
wrote src/data/rag02/generated/evaluation-matrix.jsonここを読む
configs/site.jsonが決めるのはどの実行を見せるかだけで、スコア、順位、断片の原文はすべてresults/から読み戻しています。- 走らせたあとに
git diff src/data/rag02/generated/を見てください。パラメータを何も変えていなければ、違いは manifest の時刻だけのはずです。
自分で変えてみる
configs/strategies.json の fixed-192-0 の chunk_size を 256 に変え、evaluate と export を再実行して、npm run build してみてください —— サイトはビルド時にエラーになります。手順のページが引いている数字が合わなくなるからで、これは意図したものです。
走らせたあと
走らせたあと
- 結果はこのページとまったく同じになるはずです。同じ類似度、同じ順位、同じ token 数。モデルの revision は固定され、ベクトルは決定的で、並びの同点もチャンクの順で決まります。変わるのは manifest の時刻だけです。
- 違っていたら、まず
results/manifest.jsonの依存関係のバージョンとモデルの revision が合っているかを確かめてください。 - この Lab は、チャンク分割のためだけに作ったものではありません。このあとの Query Rewrite は query の部分を変え、Hybrid Search は二本目の検索経路を足し、Rerank は検索のあとに一層足します —— ナレッジベース、質問セット、指標は動きません。
今回やらなかったこと
生成のステップはありません。top-k をローカルの LLM に渡して答えを書かせることはできますが、今回はやりませんでした。生成は、測りたい変数の上にもう一つ変数を重ねてしまううえ、「根拠が戻ってきたか」という問いには、どのモデルも一文を書く前から答えが出ているからです。
参考
固定している側
変わるのは切り方だけです。以下はどの実行でも同じです。そうでなければ結果の変化に二通りの原因が生まれ、実験は何も示さなくなります。
- Embedding モデル
intfloat/multilingual-e5-small@614241f622f53c4eeff9890bdc4f31cfecc418b3(MIT · 384 次元 · ウィンドウ 512 tokens · XLMRobertaTokenizer)- プレフィックス
query:/passage:—— e5 ファミリーのモデルが求める入力の形式- 類似度
- cosine (dot product of L2-normalized vectors)
- top_k
- 3
- カバーのしきい値
- ルールの文字数のうち 60% 以上が top-k に入っていて、はじめて「取れた」と数える
- ナレッジベース
- 7 件の文書 · 61 セクション · sha256
46d3bdea87debd98 - 質問セット
- 11 問 · sha256
bdd2a597778ca588 - 実行環境
- Python 3.11.15 · torch 2.14.0 · sentence-transformers 5.7.0 · numpy 2.4.6
変わる側 —— 8 通りの切り方を、三つの統制された比較にしたもの:
fixed-64-064 トークンずつ、重ねない113 チャンク · 2–66 tokensfixed-192-0192 トークンずつ、重ねない40 チャンク · 17–193 tokensfixed-448-0448 トークンずつ、重ねない18 チャンク · 81–449 tokensfixed-96-096 トークンずつ、重ねない76 チャンク · 17–98 tokensfixed-96-3296 トークンずつ、隣と 32 トークン重ねる108 チャンク · 36–98 tokensfixed-96-4896 トークンずつ、隣と 48 トークン重ねる141 チャンク · 61–98 tokensfixed-96-6496 トークンずつ、隣と 64 トークン重ねる207 チャンク · 66–98 tokensstructure-aware文書の構造で切る、上限 480 トークン60 チャンク · 63–402 tokens