🎛️

【図解】いつRAG?いつファインチューニング? — LLM強化の使い分け

に公開

はじめに

ルミナイR&Dチームの栗原です。
前回の記事では、レビュー論文 “Retrieval-Augmented Generation for Large Language Models: A Survey” (Gao et al., 2024) の Fig.2 / Fig.3 を手がかりに、

  • RAG の標準フロー(Indexing / Retrieval / Generation)
  • Naive RAG / Advanced RAG / Modular RAG の違い
  • Advanced RAG の最小実装例

をざっくり整理しました。

しかし、実務に落とし込もうとすると、次の疑問が必ず出てきます。

  • これは RAG で解くべきなのか?
  • それとも、プロンプト設計だけで何とかすべき?
  • あるいは、ちゃんとファインチューニングした方がいい?

この「いつどの強化手段を選ぶか?」という悩みに対して、Gao et al. の Fig.4 は、LLM 強化の代表的な手段を 2 軸で整理した “地図” を与えてくれます。

参照:“Retrieval-Augmented Generation for Large Language Models: A Survey” — Gao et al., 2024

https://arxiv.org/abs/2312.10997v5

この記事で学べること

  • LLM 強化の代表的な 3 パターン(Prompt / Fine-tuning / RAG)の守備範囲
  • 「外部知識がどれくらい必要か」「モデル自体をどこまで変えたいか」という 2 軸での考え方
  • 自分のユースケースで、まずどの手段から試すかのざっくり判断基準
  • Prompt 版と RAG 版を切り替える、最小の実装イメージ(Python + OpenAI)

1. LLM強化の4パターンを1枚で見る


図1. LLM強化の4パターン(Gao et al., 2024 Fig.4 より)

この図では、LLM を強化する手段が、次の 2 軸でマップされています。

  • 横軸:External Knowledge Required
    → どれくらい 「外部の知識ベース」 に依存するか
  • 縦軸:Model Adaptation Required
    → どれくらい 「モデル自体を書き換える必要があるか」

この 2 軸で眺めると、だいたい次のようなゾーンに分かれます。

  • 左下:Prompt Engineering / In-context Learning / Tool use
    モデルは素のまま。外部知識もほとんど使わず、プロンプトやツール指定だけで頑張るゾーン。
  • 左上:Fine-tuning(Instruction / Task / Domain)
    外部知識はそこまで求めず、モデル自体をタスク・ドメインに合わせてチューニングするゾーン。
  • 右側:RAG(Naive / Advanced / Modular)+ Fine-tuned RAG
    外部知識ベースをガッツリ用意して、検索・再ランク・エージェントなどを組むゾーン。

つまり図1は、

外部知識にどれだけ頼るか」と「モデル自体をどこまで変えるか」で、
Prompt / Fine-tuning / RAG を切り分けよう

という考え方を図解したものです。

この「地図」を頭に入れた上で、

  • 2章:一番軽い手段である Prompt Engineering
  • 3章:モデルを書き換える Fine-tuning
  • 4章:外部知識を足す RAG(+ミニ実装)

という順番で見ていきます。

2. Prompt Engineering:一番軽い“強化”

最初の選択肢は、いちばん軽い Prompt Engineering です。

2.1 Prompt Engineeringとは?

  • system / user メッセージの書き方や
  • few-shot の入力例(In-context Learning)

で、「モデルに何をさせたいか」 を丁寧に指定するアプローチです。

ここでは、

  • モデルのパラメータは変えない
  • 外部の知識ベース(ベクタDBなど)も使わない

という意味で、もっともコストが小さい強化だと言えます。

2.2 向いているケース

たとえば、次のような状況では、まず Prompt から試すのが合理的です。

  • モデルがもともと持っている 一般知識で十分 なタスク
    • 例:一般的な文章校正、英作文の添削、簡単な要約 など
  • やりたいことが「言い方・ロールプレイ・フォーマット」の制御
    • 例:敬語で答える、弁護士風に答える、JSON 形式で答える
  • 個人利用や、まだ要件が固まっていない 小さな PoC

このフェーズでは、プロンプトをどんどん試しながら、

  • そもそもタスクとして成立するのか
  • どんなフォーマット・ガイドラインがあると使いやすいのか

を探っていくのが主目的になります。

2.3 限界

もちろん、Prompt Engineering にも限界があります。

  • モデルが学習していない 最新の知識社内固有の情報 は、いくらプロンプトを工夫しても出てきません。
  • プロンプトが長くなりすぎると、
    • トークンコストが増える
    • 他のメンバーが読みにくくなる
    • どこを直すと何が変わるのか、管理しづらくなる
      といった「プロンプト地獄」が発生しがちです。

一般知識で済むうちは Prompt だけで頑張る」のはよいのですが、
最新情報・ローカル情報・大量の文書 が絡み始めたら、次の選択肢を検討するタイミングです。

