localaiforum
ログイン
ガイド › モデルの活用

RAG:モデルにあなたのデータを与える

LLMの知識は学習カットオフで凍結されたが、あなたの文書は毎週変わる。検索拡張生成(RAG)はそれを解凍する。クエリ時にデータの関連する切片を取得し、プロンプトに貼り、モデルに根拠から引用付きで答えさせる。推論はモデルが、記憶はインデックスが担当。

パイプライン、1つの実例

12,000トークンの整備マニュアルを端から端まで通す。

  1. **チャンク化。**まずヘッダーで分割(セクションが無傷で生き残る)し、次に約400トークンを超えるセクションを再分割し、約60トークン(10〜15%)のオーバーラップを保持。妥当なチャンクサイズは200〜800トークン。結果:約35チャンク。
  2. **埋め込み。**各チャンクが1ベクトルになる——bge-m3はチャンクごとに1,024個のfloatを出す。CPUでマニュアル全体に数秒。
  3. **インデックス。**ベクトル+テキスト+メタデータ(ソースファイル、ヘッダーパス)をベクトルストアに格納。
  4. **検索。**質問を埋め込み、コサイン類似度で上位k = 5チャンクを取得(4〜10が正気の範囲)。
  5. **生成。**プロンプトを組み立てる。指示、IDタグ付きの5チャンク、質問。

意味境界でのチャンク化は固定ウィンドウに勝る。ヘッダー行から切り離された表や、ステップ4の途中で切られた番号付き手順は、エンベッダがどれほど優秀でも取得不能なゴミになる。

即座に元を取れる追加が2つ。ヘッダーパス(manual › §4 › Bolts)をメタデータとして保存し、正確にフィルタ・引用できるようにすること。そして、クエリをユーザーが打ったままの形で埋め込むこと——前処理の「きれいにする」は大抵、検索を悪くする。

埋め込み:多言語という要件

「似ている」の意味を決めるのはエンベッダ。韓国語と韓国語/英語混在コーパスでは選択肢は狭い——英語専用エンベッダは韓国語クエリで黙って失敗し、もっともらしいが間違ったチャンクを返す。

モデル 次元 備考
bge-m3 1024 多言語、韓国語が強い——安全なローカルデフォルト
nomic-embed-text-v1.5 768 英語は堅実、長いコンテキスト
all-MiniLM-L6-v2 384 高速、英語中心、韓国語に弱い

埋め込みモデルは小さい(0.1〜0.6Bパラメータ)。CPUコア1個が数千チャンクを咀嚼する——CPU推論。コーパスは一度埋め込み、クエリは質問時に埋め込む。エンベッダはインデックスごとに固定すること。インデックス途中でモデルを交換すると、異なる空間のベクトルが混ざり、類似度スコアがノイズになる。

ベクトルストア

ストア とき
sqlite-vec 単一ユーザー、最大約100万ベクトル、運用ゼロ——ファイル1つ
ChromaDB メタデータフィルタ付きのローカルファースト試作
Qdrant 本番。フィルタリング、ハイブリッド検索、スケール

3つすべてローカルで動く。sqlite-vecから始め、実際に壁に当たったときに移動すること。その前ではなく。

プロンプトの組み立て

以下の抜粋のみから答えること。すべての主張を[n]として引用すること。
抜粋に答えが含まれない場合は「文書にない」と返信すること。

[1] (manual §4.2) M8ボルトは給油後12 Nmで締結...
[2] (manual §7.1) ストレッチボルトは再使用しないこと...

質問: M8ボルトに適用される締付トルクは?

引用により、ユーザーは信じる代わりに検証できる。「文書にない」出口が重要。それがないと、モデルは形式で失敗する代わりに答えをでっち上げる。

ハイブリッド検索とリランキング

純粋なベクトル検索は、キーワード検索が釘付けにする完全一致文字列——エラーコード、関数名、部品番号——を取りこぼす。ハイブリッド = BM25 + ベクトルを相互ランク融合で統合。ほぼ無料で、単独より確実に優れる。

品質のためにはリランカーを追加。20〜50候補を取得し、クロスエンコーダ(bge-rerankerファミリー——bge-m3と同じ多言語の物語)で上位5件へリランク。リランカーはチャンクとクエリを一緒に読む。単一ベクトルのエンベッダにはできないこと。大半のニーズをカバーする安いレシピ:ハイブリッドで25取得、5へリランク。

診断:検索ミスか生成ミスか

何かをチューニングする前に、1つの質問に答えること。正しいチャンクは取得されたか。

  1. 失敗するクエリの上位kチャンクをスコア付きで表示。
  2. 答えを含むチャンクがリストにないなら検索の問題——チャンク化、埋め込みを修正するかkを上げる。
  3. リストにあるのに答えが間違っているなら生成の問題——より大きいモデル、より良いプロンプト、妨害を押し出すリランク。

この1つのテストが、パイプラインの間違った半分のチューニングに費やす時間を節約する。

失敗モード

症状 原因 修正
正しい文書が検索されたのに誤答 上位k内の妨害 リランク。kを縮小
答えは文書内にあるのに出てこない 上位k飢餓または悪い境界 kを上げる。ヘッダーでチャンク化
自信満々だが古い答え インデックスが文書より古い 変更時に再インデックス
韓国語クエリが英語文書を取りこぼす 単一言語埋め込み bge-m3
引用が誤ったチャンクを指す チャンクがソースメタデータを失った チャンクごとにソース+ヘッダーを保存

雰囲気ではなく測る

既知の答えを持つ20〜50の実質問のゴールデンセットを維持する。2つの数値を別々に採点。検索ヒット率(正しいチャンクが上位kにいるか)と回答正確性。チャンクサイズ、エンベッダ、kの変更ごとに追跡。ゴールデンセットのないRAG作業は占星術。

代わりにファインチューニングするとき

ファインチューニングは挙動——形式、スタイル、用語——を変える。事実を追加する方法としては貧弱。モデルはチューニングされたものを引用できず、新しい文書は新しい学習実行を意味する。経験則:事実 → RAG、挙動 → LoRA、両方はプロンプトを使い果たしたときだけ:ファインチューニング。

ローカルスタック、最小から

Open WebUIはドキュメントアップロードとハイブリッド検索を内蔵する——そこから始めればパイプラインコードを書かずに済むかもしれない。RAGの正直な核は約50行。フレームワークが何をしているか知るために一度書くこと。

chunks = chunk_by_header(read("manual.md"), max_tokens=400, overlap=60)
index = [(embed(c), c) for c in chunks]          # 一度だけ実行

def answer(question):
    hits = topk_by_cosine(embed(question), index, k=5)
    ctx = "\n\n".join("[{}] {}".format(i, c) for i, (v, c) in enumerate(hits))
    prompt = TEMPLATE.replace("{ctx}", ctx).replace("{q}", question)
    return chat(prompt)                           # 小さいローカルモデルで十分

LlamaIndexやLangChainに手を伸ばすのは、権限、定期リフレッシュ、評価ハーネスが必要なとき——上のループのためではない。韓国語文書については特に、多言語埋め込み(bge-m3)は選択制ではなく、ヘッダーベースのチャンク化はmarkdownの###見出しを尊重するのと同じように韓国語文書構造(장/절見出し)を尊重すべきだ。

関連: ファインチューニング · プロンプト · 構造化出力 · CPU推論