RAG vs ファインチューニング: LLMアプリケーションでの使い分け完全ガイド

6ヶ月前、チームでカスタマーサポートbotを本番リリースした。そのbotは自信満々に「返品期間は60日です」とユーザーに答え続けた。実際は30日だった。2023年の製品ドキュメントでファインチューニングしたモデルで、ポリシーが変更されていたことを誰も——自分も含めて——確認していなかった。300件のサポートチケットが飛んできて、同僚がQuarterlyのレビューで恥をかいた。

そこからRAGで作り直して、今は安定して動いている。

この経験があるから、「RAGとファインチューニングはケースバイケースです」だけで終わるブログ記事を読むたびに少しイライラする。どちらを選ぶかには具体的な結果が伴う。比較表より、その結果から学んだことの方がよほど役に立つ。

ということで、2つのプロダクトで3つのLLM機能を作り、18ヶ月ほどこのテーマに向き合ってきた経験から書く。

ファインチューニングは見た目より正当化が難しい

ファインチューニングは、モデルに特定の振る舞いをさせたい時の自然な選択肢に見える。自社データで訓練して、ドメイン知識を焼き込む。最初の自分もそう思っていた。

社内コードレビューアシスタントを作った時がその典型だった。18ヶ月分のPRコメント——シニアエンジニアたちの知見が詰まったやつ——があった。これは良い訓練データになると確信していた。

OpenAIのファインチューニングAPI(当時はgpt-3.5-turbo、2024年半ば)を使い、4回ほどの訓練を経てアウトプットがチームのレビュースタイルに近づいてきた。短く直接的なコメント。無駄な褒め言葉なし。社内スタイルガイドへのリンク付き。チームの反応も悪くなかった。

で、そこに新しいエンジニアが3人入ってきて、スタイルガイドも更新された。突然、ファインチューニングしたモデルが古い慣習——明示的に廃止したやつ——を教え始めた。再訓練にはコストがかかり、数日間のイテレーションが必要になる。さらに厄介なのは、新しい訓練データのキュレーション自体がゼロコストじゃないということ。

ここで気づいた本質的な問題:ファインチューニングはモデルに「どう答えるか」を教えるのであって、「何を知っているか」ではない。「特定のトーンで話す」とか「常にJSONの特定フィールドでアウトプットする」という用途なら、複雑さは正当化される。でも「製品について正確に答える」という用途なら、データの陳腐化という上り坂を登り続けることになる。

もう一つ見落としていたこと:評価の問題が思ったより大変だった。ファインチューニングしたモデルが本当に改善されているかどうか、どうやって判断する?評価インフラに、訓練自体とほぼ同じくらいの時間を使ってしまった。それがなければ、基本的に勘で進めることになる。

ここから学んだこと: ファインチューニングが有効なのは、問題が「振る舞い」に関するもの——一貫したフォーマット、トーン、タスク構造——で、その振る舞いが頻繁に変わらない場合。情報が更新され続ける用途には、最初からRAGの方が合っている。

RAGも銀の弾丸ではなかった

返品ポリシーの失敗の後、RAGが全ての答えだという確信を持ちすぎた。少し恥ずかしいが、2ヶ月くらい、あらゆるアーキテクチャ議論で「RAG使えばいい」と言い続けた——RAGが本当に苦手なユースケースに出会うまで。

契約書要約ツールを作っていた。契約書をチャンクに分割してベクターストアに保存し、関連する条項を取得してモデルに要約させる設計。シンプルに見えた。取得部分は動いた。問題は、法的文書には複雑な相互参照がある——「第4.2条(b)で定義されるとおり」——のに、チャンク戦略がその定義を参照元の条項から切り離していたこと。モデルは不完全なコンテキストで答えていたが、それが不完全だとすら認識できていなかった。

RAGの品質はチャンクと取得の戦略で決まる。これをちゃんと説明している入門チュートリアルはほとんどない。最終的には、大きめのチャンク、オーバーラップ、そしてCohereのrerank APIを使った再ランキングのハイブリッドアプローチに移行した。改善はしたが、そこに到達するまで数週間かかった。

