【図解】いつ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
この記事で学べること
- 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 のまま使うケースも多い
- retriever を学習したり、Reranker を足すことはあるが、
というゾーンになります。
前回の記事では、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
Discussion