RAGシリーズ(1):なぜLLMには外部メモリが必要なのか

Dev.to / 2026/5/2

💬 オピニオンIdeas & Deep AnalysisTools & Practical UsageModels & Research

要点

  • この記事は、LLMの「ハルシネーション(作り話)」の多くが、学習後に固定される知識のカットオフと、情報がないときに流暢な文章でそれらしく埋めてしまう性質に起因すると説明しています。
  • 学習完了後にLLMの「メモリ」が固定されるため、今日の出来事や社内の非公開文書にアクセスできず、結果として無知を認めるか、もっともらしい内容を捏造することになると論じています。
  • 知識の制約に対処する3つのアプローチ(ファインチューニング、長いコンテキスト、RAG)を、その仕組み・適するケース・コストと更新性のトレードオフとともに比較しています。
  • 特に「ファインチューニングは新しい事実を注入するのに最適」という誤解を否定し、ファインチューニングは主に振る舞いや言語パターンを変えるだけで「本をパラメータ内に保存する」わけではないと述べています。
  • RAGは「何を知るか」(外部で更新可能なデータベース)と「どう伝えるか」(モデル側)を分離する考え方として位置づけられ、動作には検索(リトリーバル)の基盤が必要だとしています。

LLMの「ハルシネーション」の背後にある2つの根本原因

大規模言語モデルを扱ってきた人なら、次の2つの状況に必ず遭遇します:

状況1:知識のカットオフ

You: What were our company's Q1 sales figures?
GPT: I'm sorry, my training data only goes up to early 2024 and I have
     no access to your company's internal data.

状況2:ハルシネーション

