RAG本番2年で「なんか惜しい」を乗り越えた話——2026年の現実的な構成

「なんでこの文書を拾ってこないの」「遅い」——RAGあるあるで困った経験ありませんか?2年の本番運用で辿り着いたHyDE・Rerank・Agentic RAGの実装パターンを正直に共有します。

RAGで「そこそこ動く」から「実用に耐える」への距離感

2年ほど前にRAGを本番投入して、正直最初の半年は「なんか惜しい」が続いていた。質問によってはちゃんと答えが返ってくるし、デモ映えもする。でも毎日使うユーザーからのフィードバックは手厳しくて、「なんでこの文書を拾ってこないの」「関係ない話が混じってる」「遅い」の三点セットが延々と来る。

当時は「RAGはプロンプトとChunkingを調整すれば十分」と思ってた。でも実際には、RAG本番運用1年で痛感したこと|チャンキングで半分決まるという現実でも書いたとおり、Chunking設計はたしかに重要なんだけど、それだけじゃ全然足りなかった。2026年時点でうちが辿り着いた構成は、当初想定していたシンプルなRetrieve→Generate二段構成とはかなり違うものになってる。

この記事では、その進化の過程と、今現在動かしている実装パターンを共有したい。完璧な答えじゃないし、正直まだ検証中の部分も多い。でも「2年運用してここまでは言える」という実体験ベースの話として読んでもらえれば。


2026年のRAGアーキテクチャ全体像

うちのチームが今動かしているパイプラインの全体像はこんな感じ。

flowchart TB
    subgraph Ingestion["Ingestion Pipeline"]
        DOC["ソースドキュメント\n(PDF / Confluence / GitHub)"] --> PARSE["パーサー\n(Docling 2.x)"] 
        PARSE --> CHUNK["Semantic Chunking\n+ Parent-Child構造"]
        CHUNK --> EMBED["Embedding\n(text-embedding-3-large\n / Cohere embed-v4)"]
        EMBED --> VDB[("Vector DB\n(pgvector 0.8 / Qdrant 1.9)")]
        CHUNK --> META["メタデータ付与\n(ファイル種別・更新日・セクション)"]
        META --> VDB
    end

    subgraph Query["Query Pipeline"]
        USER["ユーザークエリ"] --> QEXP["Query Expansion\n(HyDE + Multi-query)"]
        QEXP --> RET1["Semantic Search"]
        QEXP --> RET2["BM25 Full-text"]
        RET1 --> HYBRID["Hybrid Fusion\n(RRF)"] 
        RET2 --> HYBRID
        VDB --> RET1
        VDB --> RET2
        HYBRID --> RERANK["Reranker\n(Cohere rerank-v3.5)"]
        RERANK --> CONTEXT["Context Window Assembly"]
        CONTEXT --> LLM["LLM\n(GPT-4o / Claude 3.7 Sonnet)"]
        LLM --> ANS["回答"]
    end

    subgraph Eval["Evaluation & Feedback"]
        ANS --> RAGAS["RAGAS評価\n(faithfulness / relevancy)"]
        RAGAS --> DASHBOARD["監視ダッシュボード"]
    end

2年前はDoc取り込み→Embedding→Retrieve→Generateの4ステップだったのが、今やこの規模になってる。順を追って何が変わったか説明していく。


ハマりポイント① クエリ品質問題——HyDEとMulti-Queryで改善

RAGの検索精度が低い原因の半分以上は、実はドキュメント側じゃなくてクエリ側にある、というのが今の認識だ。ユーザーが投げる質問って短くて、具体的なキーワードが含まれてないことが多い。「〇〇の設定方法は?」みたいなクエリで、ドキュメントには「〇〇をセットアップするには以下の手順を実施してください」と書いてある、なんてことはザラ。意味は同じなのにEmbeddingの距離は意外と開く。地味に厄介な問題なんですよね、これが。

最初に試したのがHyDE(Hypothetical Document Embeddings)。クエリに直接答えるような仮想的な文書をLLMに生成させて、その文書のEmbeddingで検索する手法だ。

import openai
from openai import AsyncOpenAI

client = AsyncOpenAI()

