全4回シリーズ「ローカルLLMでつくる 業務で使える社内RAG構築記」第2回
前回は、WSL2 + Docker + Ollama + Qdrant を組み合わせて、ローカルLLMによるRAGチャットの「プロトタイプ」を構築しました。
RAGの基本的な仕組みは非常にシンプルです。
質問 → 関係する資料を検索 → 検索結果をLLMに渡す → 回答
ひとたびシステムが動き出すと、誰しも次にこう考えるはずです。
「よし、社内に転がっている資料を片っ端から全部読み込ませてみよう!」
私たちも、そう意気込んで作業を開始しました。
しかし、実際に社内資料を投入し始めた途端、いくつもの課題にぶち当たることになります。PDF、メール、テキストファイル、過去のサポート対応履歴……。形式が統一されていないのはもちろんのこと、製品名の略称やファイル名の命名規則までチームや年代ごとにバラバラだったのです。
そして何より現場を悩ませたのは、「資料をVector DBに登録すること」と「その資料を実際にLLMに読ませること」はまったくの別問題であるという現実でした。
今回は、社内データをRAGへ取り込む際に直面したリアルな問題と、試行錯誤の中で得られた知見を紹介していきたいと思います。
今回は、社内データをRAGに取り込む際に実際にぶつかった問題と、そこで得た知見を紹介します。
「社内資料を全部入れる」は、最初からそんなに簡単ではない
RAGでは、資料をそのままLLMに渡すのではなく、通常は文章を適切な大きさに分割して検索用データベース(ベクトルDB)へ登録します。この分割された単位をチャンクと呼びます。
例えば100ページあるマニュアルを、そのまま1件のデータとして登録するのではなく、「第1章」・「第2章」……といった意味のまとまりを意識しながら、検索しやすい単位に細かく分けていくイメージです。
ここが最初の壁となります。つまり、「具体的に、何文字ごとに切ればいいのか?」という問題です。
1500文字ごとに切ればいい……わけではなかった
最初はシンプルに「1,500文字などの一定文字数で機械的に分割する」方法を考え付く方も多いのではないでしょうか。実装自体はとても簡単です。
しかし、もし「USBドライバのインストール手順」の途中でチャンクが切れてしまったらどうなるでしょうか?
- 前半チャンク:「〜〜の理由から、ドライバをインストールする必要があります。」
- 後半チャンク:「1. ○○画面を開く 2. ××をクリックする……(実際の手順)」
検索で前半だけがヒットしてLLMに渡された場合、LLMには肝心の手順情報が届かず、まともな回答が作れなくなってしまいます。
そこで、単なる文字数での機械的な分割ではなく、文章の「構造や意味のまとまり」をできる限り維持して分割する方針を採用することにします。
- 見出しや段落の区切り
- 不具合票の項目
- メールの1往復(やり取り)
- 一連の操作手順のまとまり
など、意味のまとまりをなるべく維持するように分割し、どうしても長すぎる場合のみ、補正として文字数制限をかけるようにしました。
さらに、あるチャンクが検索で引っかかった際、必要に応じて前後関係を辿れるような仕組みも導入しました。「検索エンジンが探し出せる単位」と「人間(およびLLM)が読んで意味を理解できる単位」の両立を意識することが大切だったのです。
本文だけを取り込んでも、重要な情報が消えてしまう
もう一つ頭を悩ませたのが、資料自体が持っている「背景情報(コンテキスト)」の欠落です。
例えば、社内サーバーに以下のようなフォルダ構成でファイルが置かれていたとします。
顧客A
└─ 製品X
└─ 不具合対応/
└─ 2026年/
└─ issue_001.txt
この issue_001.txt から「本文のテキストだけ」を抜き出してデータベースに登録してしまうと、「これが『顧客A』の『製品X』に関する不具合資料である」という極めて重要な情報がスッポリと抜け落ちてしまいます。
そこで、本文とは別に、製品名・顧客名・文書種別・作成日付・元のフォルダ階層パスなどをメタデータとしてチャンクに持たせるようにしました。
メタデータの付与は、単に情報を保持するだけでなく、検索時のフィルタリング(絞り込み)でも絶大な効果を発揮します。
例えば「製品Xの不具合について調べたい」という質問に対して、製品Yや製品Zの資料まで同列に検索しに行く必要はありません。あらかじめ「製品X」のタグがついたチャンクだけに絞り込んでからベクトル検索を行うことで、検索精度も応答速度も大幅に向上します。
(※このあたりの「検索の絞り込みとチューニング」については、次回のテーマとして詳しくお話しします。)
LLMには一度に読める量の上限がある(コンテキスト長制約)
ここまでの話を読むと、「なんだ、資料をきれいにチャンク分けしてQdrantにぜんぶ放り込めば解決じゃないか」と思われるかもしれません。しかし、実際にRAGを動かしていると、別の制約に気づきます。
「コンテキスト長が大きいモデルなら全部入る」のでは?
LLMには「コンテキスト長(Context Length)」という制約があります。簡単に言えば、1回の推論(入力+出力)でモデルが同時に処理できるトークン数(文字数の目安)には上限がある、ということです。
「最近のLLMはコンテキスト長が数万〜数十万トークン対応のものも増えているし、関連しそうな資料を片っ端からコンテキストに詰め込めば解決するのでは?」
そう考える方も多いのではないでしょうか。しかし実際は、ローカルLLM環境でこれをやろうとすると、切っても切り離せない技術的ハードルが存在します。それが「KVキャッシュ(Key-Value Cache)」とGPUメモリの消費問題です。
コンテキスト長(スペック値)だけでRAGを設計してはいけない
LLMをローカル環境(自前サーバー + GPU)で動かす場合、GPUメモリ(VRAM)は主に以下の用途で激しく消費されます。
- モデル本体のロード(パラメータをメモリ上に載せる)
- 推論処理時の中間データ保持
- 入力コンテキストに応じた「KVキャッシュ」の展開
特に見落としがちなのが3つ目のKVキャッシュです。入力するコンテキスト長(渡す資料の量)が増えれば増えるほど、このKVキャッシュが占有するGPUメモリ量は一気に膨れ上がります。
つまり、「モデルが最大○万トークン対応だから、○万トークンの資料を渡せる」とは単純には考えられません。実際には、モデルをロードするために必要なメモリに加えて、推論に必要なメモリ、さらに入力コンテキストを保持するKVキャッシュなどの使用量を考える必要があります。
そして、業務運用に耐えるRAGを作るためには、ユーザーからの質問に対して単発の回答生成だけでなく、回答候補を確認したり、条件を変えて再試行したりする処理(多段的推論)を行う必要があります。
発想の転換:「ギリギリ入る量」ではなく「安定して回る量」
✖: 「1回の推論で物理的に詰め込める限界の量」で設計する
〇:「通常推論 + リトライ処理まで含め、GPUメモリを飛ばさずに安定して処理できる量」で設計する
「モデルが入るから」と安易に大量の資料を渡すのではなく、常に必要なチャンクだけを厳選し、コンテキストに渡す設計にすることが重要です。
私たちの開発の中でも、質問に対して回答精度が想定以上に低いスコアの場合、リトライのフローと消費されるコンテキストを見直すことが非常に重要であることを強く感じました。
実際に「資料を全部読ませる」ことはできなかった
ここで重要なのが、「ベクトルデータベース Qdrantに登録できる資料量」と、「1回のLLM推論で読み込ませられる資料量」はまったく別物だという点です。
仮に社内に数百MB〜数GBに及ぶ膨大なドキュメントが存在し、それらすべてをデータベースに格納できたとしても、そのすべてを一度のやり取りでLLMに丸投げすることは物理的に不可能なのです。
だからこそRAGでは、「巨大なデータベースに資料を蓄積しておき、質問に応じて真に必要なパーツ(チャンク)だけをピンポイントで切り出してLLMに渡す」というアーキテクチャを採用します。これはRAGの基本原則ですが、実際にローカル環境でシステムを組んでみると、その理由が「メモリ不足」や「レスポンスの悪化」といった非常に生々しい問題として実感を伴って見えてきます。
LLMの「頭の中(コンテキスト)」にすべての資料を詰め込んでおくのではなく、巨大な「外部の資料庫」を用意し、必要なときに必要な分だけ資料を取り出してくる――これこそがRAGの本質です。
ところが、「必要な資料だけ」の判断が難しい
それなら、「質問に関連しそうな資料を上から10件だけ検索してLLMに渡せば良いのでは?」と思われるかもしれません。しかし、現実はそう単純ではありません。
10件では正解が書かれた資料が含まれず、LLMが「わかりません」と答えてしまうかもしれません。
では20件にするとその可能性はなくなるでしょうか?
50件では?
そうなると正解が書かれた資料が含まれるようになりますが、同時に大量の無関係な資料(ノイズ)も紛れ込みます。その結果、LLMが混乱して誤答(ハルシネーション)を引き起こす原因となります。
さらに、資料(チャンク)1件あたりの文章が長い場合、たとえ数件に絞ったとしても一気にコンテキスト上限を圧迫してしまいます。
つまり、単に「検索結果を何件渡すか(Top-K件数)」という件数の制御だけでなく、「検索結果の合計が何トークンになるか」という全体量の制御を同時に行う必要があります。
RAGのサンプルプログラムや解説を見ると、Top-k で処理しているのをよく見ますが、回答精度にも関わるので、業務で使うことを想定したRAGの場合、ここは慎重に設計する必要があります。
さらに、厄介な問題はほかにも….
ここからは、今回の開発で特に印象に残っている失敗談です。
開発の途中で「回答精度の評価」を行うため、評価用の質問セットを準備しました。これは、社内エンジニアがサポートチームへ寄せる技術的な問い合わせや顧客からの要望、さらには仕様にない質問や勘違いに基づく質問まで、さまざまなケースを想定した全62問の質問集です。
あらかじめ模範回答を用意し、必須・禁止事項の加減点方式で精度を定量評価できるようにしたものです。
この質問セットを使って評価を行い、スコアの低い質問を1件ずつ確認していったところ、ある明確なパターンに気づきました。
「正解の資料はちゃんと検索できているのに、なぜか最終的な回答には反映されていない。」そういった質問が、低スコアのなかのかなりの割合を占めていたのです。
低スコアに潜んでいた「謎の失敗」
最初は、「回答を組み立てているロジックのどこかが間違っているのでは?」と考えました。
原因は「回答内容」ではなく「検証工程への渡し方」
処理の途中経過を1ステップずつ追っていったところ、思いもよらない場所で問題が起きていました。
今回のRAGシステムでは、生成した回答をそのままユーザーに返すのではなく、別のLLMを呼び出して「その回答が本当に根拠文章に基づいているか」を最終検証する工程を挟んでいます。
ところが、この検証用LLMに受け渡す「根拠文章」と「回答文章」を、文字数で機械的にバッサリ切り詰めてしまっていたのです。しかも上限の設定がかなり厳しく、質問によっては根拠文章の2割弱しか検証用LLMに渡っていないケースがありました。
その結果、正しく書けている優秀な回答ほど、検証用LLMから「根拠が確認できない」と判定されて切り捨てられるという状態が発生していました。検証で棄却されると、システムは安全側に倒れて「資料から回答を判断できませんでした」という定型文を返してしまいます。
評価スコアだけを見れば「この質問の回答品質がなぜか異常に低い」ように見えます。しかし実態は、回答自体は正しく作られていたにもかかわらず、チェック役に渡す資料を削りすぎていたことが原因でした。
「定型文が返っている」と気づくまでが難しい
この問題が特に厄介だったのは、システムエラーとしてクラッシュしない点です。ユーザー側には「申し訳ありません。資料から回答を判断できませんでした」といった、一見もっともらしい文章が返ってきます。
一見すると「LLMが資料を読んだけれど、答えられなかった」ように見えます。しかし実際には、正しい回答は一度組み立てられていたのに、検証工程で捨てられていた。この違いは、最終回答だけを見ていてもなかなか気づくことができません。
そこで、単に最終回答だけをチェックするのではなく、
- 検索結果(取得ドキュメント)
- 回答生成に渡した根拠の文章量
- 検証工程に実際に渡った文字数
- 検証LLMの判定成否
- リトライの発生有無
- 定型文への切り替わりフラグ
- 最終回答
といった一連のパイプラインを追跡できる詳細なログの必要性を痛感しました。
RAGでは「精度」と「処理できる量」が別問題ではない
この経験から、私たちはRAGの評価についても考え方を少し変えました。例えば「検索精度が上がった」としても、検索結果を増やしたことでLLMに渡すコンテキストが大きくなりすぎれば、回答生成で失敗する可能性があります。
逆に「LLMに渡す資料を減らした」ことでコンテキストエラーは減っても、今度は正解資料を取りこぼすかもしれません。
つまり、検索精度とコンテキストサイズと推論の安定性は、それぞれ独立した問題ではありません。互いにトレードオフになることがあります。
「たくさん検索すれば安心」ではない
RAGを作り始めた頃は、「正解資料を取りこぼさないためには、検索結果を多めにLLMへ渡せばいい」と考えがちです。しかし実際には、こういう連鎖が起こり得ます。
検索候補を増やす
↓
LLMへの入力が増える
↓
KVキャッシュの消費量も増える
↓
推論可能な余裕が減る
↓
場合によってはコンテキストオーバーフロー
さらに、Fallbackや再試行など、システム側で複数回のLLM呼び出しを行う設計なら、その処理全体を考える必要があります。
意識するようになった考え方
「検索できるだけ検索する」のではなく、「その後のLLM処理まで安定して完了できる範囲で検索する」という考え方を意識することが重要。
データ整備は「検索できる形」にするだけではない
ここまでの経験から、RAGのIngest工程で考えるべきことは、単純な「ファイルを読む→チャンクにする→ベクトル化する」だけではないと分かりました。実際には、次のところまで考える必要があります。
社内資料
↓
文字コード・形式を確認
↓
意味のある単位に分割
↓
製品・顧客・文書種別などの
メタデータを付与
↓
検索可能な形で保存
↓
質問に応じて候補を取得
↓
LLMのコンテキストに収まる量へ調整
↓
回答生成
つまり、「LLMに食わせる前のデータ整備」と「LLMが実際に食べられる量の制御」はセットで考える必要があるということです。
さらに、「検索」という難題
ここまで、ベクトルデータベースへの格納方法やコンテキストの制御手法について解説してきました。ここまで来たら、いよいよRAGの肝である「ユーザーの質問に対して、本当に必要な資料をどうやって探し出すか?」という本格的な問題に向き合うことになります。
開発を始めた当初、私たちは「ベクトル検索で『意味が近い資料』さえ見つけられれば、それで十分機能するのでは?」と甘く見積もっていました。
しかし、私たちが扱う組み込み製品の現場では、そう単純にはいきませんでした。
- 製品名
- 型番
- バージョン
- 関数名
- エラーコード
- 不具合管理番号
など、こうした情報は、「意味が似ているかどうか」ではなく、「その文字列が完璧に一致しているか」が決定的に重要になります。
ベクトル検索の「なんとなくニュアンスが近い」だけでは、ピンポイントで正解のドキュメントに辿り着くことができません。
次回は、この「検索(リトリーバル)」の精度向上に焦点を当てます。そしてここでも、「検索方法を一つ追加すれば、精度は上がる」という単純な話ではありませんでした。試行錯誤の続きは、次回詳しく紹介したいと思います。
まとめ
今回、私たちがRAGのデータ整備とコンテキスト制御を通して学んだ知見を整理すると、以下のようになります。
データ整備とチャンク分割の勘所
- 社内資料(マニュアルやサポート履歴)は、そのまま登録しても検索には適さない
- チャンク分割は単なる「文字数区切り」ではなく、情報の意味のまとまりを意識する必要がある
- 本文のテキストだけでなく、製品名や文書種別などの「メタデータ」の付与が精度を左右する
- 「DBに登録できる資料量」と「LLMが一度に読める(活かせる)資料量」は全くの別物
- 単に仕様上のコンテキスト長を見るだけでなく、モデルのロード、推論、KVキャッシュ等、それぞれのメモリ消費量を統合的に考える必要がある
- リトライやFallback処理を含めた「実際の実行パス」で、トータルでどれだけコンテキストを消費しているか追跡する
- 検索結果の件数を増やせば精度が上がるわけではなく、LLMが安定して処理できる限界量とのバランスが不可欠
- 検証工程で文章を削りすぎた結果、「せっかく正しい回答を作ったのに定型文で捨てられる」という想定外の落とし穴を経験した
特に印象に残ったのは、RAGでは「大量の資料をDBに持っていること」と「LLMにそれを正しく読ませられること」はイコールではないということでした。
たとえ大規模なデータベースを用意しても、モデルが正しく読み込める適切なサイズに切り出し、最適な形でコンテキストへ流し込めなければ、正しく機能しません。膨大な社内資料から「本当に必要な情報だけをピンポイントで取り出す」という部分こそ、RAGの重要な役割だといえます。
そして次回は、その取り出し方に踏み込みます。「意味が近い」だけでは、組み込み製品の検索はなぜうまくいかないのか。実際に検索方式を変えてみたところ、期待とは逆にスコアが下がったケースもありました。次回は、その失敗も含めて紹介します。