You: How do I use LangChain's RunnablePassthrough?
GPT: RunnablePassthrough can be enabled by calling .with_config(pass_through=True)...
     (This parameter doesn't exist.)

どちらの問題も、同じ根本原因を共有しています:LLMの知識は、学習時点で凍結されるということです。

学習が完了した瞬間、モデルの「記憶」はその場で固定されます。今日何が起きたのかを知りませんし、社内文書についても何も知りません。そして、調べに行くこともしません——答えられるのは記憶にある内容だけです。記憶が尽きると、無知を認めるか、もっともらしい内容をでっち上げます。

ここでハルシネーションが生まれます:モデルが、自身の知識の空白を流暢な言語で埋めるためです。

3つの解決策と、それらの比較

この問題には、3つのエンジニアリング上のアプローチがあります:

アプローチ メカニズム 向いているケース コスト
ファインチューニング 新しいデータで再学習し、知識をパラメータに「焼き付ける」 固定されたドメインの言語スタイル、出力形式 高価で遅く、更新が制限され、事実の想起が限定的
ロングコンテキスト すべての文書をプロンプトに詰め込む 小さな文書セット、一回限りの問い合わせ トークンコストが指数関数的に増加;極端に長くすると品質が劣化
RAG 問い合わせ時に関連コンテンツを動的に取得し、プロンプトへ注入する 大規模なナレッジベース、継続的に更新されるデータ 検索(リトリーバル)のインフラが必要

よくある誤解:ファインチューニングは、新しい事実を注入するのが得意ではない

ファインチューニングは、モデルの振る舞いのパターンや言語スタイルを変えます——パラメータの中に「本を保存する」わけではありません。実験では一貫して、特定のQ&Aペアでファインチューニングしても関連する質問に対する精度向上は限定的であり、学習データに誤りが含まれていれば、モデルはその誤りを自信満々に繰り返すことが示されています。

RAGの中核的な利点は、「何を知るべきか」から「どう伝えるか」を分離することです:

  • 知識は外部のデータベースにあり、いつでも更新可能
  • モデルは純粋に理解と生成に集中し、記憶(暗記)には依存しない

ロングコンテキストはRAGの代わりにいつ使うべき?
全文書の総量が約100Kトークン未満で、問い合わせが一回限り(再発しない)であり、APIコストが許容できるなら、ロングコンテキストはしばしばよりシンプルです。ClaudeやGeminiの拡張コンテキストウィンドウは、「丸ごと一冊を詰め込む」ことを本当に現実的にしています。ですが、企業のナレッジベース——何千もの文書があり、更新が継続的で、複数の同時ユーザーがいる——では、RAGがより合理的なアーキテクチャです。

RAGとは何か:オープンブック試験のたとえ

RAG = Retrieval-Augmented Generation(検索拡張生成)。

最も直感的な捉え方は:クローズドブック試験をオープンブック試験に変えること。

クローズドブック(純粋なLLM):学生は純粋に記憶から答えます。暗記していないものは、推測します。

オープンブック(RAG):学生は参考資料を参照できますが、それでも質問を理解し、関連する内容を見つけて、回答を組み立てる必要があります。参考資料は外部のナレッジベースであり、「調べる」ことが検索(retrieval)のステップです。

このたとえは、RAGの重要な性質を2つ明らかにします:

  1. 知識はモデルの外にある — 入れ替えたり更新したりできる
  2. モデルが理解と生成を担う — 検索の後も、モデルはコンテンツを「読む」必要があり、首尾一貫した応答を生成しなければならない

完全なRAGパイプライン

RAGは、インデックス作成フェーズ(一度だけのオフライン処理)と、クエリフェーズ(リアルタイムでリクエストごと)の2つの明確なフェーズで動作します。

RAG Architecture Overview

2フェーズのRAGアーキテクチャ — 上:オフラインのインデックス作成パイプライン;下:リアルタイムのクエリパイプライン;どちらも同じVector DBを共有

インデックス作成フェーズ

このフェーズは、ユーザーの問い合わせが届く前に完了します。これは一度限りの前処理ステップです。

Raw Documents → Document Loading → Text Splitting → Embedding → Vector Database

ステップ1:ドキュメントの読み込み

さまざまな形式の生データから、プレーンテキストへ変換します。PDF、Wordドキュメント、Markdown、Webページ、コード——それぞれの形式には独自のパース上の難しさがあります(PDF内の表や画像は特に扱いが難しいことで有名です)。

ステップ2:テキスト分割(チャンク化)

長いドキュメントをより小さなチャンクに切り分けます。このステップは最終的な品質に大きな影響を与えます。チャンクが大きすぎると検索の精度が下がり、逆に小さすぎると意味的なまとまりが失われます。(チャンク化の戦略は後の別記事で深く扱います。ここでは、なぜ分割するのかだけ理解してください。)

ステップ3:埋め込み(Embedding)

埋め込みモデルを使って、各テキストチャンクを高次元ベクトルに変換します。このベクトルはテキストの意味を捉えます——意味的に近いテキスト同士は、ベクトル空間上で近い位置にあるベクトルを生成します。

ステップ4:ベクトルDBに保存

類似性検索をサポートするベクトルデータベース(Chroma、Qdrant、Weaviateなど)に、すべてのベクトルと元のテキストを保存します。

クエリフェーズ

すべてのユーザーリクエストに対して、リアルタイムに実行されます。

User Question → Embedding → Similarity Search → Retrieved Chunks → Prompt Assembly → LLM → Answer

ステップ1:クエリの埋め込み

ユーザーの質問を同じ埋め込みモデルを使ってベクトルに変換します。

ステップ2:類似性検索

ベクトルデータベースの中から、最も類似したテキストチャンクTop-Kを見つけます。類似度はベクトル空間上の距離によって測定します(コサイン類似度など)。

ステップ3:プロンプトの組み立て

取得したテキストチャンクをユーザーの質問と組み合わせて、完全なプロンプトを作成し、LLMへ送信します。典型的な形式:

You are a knowledge assistant. Answer the user's question based solely on
the reference content provided below.

Reference content:
[Retrieved chunk 1]
[Retrieved chunk 2]
...

User question: [original question]

返却形式: {"translated": "翻訳されたHTML"}

手順4:LLM生成

LLMは、内部パラメータだけから答えを作るのではなく、提供されたコンテキストに基づいて回答を生成します。

実践:100行で作る最小のRAG

フレームワークなし――OpenAI APIだけです。ゼロから動くRAGを実装してみましょう。目的は、フレームワークの抽象化レイヤーによって詳細が隠れてしまうことなく、各ステップが何をするのかを正確に見ることです。

"""
最小のRAG実装――フレームワークなし、OpenAI APIのみ。
インデックス作成+クエリという、RAGの完全なパイプラインを示します。
"""

import numpy as np
from openai import OpenAI

client = OpenAI()  # OPENAI_API_KEY の環境変数が必要です
# ─────────────────────────────────────────
# シミュレーションされた知識ベース:5つの技術ドキュメント
# ─────────────────────────────────────────
DOCUMENTS = [
    "LangChainは、LLMアプリケーションを構築するためのフレームワークであり、チェイニング、メモリ管理、ツール統合を提供します。",
    "ベクトルデータベースは、テキストを高次元のベクトルに変換してセマンティック検索を可能にします。一般的な選択肢にはChroma、Qdrant、Weaviate、Pineconeがあります。",
    "RAG(Retrieval-Augmented Generation)は、生成の前に関連するドキュメントを取得することでLLMの幻覚を減らし、回答の正確性を向上させます。",
    "埋め込みモデルはテキストを固定次元のベクトルに変換し、意味的に類似したテキストがベクトル空間上でより近い位置に配置されます。",
    "ファインチューニングは、特定のデータでモデルを再学習し、その振る舞いを調整します――それ",
    "'は、新しい事実知識を注入するのではなく、出力スタイルを変えるのに最適です。",
]

# ─────────────────────────────────────────
# インデックス作成フェーズ:ドキュメントをベクトルへ変換
# ─────────────────────────────────────────
def build_index(documents: list[str]) -> list[dict]:
    """
    各ドキュメントをベクトルに変換します。
    [{text, embedding}, ...] を返します。
    実運用では、これらをベクトルデータベースに保存します。
    """
    print(f"インデックス作成:{len(documents)} 件のドキュメント...")

    index = []
    for doc in documents:
        response = client.embeddings.create(
            model="text-embedding-3-small",
            input=doc
        )
        embedding = response.data[0].embedding
        index.append({"text": doc, "embedding": embedding})

    print("インデックスの作成に成功しました。")
    return index

def cosine_similarity(vec_a: list[float], vec_b: list[float]) -> float:
    """2つのベクトル間のコサイン類似度です。範囲は-1〜1で、値が大きいほどより類似しています。"""
    a = np.array(vec_a)
    b = np.array(vec_b)
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

返却形式: {"translated": "翻訳されたHTML"}# ─────────────────────────────────────────
# クエリフェーズ:取得 + 生成
# ─────────────────────────────────────────
def retrieve(query: str, index: list[dict], top_k: int = 2) -> list[str]:
    """
    クエリを埋め込み、最も類似した上位top_kのドキュメントを見つけます。
    """
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=query
    )
    query_embedding = response.data[0].embedding

    scored = []
    for doc in index:
        score = cosine_similarity(query_embedding, doc["embedding"])
        scored.append((score, doc["text"]))

    scored.sort(key=lambda x: x[0], reverse=True)
    return [text for _, text in scored[:top_k]]