日本語特有の落とし穴もある。英語コーパス中心で学習したembeddingモデルを日本語テキストにそのまま使うと、精度が落ちることがある。text-embedding-3-smallは日本語でもそれなりに動くが、日本語文書が大量にあるプロジェクトではintfloat/multilingual-e5-largeを試す価値はある。自分で直接比較したわけではないので、ユースケースによって変わるとは思うが、頭に入れておいて損はない。

それからレイテンシ。すべてのクエリがベクターDBを叩き、類似検索して、チャンクを取得して、コンテキストウィンドウに詰め込み、そこからLLMの推論が走る。自分の環境(Pinecone + GPT-4o)では、モデルの推論時間に加えて800ms〜1.2sが上乗せされた。非同期タスクなら問題ない。インタラクティブに見せたいものだと、体感できる遅さになる。

# 最初に使っていた取得ロジック——再ランキングなし版
from pinecone import Pinecone
from openai import OpenAI

pc = Pinecone(api_key="...")
index = pc.Index("contracts-v2")
client = OpenAI()

def retrieve_and_answer(query: str, top_k: int = 5) -> str:
    # クエリをembedding化
    query_embedding = client.embeddings.create(
        input=query,
        model="text-embedding-3-small"
    ).data[0].embedding

    # シンプルな上位K件取得——再ランキングなし
    results = index.query(
        vector=query_embedding,
        top_k=top_k,
        include_metadata=True
    )

    # 取得したチャンクからコンテキストを構築
    context = "\n\n---\n\n".join(
        r["metadata"]["text"] for r in results["matches"]
    )

    # モデルが参照できるのはここに入れたコンテキストだけ
    # 重要な相互参照がチャンクをまたいで分断されていると、それは見えない
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "提供されたコンテキストのみをもとに回答してください。"
            },
            {
                "role": "user",
                "content": f"コンテキスト:\n{context}\n\n質問: {query}"
            }
        ]
    )

    return response.choices[0].message.content

このtop_k=5の単純な取得を、20件の候補取得から上位5件に再ランキングする2段階アプローチに変えた。その変更だけで、他のどの改善より回答精度が上がった。

ここから学んだこと: RAGは、データが変化する場合、ソース帰属が必要な場合、大規模なドキュメントコーパスを扱う場合に強い。ただし取得パイプラインは本物のエンジニアリング作業だ。その工数を最初から見積もりに入れておくこと。

両方を組み合わせるのが意味を持つケース

両方で失敗を経験してから、組み合わせて使っているチームのケースに注目し始めた。そこで「なるほど」と思えるパターンが見えてきた。

ファインチューニングで振る舞いとスタイルを、RAGで知識を扱う。モデルが「何を知っているか」と「どう答えるか」を分離するイメージだ。

具体例:社内プラットフォームのドキュメントアシスタントを作った。モデルに必要だったのは3つ——(a)特定の構造化フォーマットで常に答えること、(b)スプリントごとに変わるドキュメントに追随すること、(c)幻覚するより「分からない」と正直に言うこと。

適切な不確実性を表現した整形済み回答の例でファインチューニングした小さめのモデルを、クエリ時にドキュメントを取得するRAGパイプラインに組み込んだ。

結果はどちらか単体より良かった。APIの仕様を幻覚しなくなった(コンテキストに正しい情報があるから)。アウトプットのフォーマットが安定した(それを訓練で入れたから)。プロンプト単体では安定させるのが難しい「分からない」の挙動が、ファインチューニング後は信頼できるものになった。

# 組み合わせた実装——シンプルにまとめたバージョン
def answer_with_finetuned_rag(query: str) -> str:
    # 関連ドキュメントを取得
    docs = retrieve_docs(query, top_k=5)
    context = format_context(docs)

    # ファインチューニング済みモデルで一貫性のある回答を生成
    # ft:gpt-3.5-turbo-0125:our-org:docs-assistant:abc123 — 自分たちのチェックポイント
    response = client.chat.completions.create(
        model="ft:gpt-3.5-turbo-0125:our-org:docs-assistant:abc123",
        messages=[
            {
                "role": "system",
                "content": (
                    "あなたはドキュメントアシスタントです。"
                    "提供されたコンテキストのみから回答してください。"
                    "コンテキストに回答がない場合は、その旨を明示してください。"
                )
            },
            {
                "role": "user",
                "content": f"ドキュメントコンテキスト:\n{context}\n\n質問: {query}"
            }
        ],
        temperature=0.2  # フォーマットの一貫性を高めるため低めに設定
    )

    return response.choices[0].message.content