3. Fine-tuning:モデルの中身を書き換える

2つめの選択肢は、Fine-tuning(ファインチューニング) です。

3.1 Fine-tuning のざっくり分類

論文や実務では、だいたい次のような形で分類されます。

  • Instruction tuning
    • 汎用モデルに「指示に従う能力」を教え込むチューニング
    • すでに多くの公開モデルで実施済み
  • Domain-specific tuning
    • 法律・医療・金融など、特定ドメインの専門用語やスタイルに寄せる
  • Task-specific tuning
    • テキスト分類、タグ付け、スパン抽出など、特定タスクに特化したモデルを作る

いずれも共通しているのは、

「サンプルデータ(入力と望ましい出力)をたくさん用意して、モデルのパラメータ自体を更新する」

という点です。

3.2 向いているケース

Fine-tuning が効きやすいのは、たとえばこんな状況です。

  • ラベル付きデータが数千〜数万サンプル単位である
    • 例:問い合わせメール+カテゴリラベル、ログ+アラート種別
  • 出力フォーマットが厳密
    • 例:DSL、JSON、特定のテンプレートに沿ったレポート
  • RAG で知識は補った上で、さらに
    • 「分類精度をあと数%上げたい」
    • 「スタイル・口調を特定のブランドに合わせたい」

といった 「振る舞い」 を調整したいときです。

3.3 限界・注意点

その一方で、Fine-tuning にはそれなりのコストが伴います。

  • 学習・評価・デプロイのパイプラインが必要
  • モデル自体のバージョン管理・権限管理が発生する
  • チューニングを間違えると、もとの能力(汎用知識・推論力)を劣化させてしまう

そして何より大事なのは、

最新知識の問題は別だということです。

モデルをいくらチューニングしても、学習データに含まれていない 新しい社内規定や最新ニュース はそのままでは出てきません。この部分は、次の RAG が得意な領域になります。

4. RAG:外部知識で補強する

3つめの選択肢が、前回から見てきた RAG(Retrieval-Augmented Generation) です。

4.1 Fig.4の中でのRAGの位置

Fig.4 上での RAG のポジションを、ざっくり言い直すと:

  • 外部知識必要度:高
    • 社内ドキュメント、ナレッジベース、長い PDF、Web ページ群 など
  • モデル改造必要度:低〜中
    • retriever を学習したり、Reranker を足すことはあるが、
      LLM 本体は API のまま使うケースも多い

というゾーンになります。

前回の記事では、Fig.2 / Fig.3 を使って

  • Indexing(チャンク化&ベクタ化)
  • Retrieval(埋め込み検索 / ハイブリッド検索 / 再ランク)
  • Generation(コンテキストを束ねて LLM へ)

という標準フローを見ました。
今回はそれを前提として、

Promptだけで聞く のと RAG を噛ませてから聞く のでは、構造として何が違うのか?」

を、ミニ実装で見てみます。

4.2 どんなときにRAGを選ぶか

RAG がフィットしやすいのは、おおよそ次のような状況です。

  • 法務・規約・設計書・議事録・FAQ など、長い文書群を参照したい
  • データは頻繁に更新されるが、モデル自体は頻繁に作り直したくない
  • 「このページのどこを根拠にしたか」を 引用として明示したい

ここでは、

モデルではなく、データを中心に改善する

というスタンスが重要です。

  • 間違っているときは プロンプトではなく文書側 を直す
  • 対応範囲を広げたいときは 文書やインデックスを追加 する

というサイクルが回せるようになると、運用がだいぶラクになります。

4.3 実装ミニ例:Prompt版とRAG版を切り替える

最後に、Prompt だけで聞く場合RAG を噛ませてから聞く場合 を、
mode パラメータで切り替えられる最小コードを書いてみます。

※ 前回の Advanced RAG コードよりかなり簡略化しています。
ベクタDBもローカルの FAISS だけです。

"""
LLM強化手段のミニ実装:
- mode="prompt": 生のLLMに直接聞く(Prompt Engineering)
- mode="rag":    手元のドキュメントを簡易RAGで検索してから聞く

準備:
  - 環境変数 OPENAI_API_KEY を設定
  - pip install openai sentence-transformers faiss-cpu
"""

import os
from dataclasses import dataclass
from typing import List, Literal

import numpy as np
import faiss
from sentence_transformers import SentenceTransformer
from openai import OpenAI

# ===== 1. サンプル知識ベース =====
DOCS = [
    {
        "id": "policy",
        "title": "社内セキュリティポリシー概要",
        "text": "社内データを外部に送信する場合は、必ず情報セキュリティ部門の承認を得ること。"
    },
    {
        "id": "product",
        "title": "新製品Aのリリースノート",
        "text": "新製品Aでは、2025年10月リリースよりAPIの認証方式がOAuth2に変更された。"
    },
]