def generate(query: str, context_docs: list[str]) -> str:
    """
    取得したドキュメント + ユーザーの質問をプロンプトに組み立ててLLMを呼び出します。
    """
    context = "
".join([f"- {doc}" for doc in context_docs])

    prompt = f"""あなたは知識アシスタントです。下記の参照コンテンツのみに基づいて
ユーザーの質問に回答してください。参照コンテンツに関連する情報が
含まれていない場合は、それを明確に述べてください。何かをでっち上げないでください。

参照コンテンツ:
{context}

ユーザーの質問: {query}

回答:"""

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}]
    )
    return response.choices[0].message.content

def rag_query(query: str, index: list[dict]) -> str:
    """完全なRAGクエリのパイプライン。"""
    print(f"
質問: {query}")

返却形式: {"translated": "翻訳されたHTML"}docs = retrieve(query, index, top_k=2)
    print(f"取得した {len(docs)} 個の関連ドキュメント:")
    for i, doc in enumerate(docs, 1):
        print(f"  [{i}] {doc[:60]}...")

    answer = generate(query, docs)
    print(f"\n回答: {answer}")
    return answer

# ─────────────────────────────────────────
# デモを実行
# ─────────────────────────────────────────
if __name__ == "__main__":
    # インデックスを構築(実運用では1回だけ実行すればよい)
    index = build_index(DOCUMENTS)

    # いくつかの質問でテスト
    rag_query("ベクトルデータベースとは何ですか?", index)
    rag_query("RAGとファインチューニングの違いは何ですか?", index)
    rag_query("PythonのGILとは何ですか?", index)  # 知識ベースにない内容 —  отказ(拒否)をテスト

サンプル出力:

ドキュメント5件をインデックス化...
インデックスを正常に構築しました。

質問: ベクトルデータベースとは何ですか?
関連するドキュメントを2件取得しました:
  [1] ベクトルデータベースは、テキストを高次元に変換してセマンティック検索を可能にする...
  [2] 埋め込みモデルはテキストを固定次元のベクトルに変換し、そこではセマンティ...

回答: ベクトルデータベースとは、テキストを高次元のベクトルに変換してセマンティック検索を可能にするデータベースシステムです。一般的な例としては、Chroma、Qdrant、Weaviate、Pineconeなどがあります...

質問: PythonのGILとは何ですか?
関連するドキュメントを2件取得しました:
  [1] LangChainは、LLMアプリケーションを構築するためのフレームワークです...
  [2] ファインチューニングは、特定のデータに対してモデルを再学習します...

回答: 提供された参照コンテンツに基づくと、PythonのGILについては回答できません — 参照資料には関連情報が含まれていません。

最後の質問に注目してください。知識ベースにはPythonのGILに関する情報がなく、LLMは回答を捏造するのではなく、明確に答えられないと言っています。これがRAGによるハルシネーションの制御方法です:プロンプト内の制約により、モデルは取得したコンテンツのみを根拠に回答するよう指示されます。

この実装の制限

上記の100行は、完全なRAGパイプラインを示していますが、明確な欠点もあります:

問題 原因 エンジニアリング上の解決策
ベクトルはメモリ上に存在し、再起動で失われる 永続化がない ベクトルデータベース(Chroma / Qdrant)
長いドキュメントをそのまま渡すとトークン上限を超えてしまう チャンク分割がない テキスト分割の戦略(次の記事)
キーワード照合が貧弱;ベクトル検索のみ ハイブリッド検索がない ハイブリッド検索(このシリーズの後半)
品質の測定がない 評価がない RAGAS評価フレームワーク(このシリーズの後半)

各制限は、今後の各記事で扱うトピックに対応しています。

まとめ

この記事では、3つの中核となる疑問に答えました:

  1. なぜRAG? — LLMの知識カットオフとハルシネーションの両方が、「知識がモデルのパラメータに凍結されていること」に由来します
  2. RAGとは? — クエリ時に外部の知識を動的に取得し、それをプロンプトに注入して、LLMが根拠(エビデンス)に基づいて回答する
  3. RAGと代替手法 — ファインチューニングは振る舞いを変える;長いコンテキストは小規模ドキュメントに有効;RAGは、大規模で継続的に更新される知識ベース向けに作られている

次は:RAGの主要コンポーネントの最初の深掘り — テキストのチャンク分割戦略です。なぜチャンク分割のアプローチがこれほどまでに品質へ劇的な影響を与えるのか、そして4つの主要戦略のうちどれを選ぶべきかを説明します。

参考文献