実験日: 2026-08-24
環境: RTX 5070 Ti 16GB / WSL2 / Ollama 0.32.15 / Qwen 3 8B Q4
RAGを使うとLLMが賢くなる、と説明されることがあります。しかし検索が外れれば、LLMへ正しい根拠は届きません。また、元からmodelが知っている一般知識だけで試すと、retrievalの効果を過大評価します。
RAGをつなぐと賢くなったように見えますが、検索と回答を分けて測らなければ効果は判断できません。
そこで、このPCで直前に測った速度・VRAM・正解率を含む自作13記事をlocal corpusにしました。これらの数値はQwen 3のpretraining時には存在しません。同じ20問を「検索なし」と「TF-IDFで上位3 chunkを検索して渡すRAG」で回答させたところ、正解は0/20から19/20へ増えました。
ただし1問は、正しいdocumentを検索できたのに必要なheading chunkを渡せず失敗しました。RAGはdatabase接続の魔法ではなく、retrieval、chunking、generationを別々に評価すべきpipelineです。
0問から19問へ増えた
| metric | 結果 |
|---|---|
| 検索なしの数値正解 | 0/20 |
| RAGの数値正解 | 19/20 |
| 期待sourceがrank 1 | 17/20 |
| 期待sourceがtop 3内 | 20/20 |
| 期待sourceを回答で引用 | 19/20 |
| 平均回答時間:検索なし | 0.316秒 |
| 平均回答時間:RAG | 0.376秒 |

