「RAGを作りたいけど、どのアプローチで実装すればいいか分からない」という相談を、最近よく受けます。
シンプルなベクトル検索でいいのか、クエリ変換やリランクを組み込んだAdvanced RAGが必要なのか、それともLLMが自律的に検索を繰り返すAgentic RAGまで踏み込むべきなのか。選択肢が増えるにつれて、かえって判断が難しくなっています。
私が複数のプロジェクトで試してきた経験から言うと、「最初に正解のパターンを選ぼうとしないこと」が一番大切です。ただ、その前提として各パターンの実際の動きとコストを把握しておく必要があります。ここでは3つのパターンを実際のコードで比較しながら、どのケースで何を選ぶかの判断軸をまとめます。
なぜパターン選択で迷うのか
RAGの実装解説を読み進めると、途中で「でもこれだとクエリの表現がずれたときに検索精度が落ちるよね」という疑問が出てきます。それを解消しようとAdvanced RAGの手法を追いかけると、今度は実装の複雑さと見合うかどうかが気になります。
この迷いの根本は、「何を改善したいか」が具体化される前にアーキテクチャを選ぼうとしていることにあります。
実際のプロジェクトでは以下の流れで考えると判断が早くなります。
- まずシンプルRAGを動かして、何が問題かを計測する
- 検索精度の問題なら → Advanced RAGの手法を部分的に導入する
- 複数ステップの推論が必要なら → Agentic RAGを検討する
前置きが長くなりましたが、各パターンを順に見ていきます。
パターン1:シンプルRAG — 30分で動くが精度の壁がある
最もベーシックな構成です。ドキュメントをチャンクに分割してベクトル化しておき、クエリを同じモデルで埋め込んでコサイン類似度で上位k件を取得、それをコンテキストにしてGeminiに回答させます。
import google.generativeai as genai
import numpy as np
genai.configure(api_key="YOUR_GEMINI_API_KEY")
# ドキュメントを事前にチャンク化・埋め込み済みと仮定
# chunks: [{"text": "...", "embedding": [...]}]
def embed_text(text: str, task_type: str = "RETRIEVAL_DOCUMENT") -> list[float]:
result = genai.embed_content(
model="models/text-embedding-004",
content=text,
task_type=task_type,
)
return result["embedding"]
def cosine_similarity(a: list[float], b: list[float]) -> float:
a, b = np.array(a), np.array(b)
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
def simple_rag(query: str, chunks: list[dict], top_k: int = 3) -> str:
# クエリをREトリーバルQUERYタスクで埋め込む(DOCUMENTと区別する)
query_embedding = embed_text(query, task_type="RETRIEVAL_QUERY")
# 類似度でソートして上位k件を取得
scored = sorted(
chunks,
key=lambda c: cosine_similarity(query_embedding, c["embedding"]),
reverse=True,
)[:top_k]
context = "\n\n---\n\n".join(c["text"] for c in scored)
# Geminiで回答生成
model = genai.GenerativeModel("gemini-2.5-flash")
prompt = f"""以下の情報のみをもとに質問に答えてください。情報に含まれていない内容は「分かりません」と答えてください。
【参照情報】
{context}
【質問】
{query}"""
response = model.generate_content(prompt)
return response.textこのコードで動きます。task_type="RETRIEVAL_QUERY" と RETRIEVAL_DOCUMENT を使い分けている点が地味に重要で、同じ text-embedding-004 でも非対称タスクとして最適化されるため、検索精度が上がります。
シンプルRAGの限界
実際にプロダクションで使おうとしたときに最初にぶつかる壁は、クエリの表現揺れです。「APIのエラー対処法は?」と「レート制限を回避するには?」は内容的には近いはずですが、表現が違うと類似度が下がります。
もう一つは、長いドキュメントをどうチャンク分割するかです。単純にcharacterで切ると文脈が途切れ、精度が落ちます。
これらが許容できない場合に、次のパターンを検討することになります。
パターン2:Advanced RAG — 精度はぐっと上がるが実装コストが増える
シンプルRAGに手を加えてクエリの品質を上げたり、検索結果の精度を向上させたりするアプローチです。代表的な手法はクエリ変換(HyDE)とリランクです。
ここではHyDE(Hypothetical Document Embeddings)を試してみます。これは「クエリに直接答える仮想ドキュメント」をLLMに生成させてから、その仮想ドキュメントで検索する手法です。ユーザーの「短い質問」よりも「答えの形をした長いテキスト」のほうが、ドキュメントの埋め込みに近くなるという発想です。
def generate_hypothetical_document(query: str) -> str:
"""クエリに答える仮想ドキュメントを生成(HyDE)"""
model = genai.GenerativeModel("gemini-2.5-flash")
prompt = f"""次の質問に答える文書の一部を、実際の文書であるかのように200字程度で書いてください。
事実かどうかは問いません。検索精度向上のための仮想文書です。
質問: {query}
文書の一部:"""
response = model.generate_content(prompt)
return response.text
def advanced_rag_with_hyde(query: str, chunks: list[dict], top_k: int = 5) -> str:
# HyDEで仮想ドキュメントを生成してから埋め込む
hypothetical_doc = generate_hypothetical_document(query)
search_embedding = embed_text(hypothetical_doc, task_type="RETRIEVAL_QUERY")
# 上位k件を広めに取得
scored = sorted(
chunks,
key=lambda c: cosine_similarity(search_embedding, c["embedding"]),
reverse=True,
)[:top_k]
# 上位5件のうち本当に関連性が高いものだけに絞る(簡易リランク)
# 実際はCross-Encoderなどを使うが、ここではGeminiで再スコアリング
reranked = rerank_with_gemini(query, [c["text"] for c in scored], top_n=3)
context = "\n\n---\n\n".join(reranked)
model = genai.GenerativeModel("gemini-2.5-pro")
prompt = f"""以下の情報のみをもとに、正確に質問に答えてください。
【参照情報】
{context}
【質問】
{query}"""
response = model.generate_content(prompt)
return response.text
def rerank_with_gemini(query: str, passages: list[str], top_n: int = 3) -> list[str]:
"""Geminiでパッセージを再スコアリング(簡易リランク)"""
model = genai.GenerativeModel("gemini-2.5-flash")
numbered = "\n\n".join(f"[{i+1}] {p}" for i, p in enumerate(passages))
prompt = f"""質問に最も関連性の高いパッセージを{top_n}件選び、番号をカンマ区切りで返してください。他の文字は含めないでください。
質問: {query}
パッセージ:
{numbered}
最も関連性が高い{top_n}件の番号:"""
response = model.generate_content(prompt)
indices_str = response.text.strip()
try:
indices = [int(x.strip()) - 1 for x in indices_str.split(",")]
return [passages[i] for i in indices if 0 <= i < len(passages)]
except Exception:
return passages[:top_n] # パースに失敗した場合は上位からそのまま返す実装を見ると分かるように、Advanced RAGはシンプルRAGに比べてGeminiのAPI呼び出しが増えます。クエリ1件に対してHyDE生成・リランクと最低2回の追加呼び出しが発生するため、レイテンシとコストが上昇します。
私の経験では、Advanced RAGは専門用語が多いドメインやユーザーの入力が短くて不完全なケース(チャットボットなど)で効果が出やすいと感じています。ECサイトの商品検索のようにクエリとドキュメントが語彙レベルで近い場合は、むしろシンプルRAGで十分なことが多いです。
パターン3:Agentic RAG — 最も強力だが設計が鍵
LLMが自律的に複数回の検索と推論を繰り返すパターンです。「一度の検索では答えが出ない複雑な質問」「複数の情報を統合して比較する必要がある質問」に強みを発揮します。
Gemini APIでAgentic RAGを実装するときに活用できるのがFunction Callingです。
import json
# ツール定義:Geminiに「検索できる」ことを伝える
search_tool = {
"function_declarations": [
{
"name": "search_documents",
"description": "ドキュメントベースを検索して関連情報を返す。複雑な質問では複数回呼び出してよい。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "検索クエリ。具体的なほど精度が上がる。",
},
"top_k": {
"type": "integer",
"description": "取得件数(1〜10の整数)",
},
},
"required": ["query"],
},
}
]
}
def execute_search(query: str, chunks: list[dict], top_k: int = 3) -> str:
"""実際のベクトル検索を実行"""
query_embedding = embed_text(query, task_type="RETRIEVAL_QUERY")
scored = sorted(
chunks,
key=lambda c: cosine_similarity(query_embedding, c["embedding"]),
reverse=True,
)[:top_k]
return "\n\n".join(c["text"] for c in scored)
def agentic_rag(question: str, chunks: list[dict], max_iterations: int = 5) -> str:
model = genai.GenerativeModel(
"gemini-2.5-pro",
tools=[search_tool], # type: ignore
)
messages = [{"role": "user", "parts": [question]}]
for _ in range(max_iterations):
response = model.generate_content(messages)
candidate = response.candidates[0]
# Function Callがあれば実行する
function_calls = [
p.function_call
for p in candidate.content.parts
if hasattr(p, "function_call") and p.function_call.name
]
if not function_calls:
# ツール呼び出しなし → 最終回答
return candidate.content.parts[0].text
# モデルの発話を履歴に追加
messages.append({"role": "model", "parts": candidate.content.parts})
# 各Function Callを実行して結果を返す
tool_results = []
for fc in function_calls:
args = dict(fc.args)
result = execute_search(
query=args["query"],
chunks=chunks,
top_k=int(args.get("top_k", 3)),
)
tool_results.append(
{
"function_response": {
"name": fc.name,
"response": {"result": result},
}
}
)
messages.append({"role": "user", "parts": tool_results})
return "回答の生成に失敗しました(反復制限に達しました)"Agentic RAGの動作を見ると、Geminiが自分で「何を検索すべきか」を判断して、必要なら複数回検索を繰り返します。「AとBを比較して、共通点と相違点を教えて」のような質問では、AとBをそれぞれ検索してから統合するという動きをとります。
ただし、注意点があります。反復回数に上限を設けないと無限ループになるリスクがあること、そしてコストとレイテンシが読みにくいこと。本番で使うなら、呼び出し回数のログを必ず残して、コスト監視の仕組みを最初から組み込んでおくことをお勧めします。
Agentic RAGの設計で詳しく知りたい方は、Gemini API × GraphRAGとKnowledge Graphの実装ガイドも参考になります。
3パターンの選択基準
ここをまとめとして、判断の軸を整理します。
シンプルRAGが適しているケース
- プロトタイプ・MVP段階
- ドキュメントの語彙とクエリの語彙が近い(商品カタログ、FAQ、社内マニュアル)
- レイテンシを最優先したい(リアルタイムチャット)
- まず動かして精度を計測したい
Advanced RAGが適しているケース
- ユーザーの入力が短くて不明瞭(チャットボット)
- 専門用語が多いドメイン(医療、法律、技術文書)
- シンプルRAGで計測した結果、精度が目標を下回っている
- HyDEやリランクのコスト増分が許容できる
Agentic RAGが適しているケース
- 複数ステップの推論が必要(「AをBと比較して〜」「〜の原因を洗い出して対策を提案して」)
- ユーザーが曖昧な目標を持っていて、複数の情報を統合する必要がある
- コストとレイテンシより回答品質を最優先できる
なお、RAGの基本実装から本番デプロイまで体系的に学びたい方はGemini APIで作るRAGシステム完全実装ガイドが参考になります。また、大量のドキュメントを効率よく処理するパイプラインについてはGemini 3 Flashを使った高速RAGパイプライン構築も合わせて読んでみてください。
まず「シンプルRAGを計測する」から始める
最後に、私が毎回やっているアプローチを共有します。
どんなプロジェクトでも、最初はシンプルRAGを動かして精度を計測します。具体的には、テストクエリ20〜30件に対して「期待する回答と実際の回答の一致率」をざっくり記録します。これが80%以上なら、コストをかけてAdvanced RAGに移行する必要がないことが多いです。70%を切っていたら、クエリ変換かリランクのどちらで改善できるかを試します。
Agentic RAGは、シンプルやAdvancedで解決できなかった「複雑な推論が必要な質問」が明確に存在するときに初めて検討します。最初からAgentic RAGで設計すると、デバッグが難しくなるためです。
まずシンプルなものを動かして計測します。その積み重ねが、最終的に一番早く目標の精度に到達できる道です。