はじめに
RAGを組んでみたら、検索が当たった質問にはちゃんと答えるのに、外した質問ではしれっと嘘をつく。「本文に書いていないこと」を聞くと、ありもしない設定をでっち上げる——ローカルでRAGを動かした人なら、一度は見た光景だと思います。
これまでは プレフィックスキャッシュで TTFT が約9割短くなること など、GPUを使用した運用基盤側の実測してきました。
その流れで、今度はRAGそのものを自分の手で分解して測ってみたくなりました。ライブラリの RetrievalQA を一行呼べば動きますが、その一行の裏では工程ごとに設計判断が走っていて、既定値で流すと「なぜか精度が出ない」とハマるからです。
じつはこの「しれっと嘘」に気づいたのは、別の検証の途中でした。RAGと、全文をそのまま渡すCAG(Cache-Augmented Generation)を同じ50問で突き合わせたとき、CAGなら正解する質問でRAGだけが幻覚を起こす場面が何度もあったのです。その比較は別シリーズにまとめてあります——CAGの実装と設計(実装編)と速度・精度を50問で実測(結果編)。
「なぜRAGは外すのか」という問いは、あの比較から持ち帰ったものです。だからこの記事は「RAGを動かしてみた」では終わらせません。チャンク分割・埋め込み・検索・生成の4工程それぞれで、どんな選択肢があり、なぜそれを選んだのかを1つずつ言語化し、自作の日本語小説50問で実測して裏を取りました。狙いは2つ。まず、RAGの良し悪しが生成LLMの賢さではなく手前の検索設計でほぼ決まることを数字と失敗例で示すこと。その上で——RAGはもちろん強力ですが、ローカルで動かすならもう一歩先に行ける。チャンクやベクトル検索を詰める前に、最重要の知識をGPUのキャッシュに常駐させて取りこぼしを減らすという、ローカルserving だからこそ取れる設計を提案します。
この記事ではRAGを組んだのに精度が伸びず、チャンク分割やtop_kを調整しても頭打ち——という人に向けて、新たなアプローチを提案します。
外したら困る最重要の知識は、検索の当たり外れに賭けず、最初からキャッシュに含めてしまえばいい(ローカル運用が前提)。前提知識は埋め込みとベクトル検索の概念程度で十分。自分の困りごとに近い節へ飛んで読んでください。
要点
- RAGを4工程に分解して自作し、自作日本語小説への50問(6タイプ)で実測した。
- 速度・コストは優秀。検索は1問あたり 0.034 秒(CPU・FAISS)でほぼ無視でき、プロンプトは平均 393トークンと短い。所要時間は実質LLM生成だけで、1問あたり 0.524 秒でした。
-
正答率は 1.580 / 2.0(全50問・0/1/2点採点)。
simple(人物名など単純な事実)は満点。一方でinference(推論)が最弱の 1.33。 - 失点の大半はLLMの言語能力ではなく、「検索が答えのチャンクを引けなかった」ことに由来していました。ただし「RAGの質はリトリーバで決まる」は当たり前。本記事の提言は一段先——チャンク調整やベクトル検索より、最重要の知識を 32K のキャッシュに常駐させて検索の当たり外れから外すほうが効く。ローカルserving ならキャッシュを自分で管理でき、常駐を増やしても追加コストは電気代だけ(APIは呼ぶたび入力課金)。本構成の常駐予算は約30Kトークン、入りきらないロングテールは従来RAGに任せるハイブリッドを提案します(記事末の考察)。
- なお本記事は各問1試行・ウォームアップなしの単発計測であり、厳密なベンチマークではありません(計測条件・限界を参照)。
「全文をそのまま見せる CAG なら、本記事の失敗のどれが消えるのか」が気になった方は、50問で突き合わせた結果編へ。本記事の常駐提言は、その比較で見えた CAG の強みを RAG 側に取り込む試みでもあります。
RAGの4工程と、各工程に潜む設計判断
RAGは「質問のたびに関連箇所を検索し、当たった断片だけをLLMに渡す」方式です。仕組みは4工程に分かれます。
前半3工程(分割・埋め込み・インデックス構築)は事前に一度だけ、後半(検索・生成)が質問のたびに走ります。
ライブラリの RetrievalQA を呼べば一行で動きますが、その一行の裏には工程ごとに分岐点があります。ここを既定値のまま流すと、後で「なぜか精度が出ない」とハマる。今回採った判断を先に一覧にします。
| 工程 | 主な選択肢 | 今回の採用 | 理由(要約) |
|---|---|---|---|
| チャンク分割 | 固定長 / 文単位 / セマンティック | セマンティック(256字目標) | 1チャンクに話題を混ぜないため。当たりの純度を上げる |
| 埋め込み | OpenAI / 多言語E5 / 日本語特化ruri | ruri-v3-310m |
日本語の検索精度を優先。ローカル完結・コスト0 |
| 索引 | HNSW / IVF / 全探索(FlatIP) | IndexFlatIP |
32チャンクと小規模。近似は不要、再現性も上 |
| 検索数 | top_k = 1 / 3 / 5 … | top_k = 3 | 文脈と混入のバランス。後述の失敗の遠因でもある |
| 生成 | 各種LLM / 温度・出力長 | Qwen3.5-4B-FP8, temperature=0
|
12GBで動く範囲・再現性のため貪欲デコード |
| 評価 | 完全一致 / 人手 / LLM採点 | LLM-as-a-Judge(0/1/2点) | 表現揺れを吸収しつつ大量採点 |
以降、この表の各行を「なぜそうしたか/他の手はなかったか」まで開いていきます。
検証環境と計測条件
比較をフェアにするため、LLM・質問・採点者を固定し、変えるのは「知識をどう渡すか」だけにしました。
環境(再現性ブロック)
| 項目 | 値 |
|---|---|
| GPU / VRAM | NVIDIA RTX 4070 / 12 GB |
| OS / ドライバ | Ubuntu 24.04.4 LTS / NVIDIA driver 580.126.20 |
| Python | 3.12.3 |
| 推論基盤 | vLLM(vllm/vllm-openai:nightly, OpenAI互換API) |
| LLM |
RedHatAI/Qwen3.5-4B-FP8-dynamic(FP8, --enforce-eager, --max-model-len 32768) |
| 埋め込み |
cl-nagoya/ruri-v3-310m(日本語特化, 768次元) |
| 検索 |
faiss-cpu 1.14.3(IndexFlatIP 内積=コサイン類似度) |
| 主要ライブラリ | sentence-transformers 5.6.0 / torch 2.12.1 / numpy 2.4.6 / openai 2.43.0 |
| 採点 | LLM-as-a-Judge(Gemini, temperature=0.0, 0/1/2点) |
| 題材 | 自作の日本語短編小説『潮霧湾の門守り』(3,768文字 / 約2,450トークン) |
| 質問 | 50問。6タイプ(simple / detail / relation / timeline / inference / unknown) |
計測条件
-
生成:
temperature=0.0(貪欲デコード)・max_tokens=256・ストリーミング。TTFTとデコード時間を分けて計測。 - 試行回数:各問 1回のみ。RAG側はウォームアップなし、埋め込みモデルはCPU常駐。
- 同時実行:なし(逐次)。検索・埋め込みはCPU、生成はGPU。
注意:試行1回の単発計測です。 速度の数値は桁感をつかむ目安として読んでください。厳密な比較には複数試行と分散の報告が要ります——ベンチは疑ってかかるものです。
工程1 チャンク分割:固定長か、意味境界か
RAGの検索精度は、まずチャンクの切り方で決まります。ここが最初の、そして見落とされがちな設計判断です。
選択肢は3つありました。
- 固定長スライド(例:512字ごと、オーバーラップ付き)。実装は最も簡単。ただし文や話題の途中で機械的に切れる。
- 文単位。切れ目は自然だが、1文では文脈が足りず、検索の単位として細かすぎる。
- セマンティックチャンク。意味のまとまりで切る。実装は重いが、1チャンクの話題が揃う。
今回は3を採りました。理由は明確で、1チャンクに複数の話題が混ざると、検索の「当たり」の純度が落ちるからです。固定長で切ると、「父の失踪」と「町の規則」が同じチャンクに同居し、どちらを聞かれても中途半端なチャンクが返ってしまう。
具体的には、隣り合う文の埋め込みのコサイン類似度を見て、類似度が**下位25%**に落ちる境界(=話題の変わり目)で区切ります。段落・章見出しは強制的な区切りとして扱いました。
なぜ閾値をパーセンタイル(相対値)にしたか。コサイン類似度の絶対値はモデルやテキストでばらつくため、「0.7以下で切る」のような固定値は移植性が低い。そのテキスト内で相対的に類似度が落ちる点を切れ目とすれば、文章の性質に自動で追従します。
# 隣接文のコサイン類似度を計算(埋め込みは正規化済みなので内積=コサイン)
sims = [float(np.dot(emb[i], emb[i + 1])) for i in range(n - 1)]
# 下位25パーセンタイルを「意味の切れ目」とする(絶対値でなく相対値で)
threshold = float(np.percentile(sims, PERCENTILE)) # PERCENTILE = 25
# 目標256字。意味の切れ目 or サイズ超過で chunk を確定
semantic_break = i < len(sims) and sims[i] < threshold
size_break = cur_len >= target * 1.5
if is_last or must_break or (semantic_break and cur_len >= target * 0.5) or size_break:
flush() # ← ここでチャンク確定
なぜ目標256字か。長すぎると1チャンクに話題が増えて純度が落ち、短すぎると文脈が切れる。日本語の段落1〜2個ぶんに当たる256字を中心に、下限(target * 0.5)と上限(target * 1.5)でガードしました。
結果、約2,450トークンの小説が 32チャンクになりました。目標は256字ですが、章見出しが独立チャンクになるぶん、実測のチャンク長は 平均118字・中央値139字・最大212字とばらつきます。「見出しは見出しで独立、本文は意味境界で切る」という設計の素直な帰結です。
工程2 埋め込み:なぜ日本語特化モデルで、なぜプレフィックスが非対称なのか
分割した32チャンクをベクトルにします。ここでの判断はどの埋め込みモデルを使うかでした。
選択肢:
-
OpenAI
text-embedding-3:精度は高いが、API課金が発生し、ローカル完結の趣旨に反する。 - 多言語E5 / multilingual モデル:日本語も扱えるが、英語中心の学習で日本語の細かい意味差に弱い場面がある。
-
日本語特化
ruri-v3:日本語の検索タスクで上位。ローカルで動き、コストは0。
今回はローカル完結・日本語精度を優先して ruri-v3-310m を採りました。RTX 4070のVRAMはvLLM(生成LLM)が占有するため、埋め込みと検索はすべてCPUで回します。310mサイズなら32チャンクの埋め込みはCPUでも一瞬です。
そして日本語特化モデルには、外すと検索精度が露骨に落ちる作法があります。
ruri-v3系は、ドキュメント側にはプレフィックスを付けず、クエリ側にだけ"クエリ: "を付ける。
前処理が非対称なのです。これはモデルが「文書」と「検索クエリ」を別々の役割として学習しているためで、揃えてしまうと本来の性能が出ません。比較実験では、ここを外すとRAGが不当に弱く見え、結論そのものが歪みます。今回はドキュメント側をプレフィックスなしで素直に埋め込みました。
# ドキュメント(チャンク)はプレフィックスなしで埋め込む
embeddings = model.encode(
texts,
batch_size=16,
normalize_embeddings=True, # ← 正規化して内積をコサイン類似度に
convert_to_numpy=True,
)
# FAISS は内積インデックス(正規化済みなのでコサイン類似度と等価)
index = faiss.IndexFlatIP(embeddings.shape[1])
index.add(embeddings)
faiss.write_index(index, str(INDEX_PATH))
索引の選択もここで決めました。HNSWやIVFといった近似最近傍(ANN)は大規模コーパスで効きますが、今回は32チャンクしかありません。近似は精度を犠牲にするだけ無駄なので、全探索の IndexFlatIP を選択。埋め込みを正規化しておけば内積がそのままコサイン類似度になり、結果も完全に再現します。「データ規模に索引を合わせる」——数千チャンク程度までは全探索で十分、というのが今回の判断です。
工程3 検索:なぜ top_k=3 で、検索が品質の天井を作るのか
質問が来たら、クエリにだけ "クエリ: " を付けて埋め込み、FAISSで上位チャンクを引きます。
QUERY_PREFIX = "クエリ: "
def retrieve(model, index, chunks, query, top_k=3):
vec = model.encode(
[QUERY_PREFIX + query], # ← クエリにだけプレフィックス
normalize_embeddings=True,
).astype(np.float32)
scores, indices = index.search(vec, top_k) # FAISS で内積検索
return [chunks[i] for i in indices[0]]
なぜ top_k=3 か。ここはトレードオフです。
- **少なすぎる(top_k=1)**と、答えが2位以下のチャンクにあると即アウト。文脈も足りない。
- **多すぎる(top_k=10)**と、無関係なチャンクがノイズとして混ざり、LLMが余計な情報に引きずられる(後述の「過剰回答」の温床)。プロンプトも長くなる。
小説約2,450トークン・32チャンクという規模で、1問に必要な文脈はおおむね数チャンク。そこで3を初期値に置きました。ただし正直に言うと、top_kのアブレーション(1/3/5/10での比較)は今回やっていません。後述する失敗のいくつかは top_k を上げれば救えた可能性があり、ここは次の検証課題です。
そして、この工程こそRAGの品質の天井を決めるところです。
LLMが見られるのは、この検索で当たった3チャンクだけ。物語の全体像は見えていない。
どれだけ生成LLMが賢くても、答えのチャンクが上位3件に入らなければ、LLMには答えようがありません。RAGの正答率は、生成より先にこの検索のリコールで頭打ちになる——これが後半の結果すべてを説明します。
工程4 生成:12GBで動かす範囲と、再現性のための既定値
当たった3チャンクを参考文章として詰め、質問とともにLLMへ投げます。
def build_prompt(question, context_chunks):
context = "\n\n".join(
f"[チャンク{i+1}]\n{c['text']}" for i, c in enumerate(context_chunks)
)
return (
"以下の文章を参考にして、質問に日本語で簡潔に答えてください。\n\n"
f"【参考文章】\n{context}\n\n"
f"【質問】\n{question}\n\n【回答】"
)
LLMの選定は、RTX 4070(12GB)で動くことが第一条件でした。BF16の4Bモデルは12GBに載らずOOMになるため、FP8量子化版の Qwen3.5-4B-FP8-dynamic を選択。vLLMは --enforce-eager(CUDA graphも12GBではOOM)で起動しています。このあたりの12GB運用の詳しい根拠は別記事に譲ります。
生成パラメータは再現性優先です。temperature=0.0 の貪欲デコードで毎回同じ出力にし、max_tokens=256 で打ち切り。回答はストリーミングで受け、TTFT(最初のトークンまで)とデコード時間を分離計測しました。
RAGプロンプトは「参考文章3チャンク+質問」だけなので短く、平均 393トークンで収まります。
評価設計:自作小説・unknown型・LLM採点を選んだ理由
「測り方」そのものも設計です。ここを雑にやると、出た数字が信用されません。3つの判断をしました。
1. なぜ題材を自作小説にしたか。 既知のテキスト(Wikipediaや有名小説)を使うと、モデルが事前学習で答えを"暗記"していて、コンテキストを見ずに答えてしまう。それではRAGの検索依存を測れません。完全自作の小説『潮霧湾の門守り』を書き下ろし、知識リークを断ちました。
2. なぜ unknown(本文に答えがない)型を混ぜたか。 RAGは当たったチャンクしか見ないので、「本当に記載がないのか」を判断する材料を持っていないことがあります。**「知らないことを知らないと言えるか」**は、RAGの弱点が最も出る観点です。50問のうち8問を「本文に答えがない質問」にしました。
3. なぜ LLM-as-a-Judge か。 「五年前」と「5年前」、「アオイの父」と「ユナト」のように、正しくても表現が揺れる回答を完全一致では正しく採点できません。かといって100回答(RAG/CAG×50問)の人手採点は重い。そこで0/1/2点の基準を与えてLLMに採点させ、表現揺れを吸収しました。
| タイプ | 例 | 模範解答 |
|---|---|---|
| simple | 主人公の灯台守見習いの名前は? | アオイ・ユルカ |
| detail | 北の岬の鉄箱から見つかった三つのものは? | 濡れた海図、真鍮の半円板、薄板 |
| relation | アオイとセツの関係は? | 見習いと、その師匠の灯台守長 |
| timeline | ユナトが消えたのは何年前? | 五年前 |
| inference | レギスが開放を渋った理由は? | 契約維持と導管熱の確保を優先したため |
| unknown | アオイの髪色は何色? | 本文中に記載なし |
結果① 速度とコスト(全50問)
まず信頼できる数字から。
| 指標 | 平均値 |
|---|---|
| 検索時間(埋め込み+FAISS, CPU) | 0.034 s |
| TTFT(最初のトークンまで) | 0.099 s |
| デコード時間 | 0.390 s |
| LLM合計 | 0.490 s |
| 1問あたり総時間 | 0.524 s |
| プロンプトトークン | 393.2 |
| 生成トークン | 20.6 |
ポイントは、検索そのものは 0.034 秒とほぼ無視できることです。RAGの所要時間は実質「LLMの生成時間」で決まっており、CPUのFAISS検索は速度のボトルネックではありません。短いプロンプト(393トークン)で済むのがRAGのコスト面の強みです。
結果② 検索品質:スコアは高い。でも「高い=当たり」ではない
上位1件のコサイン類似度(top-1 score)は次の通り。
| top-1 スコア | 値 |
|---|---|
| 最小 | 0.823 |
| 平均 | 0.882 |
| 最大 | 0.938 |
50問すべてで0.82以上と、一見「常に近いチャンクを引けている」ように見えます。ところが——スコアが高くても、それが答えを含むチャンクとは限りません。日本語の意味的に近い文(同じ登場人物・同じ場所の描写)は高スコアで横並びになるため、「話題は合っているが答えは別チャンクにある」というニアミスが起きる。この油断が、次の正答率に効いてきます。
結果③ 正答率(全50問)
LLM-as-a-Judgeによる0/1/2点採点の結果です。
RAG平均:1.580 / 2.0
スコア分布:
| スコア | 件数 |
|---|---|
| 2点(正解) | 36 |
| 1点(部分正解) | 7 |
| 0点(不正解) | 7 |
タイプ別:
| タイプ | N | RAG平均 |
|---|---|---|
| simple | 8 | 2.00 |
| timeline | 8 | 1.62 |
| detail | 9 | 1.56 |
| relation | 8 | 1.50 |
| unknown | 8 | 1.50 |
| inference | 9 | 1.33 |
simple(人物名など単純な事実)は満点。最も落ちるのは inference(推論)の1.33、次いで relation・unknown・detail です。
「なぜ落ちるのか」は、平均値より失敗例の中身を見たほうが早いです。
結果④ RAGが外す瞬間 —— 失敗例の解剖
RAGの失点は、ほぼ例外なく**「検索が答えのチャンクを引けなかった」**ことに起因します。典型を挙げます(いずれも実際の回答)。
失敗1:検索ミス → 幻覚(detail / q008)
Q:北の岬の鉄箱から見つかった三つのものは?
模範解答:濡れた海図、真鍮の半円板、『灯ではなく門を見よ』と刻まれた薄板
RAGの回答:三つのものは、赤霧、潮位鐘、鉄箱です。(0点)
検索で引いた3チャンクに「三つのもの」の正体が含まれていませんでした。材料がないまま「三つ」という形式だけに引きずられ、本文中の別の単語(赤霧・潮位鐘)を寄せ集めてそれらしい嘘を作ってしまった。RAGの最も危険な失敗パターンです。
失敗2:答えは別チャンクにあるのに「わからない」(timeline / q043)
Q:ミナトが新しい台帳を書き始めたのはいつ?
模範解答:中央塔の開放が成功した翌朝
RAGの回答:具体的な日付や時期が明記されていないため、文中からは判断できません。(0点)
本文には「開放成功の翌朝」と書いてあります。だが時系列情報を持つチャンクが上位3件に入らず、見えている範囲には手がかりがなかった。結果、正直に「わからない」と言ったのに不正解になる。全文を見ていれば防げた失点です。top_kを上げれば救えたかもしれません。
失敗3:unknown型での誤回答(unknown / q049)
Q:静まり溜りはどこに位置するか?(模範解答:本文中に記載なし)
RAGの回答:中央塔地下の室蓋計の部品である半円板が…(場所を特定)(0点)
「記載なし」が正解なのに、近い話題のチャンクが高スコアで引けてしまったために、それを根拠に場所をでっち上げました。RAGは「自分が見ている3チャンクの外に答えがないこと」を保証できない——unknown型で取りこぼす理由はここにあります。
失敗4:3チャンクでは推論が組み立たない(inference / q023)
Q:赤霧や記憶の乱れは何の兆候だった?
模範解答:残響の余剰を逃がせず逆流が起きていた兆候
RAGの回答:赤霧は「導管熱」の兆候、記憶の乱れは「誰かが規則や記録を消そうとしている」兆候です。(0点)
最弱の inference 型の典型です。正解にたどり着くには、物語の離れた場所に散らばった伏線(残響の仕組み・逆流・赤霧の描写)をつなぎ合わせる必要があります。だがRAGが見ているのは上位3チャンクだけ。手元の断片から局所的に"それらしい"因果を組み立ててしまい、全体を貫く真因に届かない。推論型が最弱なのは、「全体を見渡せない」というRAGの設計上の制約から来ていると考えられます。
部分正解の例:過不足(detail / q034)
Q:手帳の末尾に記された正しい開放の条件は何?
模範解答:第三塔と第五塔の位相をそろえてから行うこと
RAGの回答:第三塔と第五塔の位相をそろえ、半円板を室蓋計へ戻すこと。(1点)
正しい条件は当てたが、別チャンク由来の余分な条件を足してしまった。top_kで複数チャンクを混ぜるがゆえの過剰回答で、検索数のトレードオフがそのまま出ています。
ここで挙げた失敗は、冒頭で触れた比較シリーズで「全文を見るCAG」に同じ質問を解かせると、その多くが消えました。とくに unknown 型と推論型で差が大きい。「3チャンクしか見ない」ことがどれだけ効いているかは、結果編:速度・精度を50問で実測に数字で出しています。
考察:RAGの一歩先へ —— チャンク調整より「何を常駐させるか」
§結果④で見たRAGの失点は、ほぼ例外なく検索が答えのチャンクを上位3件に入れられなかったことに起因していました。ではどう良くするか。ここで多くの記事は「チャンクの切り方を変える」「top_kを上げる」「リランカーを足す」へ向かいます。本記事でいちばん言いたいのは、それより手前に、もっと効くレバーがあるということです。
最重要の知識は、検索の当たり外れに賭けず、はじめからコンテキストに常駐させておけばいい。
検索ミスが起きるのは、答えがインデックスの奥にあって「引けるかどうか」が確率的だからです。ならば、よく聞かれる/答えの密度が高い中核部分を最初からプロンプトに載せてプレフィックスキャッシュで常駐させてしまえば、その範囲については検索ミスが構造的に起こりえなくなる。今回の小説(約2,450トークン)はまるごと32Kに収まるので、"中核" は全文そのもの——全文を載せたCAGがRAGに勝った(1.90 対 1.58)のは、賢くなったからではなく、当てる必要がなくなったからです。モデルを大きくするより、まず「何を常駐させるか」を設計するほうが効きます。
なぜ「常駐させる」がローカルserving でこそ効くのか
この発想は、ローカルで自分でサービングしているからこそ現実的になります。私がローカルserving のエンジニアの立場で強調したいのはここです。
-
キャッシュを自分で管理できる。 どのコンテキストを、いつまで常駐させるかを自分で決められます。本構成も
--enable-prefix-cachingで中核プロンプトを一度プレフィルすれば再利用が効く。ホスト型APIではプレフィックスキャッシュを思いどおりに固定できず(TTLが短く、ヒットも保証されない)、「中核を常駐させる」戦略そのものが取りづらい。 - コストの増え方が違う。 30Kの中核を毎クエリ載せると、APIでは呼ぶたびに入力課金が乗り、利用が増えるほど料金が積み上がります。ローカルは初回に一度プレフィルすれば以降は再利用で、追加コストは実質電気代だけ。常駐サイズを増やしても、APIのように呼び出しごとに比例して効いてくることはなく、使うほど1クエリあたりは相対的に安くなる。「重要なものをコンテキストに常駐させておく」は、APIでは不経済でも、ローカルではほぼタダでできるのです。
つまり、APIのRAGプレイブックが見落としているレバーは、リトリーバの調整ではなくキャッシュ常駐そのものです。チャンク戦略やベクトル検索の工夫の前に、まず「何を常駐させれば検索に賭けずに済むか」を設計する。これがローカルserving だから取れる一歩先です。
それでも一般RAGは捨てない —— 32Kに「最重要」を入れて取りこぼしを減らす
もちろん、すべてが常駐予算に収まるわけではありません。RTX 4070(12GB・--max-model-len 32768)で常駐させられるのは約30Kトークンまで——これはVRAMで張れるKVキャッシュの上限で、--gpu-memory-utilization を 0.90 まで下げて捻出した枠です("メモリに載る" の本体はRAMではなく、アテンションが張れるKVキャッシュ。プレフィルしていないトークンには注意を向けられない)。数百万トークンの知識ベースはここに入りません。だから、はみ出すぶんを引いてくる一般のRAGは依然として必要です。
そこで提案する構成はハイブリッドです。
- 最重要の中核(FAQ・主要文書・用語集など)を 32K に常駐させる。 ここは検索を介さないので取りこぼしゼロ。
- 残りのロングテールは従来どおりRAGで引く。 外しても被害が相対的に小さい部分に検索ミスを押し込む。
効果はシンプルで、「確実に当てたいもの」を検索の運任せから外すぶん、§結果④のような失点が減る。常駐に入れた範囲では、幻覚も誤った「わからない」も unknown 型の作話も起こりえません。最重要を32Kに入れれば取りこぼしは確実に減る——これが本記事の提言です。チャンク幅やtop_kのチューニングは、この常駐/検索の切り分けを決めたあとの二次調整に格下げされます。
これはRAGの否定ではありません。RAGの仕事を「答えを正確に1チャンクで当てる」から「常駐に入りきらないロングテールを掬う」へ役割分担する、という再設計です。検索の評価軸も、精度(top_kの最適化)から常駐予算ぶんの超集合をどれだけ高リコールで掬えるかへ移ります。
この実験の限界
一般化する前に断っておきます。
- 試行1回・ウォームアップなしの単発計測。速度は桁感の目安で、分散は測っていません。
-
題材は自作小説1本・モデル1種・
top_k=3固定。コーパスの種類やサイズ、top_k、チャンク戦略を変えれば結果は動きます。とくに「検索ミス由来の失点」はtop_kを増やすと一部救える可能性が高い——ここは次回の課題です。 - 採点は単一Judge(Gemini)。人手評価や複数Judgeとの一致は取っていません。スコアは相対比較の目安として読むのが妥当です。
- 結論は**「この条件ではこうなった」であって、RAG一般の優劣を証明**するものではありません。
まとめ:チューニングの順番を間違えない
- 速度・コストは優秀。検索自体は実質ゼロコストで、所要時間はLLM生成で決まります。
- 全50問のRAG平均は 1.580 / 2.0。
simpleは満点だが、inference(1.33)を筆頭に取りこぼす——その大半は「検索が答えのチャンクを引けなかった」失点です。 - だからまず疑うべきは生成LLMではなく検索です。精度が出ないとき、モデルを大きくする前に検索を見直す——ここまでは当たり前。
- もう一段先が本題です。チャンク幅やtop_kを詰める前に、最重要の知識を 32K のキャッシュに常駐させ、検索の当たり外れから外す。常駐させた範囲は取りこぼしが構造的に消えます(全文が載った今回の小説でCAGが勝ったのもこれ)。ローカルserving ならキャッシュを自分で管理でき、常駐を増やしても追加コストは電気代だけ——APIのように呼ぶたび入力課金が乗ることはありません。入りきらないロングテールは従来RAGに任せるハイブリッドが、今回いちばん伝えたい設計指針です。
あわせて読みたい:同じ50問を「3チャンクだけ見るRAG」と「全文を見渡すCAG」に解かせた比較は、別シリーズの実装編・結果編にまとめてあります。本記事で見たRAGの失敗のうち、全文を見るCAGで消えたものがどれか——はそちらが詳しいです。詰まった点や「うちの環境では違った」があれば、コメントで教えてください。追記します。
ローカル化の相談、受けます:「APIのRAGはコストが読めない」「精度が出ないからCAG的に重要部分をキャッシュへ常駐させたいが、自前GPUにどう載せるか分からない」——そんな相談があれば、コメントで気軽にどうぞ。本記事の構成(vLLM + プレフィックスキャッシュ + RAG/常駐ハイブリッド)をベースに、載る/載らないの見積もりから一緒に考えます。
参考文献
- 実装コード・データ・計測スクリプト一式(本記事のリポジトリ): ai-systems-notes/local-llm-rag-cag-benchmark- 実装コード・データ・計測スクリプト一式(本記事のリポジトリ): ai-systems-notes/local-llm-rag-cag-benchmark
- ruri-v3 埋め込みモデル(クエリ/ドキュメントのプレフィックス作法): cl-nagoya/ruri-v3-310m
- FAISS(ベクトル検索): facebookresearch/faiss
- vLLM(推論基盤): docs.vllm.ai
- 量子化モデル: RedHatAI/Qwen3.5-4B-FP8-dynamic
- 本記事の着想元(RAG vs CAG 比較シリーズ): CAG の実装と設計(実装編) / CAG は RAG に勝てるか — 速度・精度を50問で実測(結果編)
- 拙稿(prefix cache / ローカルLLM高速化): ローカルLLMでのRAGの回答が遅い。それ毎回同じ長文をprefillしていませんか? / あなたのボトルネックはどれ? — ローカルLLMを速くする手段の早見表
- CAGの原典: Chan et al., "Don't Do RAG: When Cache-Augmented Generation is All You Need for Knowledge Tasks", arXiv:2412.15605