この結果は「RAGなら95%正しい」という一般性能ではありません。このcorpus、この20問、公開した正規表現採点での結果です。一方、検索なし0/20は、modelの記憶では答えられないPC固有の数値を選べたことを確認します。
RAGとは何を追加する仕組みか
RAGはRetrieval-Augmented Generationの略です。質問を受けてから外部資料を検索し、関連部分をLLMのpromptへ追加して回答させます。
user question
↓
retriever ──→ top-k chunks
↓ ↓
└──── promptへ結合
↓
local LLM
↓
answer + source ID
model parameterを再学習するfine-tuningとは違い、文書を差し替えれば新しい情報へ更新できます。反対に、検索で取得しなかった情報はpromptに入りません。
RAGの評価を1個の「回答正解率」だけにすると、どこが壊れたか分かりません。今回は次を分けました。
retrieval hit@1 : 正解sourceが1位か
retrieval hit@3 : 正解sourceが上位3件にあるか
answer accuracy : 必要な数値を回答したか
citation check : 期待source IDを引用したか
latency : generationに何秒かかったか
modelが知らない20問を作る
質問は、このPCで2026-08-24に実測して記事へ記録した値です。
12BモデルのGPU peak使用量は何GiBだった?
context 16384条件で実際のprompt token数はいくつ?
code生成でQwen 3 8Bは初回と修正後に何問合格した?
batch 8192のpeak allocatedは約何MiB?
HOG+SVMとCNNのtest accuracyをそれぞれ答えて。
一般的な「日本の首都」や「backpropagationとは」を使いません。既存知識で正答すると、retrievalが効いたのか判別できないからです。
20問には正解sourceと受理する数値patternを事前に固定しました。例えば12BのTruthfulQA strict率なら73.0を要求します。file sizeのように回答が3.1095 GiBから3.11 GiBへ丸められても意味が同じ場合は、両方を受理しました。
採点規則を出力後に都合よく変えないことが重要です。最初のdiagnostic runで3.11と4.64を誤答扱いする過度に厳しいpatternを発見し、意味を保つ丸めだけを許して全20問を再実行しました。この変更もwork logへ残しています。
corpusは13記事、198 chunk
対象は、これまで作ったlocal LLM、PyTorch、FashionMNISTなどのMarkdown 13本です。
13 documents
198 heading chunks
各## headingをchunk境界にしました。固定長で800文字ごとに切る方法より、1つの話題を保ちやすいからです。画像記法を除き、1 chunkの上限を3,500文字にしました。
parts = re.split(r"(?=^##\s+)", text, flags=re.M)
for i, part in enumerate(parts):
chunks.append({
"id": f"{path.name}#{i}",
"source": path.name,
"text": clean[:3500],
})
source IDにfilenameとchunk番号を持たせると、LLMのcitationを元文書へ戻せます。productionではheading名、URL、更新日、access controlもmetadataへ入れます。
日本語tokenizerなしでcharacter n-gramを使う
最初のretrieverにはscikit-learnのTfidfVectorizerを使いました。
vectorizer = TfidfVectorizer(
analyzer="char_wb",
ngram_range=(2, 4),
min_df=1,
sublinear_tf=True,
)
日本語は英語のようにspaceで単語が分かれません。word analyzerをそのまま使うと分割が粗くなるため、2~4文字の連続部分をfeatureにしました。Qwen 3 8B、batch 8192、GPU peakのような英数字も部分一致します。
TF-IDFは、document内で多い語のterm frequencyと、corpus全体で珍しい語のinverse document frequencyを組み合わせます。全記事に出る「実験」より、特定記事に出るTruthfulQAや8192を強く扱えます。
query vectorと198 chunkのsparse vectorの内積を取り、score上位3件を選びました。
qvec = vectorizer.transform([question])
scores = (matrix @ qvec.T).toarray().ravel()
top_idx = np.argsort(scores)[::-1][:3]
vectorをL2正規化するdefault設定なので、内積はcosine similarityとして解釈できます。
promptは「資料だけ」「無ければ無い」と指定
RAG promptです。
提供資料だけを根拠に質問へ簡潔に答えてください。
最後に根拠のchunk IDを角括弧で1つ以上引用してください。
資料に無ければ「資料内に記載なし」と答えてください。
資料:
[article_...md#4]
...
質問: ...
比較用の検索なしpromptにも「分からなければ不明」「推測で数値を作らない」と入れました。両条件はtemperature 0、seed 42、think:false、num_predict:120で揃えています。
検索なしが0/20だったのは望ましいbaselineです。例えば2026年8月にこのPCで測ったVRAM使用量を、pretrained modelが知っているはずはありません。曖昧な推測を正解に含めない設計です。
RAGは19/20へ改善した
成功例です。
質問:
12BモデルのGPU peak使用量は何GiBだった?
回答:
9.7GiB [article_ollama_4b_8b_12b.md#4]
この質問では期待sourceはretrieval rank 2でした。rank 1は別のquantization記事でしたが、top 3を全て渡したため正答できました。
別の例です。
質問:
code生成で4Bの修正後合格数はいくつ?
回答:
7/10 [article_llm_codegen_pytest.md#1]
これも期待sourceはrank 2です。hit@1が17/20でもanswerが19/20なのは、top-kを3にした効果です。
唯一の失敗は「document hit、chunk miss」
失敗した質問です。
context 16384条件で実際のprompt token数はいくつ?
retrieval rank 1は正しいarticle_ollama_context_4k_8k_16k.mdでした。しかし選ばれた3 chunkは#2、#15、#10で、11,244が書かれたchunkを含みませんでした。
LLMの回答は次です。
資料内に記載なし
これは良いfailureです。根拠が届かなかったのに11,244を推測せず、unknownを返しました。answer accuracyは不合格でも、hallucination controlは機能しています。
一方、「正しいdocumentが1位だからretrieval成功」とだけ集計すると、この失敗を見落とします。LLMが読む単位はdocumentではなく渡されたchunkです。document-level hitとevidence-level hitを分ける必要があります。
どう直すか
最初に試す改善は、chunkへdocument titleとheadingを必ず付けることです。今回のquestionにあるcontext 16384と、数値tableを含むheadingの語が近づきます。
次の候補です。
- top-kを3から5へ増やす
- 前後chunkも一緒に渡す
- heading + bodyでindexを作る
- query中の数値やmodel名を重くする
- TF-IDFとembeddingを併用するhybrid検索
- 上位候補をcross-encoderやLLMでrerankする
ただしtop-kを増やすほどpromptは長くなり、無関係な文も増えます。context windowへ収まることと、回答が正しいことは別です。embedding検索を同じquestion setで比較すると、このTF-IDF baselineとの差を測れます。
citationがあっても正しいとは限らない
期待sourceの引用は19/20でしたが、文字列としてsource IDがあるだけではfaithfulnessの証明になりません。
最低でも次を確認します。
citation validity : IDが実在するか
citation correctness: そのchunkが主張を含むか
citation completeness: 回答の主要claimすべてに根拠があるか
answer correctness : ground truthと合うか
今回は数値とexpected sourceを自動照合しました。自由記述の長文では、claim分割と人手review、または別のentailment evaluatorが必要です。
平均0.06秒の増加をどう読むか
平均wall timeは検索なし0.316秒、RAG 0.376秒でした。約0.06秒増です。
この値はretrieval計算を含むfull API latencyではなく、scriptがOllama generate requestを送ってからresponseを受け取る時間です。TF-IDF検索は小規模corpusで非常に短く、今回は個別計測していません。
RAG promptはtop 3 chunk分長いためprompt evaluationが増えます。corpusが100万chunkになればindex検索やnetwork、storageも効きます。この13記事の値を大規模systemへ外挿できません。
再現手順
cd /mnt/c/Users/user/Documents/Codex/2026-08-24/ko
/opt/ai-lab/venv/bin/python -u work/ai_lab/experiment_local_rag.py
/opt/ai-lab/venv/bin/python work/ai_lab/plot_local_rag.py
artifactです。
work/ai_lab/experiment_local_rag.py
work/ai_lab/results/local_rag.json
work/ai_lab/plot_local_rag.py
work/figures/local-rag-tfidf-qwen3.png
JSONには全20問について、期待pattern、期待source、top 3 chunk本文とscore、検索なし回答、RAG回答、正誤、citation、wall timeを保存しています。集計値だけでなく、唯一のfailureを再調査できます。
RAG実験で最初に置くべきbaseline
いきなりvector databaseやagent frameworkを導入する前に、小規模corpusならTF-IDF baselineを作る価値があります。
1. modelが暗記していないquestion setを固定
2. sourceとevidence chunkを正解dataへ付与
3. simple lexical retrievalを測る
4. retrieval hitとanswer accuracyを分離
5. raw retrieved textとresponseを保存
6. failureをretrieval / generation / scoringへ分類
このbaselineよりembeddingが本当に良いか、複雑なframeworkが本当に必要かを比較できます。
今回、検索なしのQwen 3 8Bは20個のPC固有値を1つも答えられず、RAGで19個を正答しました。しかし残る1件はmodelを大きくしても直りません。必要なchunkがpromptへ届いていないからです。
RAGの中心課題は「LLMへ資料を付ける」ことではなく、「答えを支持する最小の根拠を、再現可能な方法で選び、届かなかったときに検出する」ことです。