ただ、これが常に正当化されるとは言わない。ファインチューニング済みモデルを運用するということは、そのチェックポイントを自分たちで管理するということ。OpenAIがベースモデルを廃止すれば、再訓練が必要になる。それは本物の運用コストだ。

ここから学んだこと: 組み合わせが複雑さに見合うのは、プロンプトだけでは解決できない明確な振る舞いの問題があり、かつ知識ベースが動的に更新される——その両方が成立する場合のみ。どちらか一方が当てはまらないなら、シンプルな方を選ぶこと。

実際のところ、どちらを選ぶべきか

全部経験した上で、今の自分の判断フレームワークはかなりシンプルになった。3つの質問を順番に答える。

1. 再訓練のサイクルより速くデータが変わるか?
「はい」ならRAGが必要。これはほとんどのプロダクトのユースケースに当てはまる——サポートbot、ドキュメントアシスタント、チームが更新し続けるコンテンツDBを扱うもの全部。

2. プロンプトでは直せない一貫した振る舞いの問題があるか?
具体的に言うと、複数のシステムプロンプトのバリエーションと少数ショット例を試した上でも、アウトプットが安定しない、ということ。「はい」ならファインチューニングを検討する価値がある。本格的なプロンプトエンジニアリングをまだ試していないなら、そちらを先にやること——イテレーションが速くてコストが低い。

3. 性能の差が運用コストに見合うか?
古いベースチェックポイントでファインチューニングしたモデルは、最新のフラッグシップモデルよりトークン単価が安いことがある。1日に何百万回もAPIを叩くなら、再訓練コストを含めても元が取れる計算になることはある。社内ツールを作っているほとんどのチームには当てはまらないが。

正直に言う。自分が見ているほとんどのアプリケーション——これは一般論ではなく、実際に目にしたプロダクトの話として——には、RAGが正しいスタート地点だ。データの鮮度問題だけで、知識検索系のユースケースからファインチューニングは外れる。ファインチューニングに手を出す前に、チャンク戦略、embeddingモデルの選択、取得品質に力を注ぐべきだ。

例外は、プロンプトでは指定しにくい振る舞いが必要な場合——強く制約された出力スキーマ、特殊な推論パターン、安定させる必要のあるトーン要件。その場合はファインチューニングが意味を持つ。

チームの誰かが「ドキュメントをモデルにファインチューニングしよう」と言い出したら——これを一度以上聞いたことがある——「更新された時どうする?」と聞いてみるといい。大抵はそこで整理がつく。

まとめ

今回は RAG vs ファインチューニング: LLMアプリケーションでの使い分け完全ガイ について詳しく解説しました。以下に本記事の重要なポイントをまとめます。

重要なポイント

  • 基本概念:core concept を正しく理解することが、production ready な実装への近道です。
  • 実装方法:Python や TypeScript など主要な programming language でのimplementation が可能で、official SDK や library を活用することで開発効率が向上します。
  • パフォーマンス:response latency や throughput を考慮したsystem design が重要です。load testing と monitoring を組み合わせて最適化しましょう。
  • コスト管理:API usage cost を最小化するため、caching strategy や rate limiting の実装を検討してください。
  • セキュリティ:API key management、input validation、output sanitization など、security best practices を必ず遵守してください。

次のステップ

まずは small scale な prototype を作成して動作を確認し、staging environment でのテストを経てから production deployment を進めることをお勧めします。

GitHub や official documentation、Stack Overflow などのリソースも積極的に活用してください。open source community forum や issue tracker への参加も、knowledge sharing の観点から大変有益です。疑問点があればコメント欄でお気軽にお知らせください。

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top