async def generate_hypothetical_document(query: str) -> str:
    """HyDE: クエリに対する仮想的な回答文書を生成"""
    response = await client.chat.completions.create(
        model="gpt-4o-mini",  # 速度重視でminiを使う
        messages=[
            {
                "role": "system",
                "content": (
                    "あなたは技術文書の専門家です。"
                    "与えられた質問に対して、社内ドキュメントに書いてありそうな回答文章を生成してください。"
                    "200文字程度で、具体的な技術用語を含めて書いてください。"
                ),
            },
            {"role": "user", "content": query},
        ],
        temperature=0.3,
    )
    return response.choices[0].message.content


async def expand_query(query: str) -> list[str]:
    """Multi-Query: 複数の観点からクエリを展開"""
    response = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": (
                    "以下の質問を、異なる表現・観点で3つのバリエーションに書き換えてください。"
                    "JSON配列形式で返してください: [\"query1\", \"query2\", \"query3\"]"
                ),
            },
            {"role": "user", "content": query},
        ],
        temperature=0.5,
        response_format={"type": "json_object"},
    )
    import json
    data = json.loads(response.choices[0].message.content)
    return data.get("queries", [query])

HyDEは体感として効く。特にドキュメントが説明体で書かれていて、クエリが疑問文の場合に顕著だった。ただ、LLMを余分に一回呼ぶのでレイテンシが100〜200msくらい増える。コスト意識があるプロダクトでは使うクエリを絞った方がいいかもしれない。

Multi-Queryと組み合わせて検索した結果は、Reciprocal Rank Fusion(RRF)でマージする。

from collections import defaultdict
from typing import List, Tuple

def reciprocal_rank_fusion(
    results_list: List[List[Tuple[str, float]]],
    k: int = 60
) -> List[Tuple[str, float]]:
    """
    複数の検索結果リストをRRFでフュージョン
    results_list: [(doc_id, score), ...] のリストのリスト
    """
    scores: dict[str, float] = defaultdict(float)
    
    for results in results_list:
        for rank, (doc_id, _) in enumerate(results):
            scores[doc_id] += 1.0 / (k + rank + 1)
    
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

このHybrid Fusion(Semantic + BM25 + HyDE派生)で、単純なSemantic Searchと比べてRecall@10が約18%改善した。


ハマりポイント② Rerankで「関係ありそうだけど関係ない」を排除する

Hybrid Fusionで上位20件を取ってきても、LLMに全部渡すのはコンテキスト汚染になる。「関係ありそうなキーワードが含まれてるけど、文脈的には関係ない」ドキュメントが混じることが多いんですよね。「キーワードは合ってるのに全然違う話」みたいなやつで、これがユーザー不満の結構な割合を占めてた。

ここで効いてくるのがCross-Encoder系のReranker。うちはCohere rerank-v3.5を使っている(2026年現在、日本語対応が安定してきた)。

import cohere
from typing import List

cohereClient = cohere.AsyncClient()

async def rerank_documents(
    query: str,
    documents: List[str],
    top_n: int = 5,
) -> List[dict]:
    """Cohere Rerank v3.5で再ランキング"""
    response = await cohereClient.rerank(
        model="rerank-v3.5",
        query=query,
        documents=documents,
        top_n=top_n,
        return_documents=True,
    )
    
    return [
        {
            "text": result.document.text,
            "relevance_score": result.relevance_score,
            "index": result.index,
        }
        for result in response.results
    ]

Rerankの導入前後でスコアがどう変わったか、1ヶ月のRAGAS評価推移を見てほしい。Week5からRerankを有効にしている。

xychart-beta
    title "Rerankingによるスコア改善(RAGAS評価)"
    x-axis ["Week1", "Week2", "Week3", "Week4", "Week5", "Week6", "Week7", "Week8"]
    y-axis "スコア" 0.5 --> 1.0
    line [0.61, 0.62, 0.60, 0.63, 0.78, 0.81, 0.83, 0.85]
    bar  [0.58, 0.59, 0.61, 0.60, 0.75, 0.79, 0.80, 0.83]

faithfulnessが0.63→0.83まで跳ね上がった。正直これは予想以上だった。Rerankって地味な改善に見えるんだけど、コンテキスト汚染を取り除く効果がこれほど大きいとは思ってなかったというのが本音。

費用感も一応補足しておく。Cohere rerankは1000回のrerank呼び出しで$2程度(2026年5月時点)。うちのプロダクトは月に50万クエリくらいあるので月額1000ドル追加になる計算だが、精度改善によるサポートチケット削減を考えると十分ペイしている。ここは好みが分かれると思うけど、個人的にはほぼ必須だと思ってる。