# ===== 2. 簡易RAGインデックス =====
@dataclass
class RagIndex:
    texts: List[str]
    ids: List[str]
    model: SentenceTransformer
    index: faiss.IndexFlatIP

    @classmethod
    def build(cls, docs: List[dict], model_name: str = "all-MiniLM-L6-v2") -> "RagIndex":
        # 「タイトル+本文」を1チャンクとして埋め込み
        model = SentenceTransformer(model_name)
        texts = [d["title"] + "。 " + d["text"] for d in docs]
        ids = [d["id"] for d in docs]
        embs = model.encode(texts, normalize_embeddings=True).astype("float32")

        index = faiss.IndexFlatIP(embs.shape[1])  # 内積類似度
        index.add(embs)
        return cls(texts=texts, ids=ids, model=model, index=index)

    def search(self, query: str, k: int = 3):
        qv = self.model.encode([query], normalize_embeddings=True).astype("float32")
        D, I = self.index.search(qv, k)
        results = []
        for score, idx in zip(D[0], I[0]):
            if idx == -1:
                continue
            results.append(
                {"id": self.ids[idx], "text": self.texts[idx], "score": float(score)}
            )
        return results


# ===== 3. LLM呼び出し共通部分 =====
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

SYSTEM_BASE = "あなたは日本語で丁寧に回答するアシスタントです。"


def call_llm(prompt: str, system: str = SYSTEM_BASE) -> str:
    resp = client.responses.create(
        model="gpt-4o-mini",
        input=[
            {"role": "system", "content": system},
            {"role": "user", "content": prompt},
        ],
        max_output_tokens=400,
        temperature=0.2,
    )
    return resp.output_text


# ===== 4. モード別の回答関数 =====
Mode = Literal["prompt", "rag"]

rag_index = RagIndex.build(DOCS)


def answer(query: str, mode: Mode = "prompt") -> str:
    if mode == "prompt":
        # --- Prompt Engineering モード ---
        prompt = (
            "次の質問に、あなたの一般知識だけを使って答えてください。\n\n"
            f"質問: {query}"
        )
        return call_llm(prompt)

    elif mode == "rag":
        # --- RAG モード ---
        hits = rag_index.search(query, k=3)
        context = "\n".join(f"- [{h['id']}] {h['text']}" for h in hits)

        prompt = f"""次の質問に、与えられた社内ドキュメントに基づいて答えてください。
必ずドキュメントの内容の範囲で答え、推測しすぎないでください。

[ドキュメント抜粋]
{context}

[質問]
{query}

回答の中で、参照したドキュメントIDを丸括弧で示してください(例: (policy))。
"""
        return call_llm(prompt)

    else:
        raise ValueError(f"Unknown mode: {mode}")


# ===== 5. 動作確認 =====
if __name__ == "__main__":
    q = "新製品AのAPI認証方式について教えてください。"

    print("=== Promptモード ===")
    print(answer(q, mode="prompt"))

    print("\n=== RAGモード ===")
    print(answer(q, mode="rag"))

このスクリプトを実行すると、次のような違いが出ます:

  • Promptモード
    → モデルが知っている範囲で「OAuth2 かもしれない」と推測するものの、日付や社内ポリシーまでは分からない。
  • RAGモード
    DOCS に入れておいたリリースノートから
    「2025年10月以降は OAuth2 で認証する」といった、より具体的かつ根拠付きの回答になる。

コードとしては、

answer(query, mode="prompt")  # そのままLLMへ
answer(query, mode="rag")     # search → コンテキスト付与 → LLMへ

if 文 1つ分の違いしかありませんが、
Fig.4 の観点では、

  • 左下(Prompt-only)
  • 右下〜右中(RAG)

という まったく別のゾーン を切り替えていることになります。

まとめ

この記事では Gao et al. の Fig.4 を手がかりに、

  • Prompt:モデルも外部知識もいじらず、プロンプト設計だけで頑張るゾーン
  • Fine-tuning:ラベル付きデータを使って、モデルの振る舞いを直接書き換えるゾーン
  • RAG:モデルはそのままに、外部ドキュメントを検索して補強するゾーン

という 3 パターンの大まかな守備範囲を整理しました。

次回は Gao et al. の Fig.5 を手がかりに、Iterative / Recursive / Adaptive Retrieval といった「エージェント寄りの RAG」のパターンを、もう少しだけ掘り下げる予定です。


【現在採用強化中です!】

  • AIエンジニア
  • PM/PdM
  • 戦略投資コンサルタント

▼代表とのカジュアル面談URL
https://pitta.me/matches/VCmKMuMvfBEk

ルミナイ - 産業データをLLM Readyにするための技術ブログ

Discussion