9月23日、Shittu Olumideが「RAG vs. Fine-Tuning for Domain Adaptation: When to Use Which」と題した記事を公開した。この記事では、RAGとファインチューニングをそれぞれいつ使うべきかという判断基準と、実装まで踏み込んだ決定フレームワークについて詳しく紹介されている。
LLMの実用化が進む中で「RAGかファインチューニングか」という議論は至るところで起きているが、本記事はその議論が「間違った抽象レベルで行われている」と指摘することから始まる。元記事によると、本番環境のLLMデプロイメントの約60%が両者を組み合わせて使用しているという。これは意思決定できなかったからではなく、両者がそもそも異なる問題を解くツールだからだ。
RAGとファインチューニングの本質的な違い
この議論で最も重要な点を先に押さえる。
RAGはモデルを変えない。ファインチューニングは知識を増やさない。
RAG(検索拡張生成)は、モデルの重みには一切手を加えない。ユーザーのクエリに対して関連ドキュメントを検索し、コンテキストウィンドウに流し込んでから生成させる仕組みだ。モデルは「暗記から答える」のではなく「手元の資料を見て答える」状態になる。つまりRAGが真価を発揮するのは、「大量の、または頻繁に更新される情報を扱う」ケースだけだ。
一方、ファインチューニングはモデル自体を変える。入出力の事例をもとに重みを更新し、特定の振る舞いをモデルのデフォルトとして焼き込む。現在の主流はLoRA / QLoRAで、ベースモデルの総パラメータ数の1%未満のアダプタを学習するだけで済む(LoRAの概要についてはHugging Face PEFTドキュメントも参照されたい)。コストは数百ドル、時間は数時間程度まで圧縮できる。
そしてここが最重要点だ:ファインチューニングは事実知識を信頼性高く追加しない。 医療文献で学習させたモデルが、その文献の事実を正確に「知っている」わけではない。調整されるのはスタイル・構造・パターン認識であり、細粒度の事実の記憶は不安定だ。ファインチューニングは「知識ツール」ではなく「振る舞いツール」である。
実装例①:RAGパイプライン(インシデント対応ナレッジベース)
ユースケースは、エンジニアリングチームがインシデントランブックやポストモーテムに対して自然言語で質問できるシステムだ。ドキュメントは毎週更新され、深夜2時の障害対応で「この答えの根拠はどこか」をトレースできる必要がある。RAGの典型的な適用例だ。
必要なもの:
- Python 3.10+
pip install scikit-learn anthropic- Anthropic APIキー
チャンク分割と検索インデックスの実装:
# retrieval.py
import re
from dataclasses import dataclass
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
@dataclass
class Chunk:
doc_id: str
title: str
text: str
def chunk_document(doc: dict, max_sentences: int = 2) -> list[Chunk]:
sentences = re.split(r"(?<=[.!?])\s+", doc["text"])
chunks = []
for i in range(0, len(sentences), max_sentences):
chunk_text = " ".join(sentences[i:i + max_sentences])
chunks.append(Chunk(doc_id=doc["id"], title=doc["title"], text=chunk_text))
return chunks
class RetrievalIndex:
def __init__(self, documents: list[dict]):
self.chunks = [chunk for doc in documents for chunk in chunk_document(doc)]
self.vectorizer = TfidfVectorizer()
self.chunk_vectors = self.vectorizer.fit_transform([c.text for c in self.chunks])
def search(self, query: str, top_k: int = 3) -> list[tuple[Chunk, float]]:
query_vector = self.vectorizer.transform([query])
scores = cosine_similarity(query_vector, self.chunk_vectors)[0]
ranked = sorted(zip(self.chunks, scores), key=lambda pair: pair[1], reverse=True)
return ranked[:top_k]
検索にはニューラル埋め込みモデルではなくTF-IDF+コサイン類似度を使っている。外部APIもモデルのダウンロードも不要で、小規模な本番RAGシステムではこのアプローチが実際に使われているケースもある。
生成ステップでは、ソース引用を強制するシステムプロンプトを設ける:
# generate.py
SYSTEM_PROMPT = """You are an internal engineering assistant. Answer only using \
the provided source excerpts. Cite the source document ID for every claim in \
square brackets, like [runbook-db-failover-001]. If the sources don't contain \
the answer, say so explicitly rather than guessing."""
すべての主張にドキュメントIDの引用を義務付けることで、回答の監査可能性を担保している。「ページングはチームリードに直接行く」という記述がどのランブックに由来するか、その場でトレースできる。
実装例②:ファインチューニング(構造化出力の安定化)
こちらは性格の異なるユースケースだ。金融サービス企業が顧客クレームを社内独自のタクソノミー(BILLING_DISPUTE、UNAUTHORIZED_TRANSACTION、ACCOUNT_ACCESS、FEE_INQUIRY、CARD_FRAUD_SUSPECTED)に分類し、下流のチケットシステムが依存する厳密な構造化出力を毎回安定して返す必要がある。
ポイントは「新しい事実は不要」だという点だ。モデルに必要なのは、この企業固有の語彙と出力契約を「デフォルトの振る舞い」として習得させること。システムプロンプトで「お願い」するだけでは安定性に限界がある——これがファインチューニングを選ぶ根拠になる。
元記事ではこのユースケースに対するファインチューニングのコード例は示されていない。概念的な説明と判断基準の提示にとどまっており、実装の詳細についてはHugging Face TRLのSFTTrainerドキュメントやOpenAIのファインチューニングガイドが参考になる。
6つの判断フレームワーク
記事では、RAG・ファインチューニング・両者の組み合わせを選ぶための6点の判断基準が提示されている:
- 情報の更新頻度 — 週次以上で変わるならRAG。モデルの再学習サイクルは通常数日〜数週間かかるため、鮮度が命のユースケースとは根本的に相性が悪い。
- 情報量 — モデルのコンテキストウィンドウに収まらないならRAG。数万件のドキュメントをプロンプトに詰め込むことはできないが、検索で絞り込めばよい。
- 回答の根拠トレーサビリティ — ソースの引用が必要ならRAG。規制対応や監査ログが求められる業種では、「どの文書に基づいた回答か」を示せることが必須要件になる。
- 出力フォーマットの一貫性 — 厳密な構造化出力が必要ならファインチューニング。プロンプトエンジニアリングだけでは、スキーマの逸脱をゼロにするのは難しい。
- ドメイン固有の振る舞い — スタイル・トーン・語彙の統一が必要ならファインチューニング。「この企業らしい文体」や「業界特有の略語の使い方」はモデルに焼き込む方が安定する。
- 両方に当てはまるか — 元記事によると、本番環境の約60%はここに該当するという。最新情報へのアクセス(RAG)と一貫した振る舞い(ファインチューニング)を同時に求めるシステムが、実際には多数派だ。
「どちらか」ではなく「何の問題を解いているか」を先に問うべきだ、というのが本記事の核心だ。
詳細はRAG vs. Fine-Tuning for Domain Adaptation: When to Use Whichを参照していただきたい。