2026年版の新潮流——Agentic RAGをどこまで使うか

2025年後半あたりから「Agentic RAG」という設計パターンが実用レベルになってきた。シンプルに言うと、単一の検索→生成ではなく、LLMエージェントが必要に応じてツール(検索・計算・外部API等)を複数回呼び出しながら回答を組み立てるアプローチだ。

AIエージェント開発で痛い目を見た話|2026年の実装課題と解決策でも触れられているが、エージェント設計は思ったより難しい。うちのチームでも4ヶ月試行錯誤した。何度「シンプルRAGに戻そうか」と思ったかわからない。

from openai import AsyncOpenAI
import json
from typing import Any

client = AsyncOpenAI()

# ツール定義
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_documents",
            "description": "社内ドキュメントを検索して関連情報を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "検索クエリ",
                    },
                    "filter_by_date": {
                        "type": "string",
                        "description": "特定の期間で絞り込む場合のISO8601日付 (例: 2025-01-01)",
                    },
                },
                "required": ["query"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_document_detail",
            "description": "特定のドキュメントIDの全文を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "document_id": {"type": "string"},
                },
                "required": ["document_id"],
            },
        },
    },
]


async def agentic_rag(user_query: str) -> str:
    """Agentic RAGのメインループ"""
    messages = [
        {
            "role": "system",
            "content": (
                "あなたは社内ドキュメントアシスタントです。"
                "必要な情報を検索ツールを使って収集し、正確な回答を提供してください。"
                "一度の検索で不十分な場合は追加検索を行ってください。"
                "情報源が見つからない場合は正直にその旨を伝えてください。"
            ),
        },
        {"role": "user", "content": user_query},
    ]
    
    max_iterations = 5  # 無限ループ防止
    for _ in range(max_iterations):
        response = await client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )
        
        message = response.choices[0].message
        
        # ツール呼び出しがない = 最終回答
        if not message.tool_calls:
            return message.content
        
        messages.append(message)
        
        # ツール実行
        for tool_call in message.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)
            
            # 実際の処理はここで分岐
            if func_name == "search_documents":
                result = await search_documents(**func_args)
            elif func_name == "get_document_detail":
                result = await get_document_detail(**func_args)
            else:
                result = {"error": f"Unknown tool: {func_name}"}
            
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result, ensure_ascii=False),
            })
    
    return "最大試行回数に達しました。もう少し具体的な質問をお試しください。"

Agentic RAGが特に威力を発揮するのは「複合質問」だ。例えば「A機能の仕様と、それに関連するBシステムの制約を照らし合わせて判断してほしい」みたいな質問は、シンプルなRAGでは一回の検索では拾いきれないことが多い。エージェントが自律的に複数検索を行うことで、こういったケースへの対応力が上がった。

ただし、Agentic RAGはレイテンシが跳ね上がる。LLMを複数回呼ぶので、シンプルRAGの500ms程度が2〜5秒になることがある。ユーザーが待てる種類の質問かどうかで使い分けを検討すべきだと思う。うちはチャット型UIにStreaming対応を入れることで体感を改善した。

各手法の性能比較をまとめておく。

アプローチFaithfulnessAnswer RelevancyP95 Latencyコスト/1K queries
シンプルRAG(Semantic only)0.610.67620ms$0.8
Hybrid(Semantic + BM25)0.680.72650ms$0.9
Hybrid + HyDE0.740.78820ms$1.5
Hybrid + HyDE + Rerank0.830.851,100ms$3.5
Agentic RAG(全部乗せ)0.890.913,800ms$12.0

数字は自社のQAデータセット(社内ドキュメント約50万チャンク)でのRAGAS評価なので参考値として見てほしい。Agentic RAGの精度は高いが、コストとレイテンシが4倍近くになる。全クエリにかける必要はなくて、複雑な質問を検出してフォールバックさせる設計が現実的だと思ってる。


見落としがちだけど重要——ドキュメント鮮度と評価の継続運用

検索アルゴリズムを磨くことに意識が向きがちなんだけど、実務で痛感しているのはドキュメント鮮度管理と評価の継続性の重要さだ。

ドキュメントが古くなるとRAGの回答品質が静かに劣化する。しかも劣化に気づきにくい。「なんか最近おかしい気がする」がユーザーから来てから調べると、3ヶ月前の仕様変更が反映されていないConfluenceページを拾ってきてた、みたいなことが何度もあった。アルゴリズムじゃなくてデータの問題だったというやつで、これが一番悔しいパターン。

対策として実装したのが、ドキュメントの更新検知と優先度付き再インデクシングのパイプライン。

import asyncio
from datetime import datetime, timedelta
from dataclasses import dataclass
from enum import Enum

class Priority(Enum):
    HIGH = 1    # 1時間以内に再インデクシング
    MEDIUM = 2  # 24時間以内
    LOW = 3     # 週次バッチ

@dataclass
class ReindexTask:
    doc_id: str
    source_url: str
    last_updated: datetime
    priority: Priority

def classify_reindex_priority(
    doc: dict,
    query_frequency_7d: int,
) -> Priority:
    """ドキュメントの再インデクシング優先度を分類"""
    age_hours = (datetime.now() - doc["last_updated"]).total_seconds() / 3600
    
    # よく参照される古いドキュメントは最優先
    if query_frequency_7d > 100 and age_hours > 24:
        return Priority.HIGH
    
    # 更新されたばかりのドキュメント
    if age_hours < 2:
        return Priority.HIGH
    
    # そこそこ参照されるドキュメント
    if query_frequency_7d > 20:
        return Priority.MEDIUM
    
    return Priority.LOW

もう一つ大事なのが、評価の継続運用だ。RAGAS等の自動評価ツールは入れっぱなしじゃなくて、週1でチームでスコアを見る習慣を作らないと意味がない。プロンプト設計はセンスじゃない——チーム運用2年で見えた構造化の話でも書かれているような「チームでの運用文化」の話で、技術選択と同じくらい重要だと感じてる。

うちのチームでは毎週月曜に「RAGウィークリーレビュー」と称して15分だけ集まって、以下の指標を確認している。

  • RAGAS Faithfulness/Relevancy の週次推移
  • ユーザーから「答えられなかった」フィードバックが来た質問のトップ10
  • Rerankerのrelevance scoreが低いのに最終回答に使われたケースの件数

地味だけどこれが一番効く。アルゴリズムより運用だなと思う瞬間、正直かなり多い。


まとめ

2年間RAGを本番で育ててきて、今言えることをまとめる。

1. クエリ拡張(HyDE + Multi-Query)は費用対効果が高い シンプルなSemantic Searchに比べてRecall@10が15〜20%改善する。最初に試すべき施策だと思う。

2. Hybrid Fusion(Semantic + BM25)はベースラインとして必須 どちらかだけに頼ると特定のクエリパターンで大きく外す。2026年現在、pgvector 0.8やQdrant 1.9はHybrid Searchを標準サポートしているので実装コストも下がっている。なお、ベクトルDBの選定についてはベクトルDB比較2026|マルチモーダルEmbedding対応の最適選定ガイドも参考にしてほしい。

3. RerankはPrecision改善の切り札だが、コストとレイテンシとの相談 Faithfulnessが0.2近く改善したのは事実だが、月額コストが3〜4倍になる。ユーザー規模と精度要求のバランスで判断を。個人的には投資対効果でいちばん見えやすい施策だと思ってる。

4. Agentic RAGは万能ではない 複合質問への対応力は上がるが、P95レイテンシが3〜5秒になる。ストリーミング対応とクエリ複雑度に応じたルーティングをセットで設計しないと、ユーザー体験が崩れる。

5. ドキュメント鮮度管理と評価の継続運用を怠るとじわじわ崩れる アルゴリズムを磨くことと同じくらい、運用文化に投資してほしい。これが2年で一番痛感したことかもしれない。


次のステップとして、まだシンプルなSemantic Searchだけで運用しているなら、まずHybrid Fusion(BM25統合)から試してみるといいと思う。pgvectorを使っているならextension追加とクエリ変更だけで対応できる。そこから徐々にRerankを載せていくのが、無難で失敗しにくいステップアップだ。

皆さんはRAGの品質改善でどんなアプローチを試してますか?特に「日本語ドキュメントのChunking」周りはまだ正解が見えていないので、知見があればぜひ聞いてみたい。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事