Home 論文解説: AdaptiveRAG — クエリ分類×適応的検索戦略でRAGの精度とコストを両立する
投稿
キャンセル

📄 論文解説: AdaptiveRAG — クエリ分類×適応的検索戦略でRAGの精度とコストを両立する

論文概要(Abstract)

AdaptiveRAGは、RAGシステムにおけるクエリルーティングの課題に取り組んだ研究です。従来のRAGは全クエリに対して一律の検索パイプラインを適用するため、単純なクエリでは不必要な検索コストが発生し、複雑なクエリでは検索が不十分になるという問題がありました。本論文では、クエリを3タイプ(Simple / Conversational / Complex)に分類し、タイプに応じて検索戦略を動的に切り替えるフレームワークを提案しています。

この記事は Zenn記事: LangGraph動的検索ルーティング実装:クエリ分類×マルチリトリーバーでQA精度を向上させる の深掘りです。

情報源

背景と動機(Background & Motivation)

RAG(Retrieval-Augmented Generation)は大規模言語モデルの幻覚問題や知識の陳腐化に対する有力なアプローチですが、従来のRAGシステムには検索戦略が固定化されているという構造的な問題があります。

具体的には以下の課題が存在します:

  1. 過剰検索(Over-retrieval): 「Pythonとは何か」のような汎用知識クエリに対しても検索を実行し、不要なレイテンシとコストが発生する
  2. 検索不足(Under-retrieval): 「2023年Q3と2022年Q3の売上を比較して」のような複合的なクエリに対して単一ステップの検索では情報が不足する
  3. 文脈断絶(Context Loss): マルチターン会話で前のやり取りの文脈が検索に反映されない

Zenn記事では事実型・概念型・複合型・汎用知識の4分類を用いましたが、本論文ではより体系的に、会話コンテキストを考慮した3分類体系を提案しています。

主要な貢献(Key Contributions)

  • 貢献1: 会話コンテキストを考慮したクエリ分類による適応的検索戦略の提案。クエリをSimple・Conversational・Complexの3カテゴリに分類し、それぞれに最適な検索深度を割り当てる
  • 貢献2: 動的ファクト関連付けグラフ(Dynamic Fact Association Graph)の導入。会話全体を通じてエンティティ間の関係を追跡し、文脈連続性を維持する
  • 貢献3: 検索コスト57%削減と精度向上の両立。不必要な検索をスキップしつつ、複雑クエリでは多段階検索を実行する

技術的詳細(Technical Details)

クエリ分類モジュール

AdaptiveRAGの中核は、受け取ったクエリを3つのカテゴリに分類する軽量な分類器です。

カテゴリ説明検索戦略
Simple検索不要で直接回答可能検索スキップ「GPTの正式名称は?」
Conversational前の会話文脈に依存するコンテキスト考慮検索「さっきの手法のコードは?」
Complex多段階の情報収集が必要反復的多段検索「A社とB社のQ3売上を比較して」

分類は以下の式で行われます:

\[c = \text{Classify}(q, H)\]

ここで、

  • $q$: 現在のクエリ
  • $H$: 会話履歴(過去のやり取り全体)
  • $c \in {\text{Simple}, \text{Conversational}, \text{Complex}}$

分類器には軽量LLM(例: GPT-4o-mini相当)を使用し、分類自体のレイテンシを最小化しています。

適応的検索戦略

分類結果に基づいて、以下の3つの検索パスが選択されます:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def adaptive_retrieve(query: str, history: list[str], classifier, retrievers) -> list[str]:
    """AdaptiveRAGの適応的検索ロジック

    Args:
        query: 現在のクエリ
        history: 会話履歴
        classifier: クエリ分類器
        retrievers: 検索器群

    Returns:
        検索結果のドキュメントリスト
    """
    category = classifier.classify(query, history)

    if category == "Simple":
        # 検索スキップ: LLMの内部知識で直接回答
        return []

    elif category == "Conversational":
        # 会話履歴を考慮したコンテキスト検索
        contextualized_query = rewrite_with_context(query, history)
        return retrievers.search(contextualized_query, top_k=5)

    elif category == "Complex":
        # 多段階反復検索
        return chain_retrieve(query, history, retrievers, max_steps=3)

動的ファクトグラフ

会話の進行に伴い、エンティティ間の関係を動的に追跡するグラフ構造を構築します:

\[G_t = \text{Update}(G_{t-1}, \text{entities}(q_t), \text{entities}(R_t))\]

ここで、

  • $G_t$: 時刻 $t$ でのファクトグラフ
  • $\text{entities}(q_t)$: クエリから抽出されたエンティティ
  • $\text{entities}(R_t)$: 検索結果から抽出されたエンティティ

各ファクトの関連度スコアは以下で計算されます:

\[\text{Relevance}(f) = \alpha \cdot \text{SimScore}(f, q_t) + (1 - \alpha) \cdot \text{GraphProximity}(f, G_t)\]
  • $\text{SimScore}$: 意味的類似度(コサイン類似度)
  • $\text{GraphProximity}$: グラフ上での近接度(ホップ数の逆数)
  • $\alpha$: バランス係数(デフォルト0.6)

多段階反復検索(Chain Retrieve)

Complexクエリに対しては、以下の反復検索アルゴリズムを実行します:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def chain_retrieve(
    query: str,
    history: list[str],
    retrievers,
    max_steps: int = 3,
) -> list[str]:
    """多段階反復検索

    各ステップで検索結果を評価し、不十分なら
    クエリを書き換えて再検索する。

    Args:
        query: 初期クエリ
        history: 会話履歴
        retrievers: 検索器群
        max_steps: 最大反復回数

    Returns:
        統合された検索結果
    """
    all_docs: list[str] = []
    current_query = query

    for step in range(max_steps):
        docs = retrievers.search(current_query, top_k=5)
        all_docs.extend(docs)

        # 検索結果の十分性を評価
        if is_sufficient(query, all_docs):
            break

        # クエリを書き換えて再検索
        current_query = rewrite_query(query, all_docs, history)

    return deduplicate(all_docs)

Zenn記事のCommand APIによるルーティングと比較すると、AdaptiveRAGはより細かい粒度で検索深度を制御している点が特徴的です。

実装のポイント(Implementation)

分類器の学習と推論

分類器はStructured Outputを用いたLLM呼び出しで実装できます。学習データは既存のQAログから自動生成が可能です:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pydantic import BaseModel, Field
from typing import Literal

class QueryClassification(BaseModel):
    """クエリ分類結果"""
    category: Literal["simple", "conversational", "complex"] = Field(
        description="クエリの複雑度カテゴリ"
    )
    confidence: float = Field(
        ge=0.0, le=1.0,
        description="分類の確信度"
    )
    reasoning: str = Field(
        description="分類理由の簡潔な説明"
    )

実装上の注意点

  1. 分類器のレイテンシ: 分類自体が検索より遅いと本末転倒。GPT-4o-miniクラスの軽量モデルを使用し、レイテンシを80ms以下に抑える
  2. 閾値設定: 分類の確信度が低い場合(例: 0.6未満)はデフォルトでConversational検索にフォールバック
  3. ファクトグラフの肥大化: 長い会話ではグラフが膨張するため、TTL(Time-to-Live)付きのエントリ管理が必要
  4. 検索結果のキャッシュ: 同一セッション内で類似クエリが繰り返される場合、検索結果をキャッシュして再利用

Production Deployment Guide

AWS実装パターン(コスト最適化重視)

トラフィック量別の推奨構成:

規模月間リクエスト推奨構成月額コスト主要サービス
Small~3,000 (100/日)Serverless$50-150Lambda + Bedrock + DynamoDB
Medium~30,000 (1,000/日)Hybrid$300-800Lambda + ECS Fargate + ElastiCache
Large300,000+ (10,000/日)Container$2,000-5,000EKS + Karpenter + EC2 Spot

Small構成の詳細 (月額$50-150):

  • Lambda: 1GB RAM, 30秒タイムアウト ($20/月) — クエリ分類+検索実行
  • Bedrock: Claude 3.5 Haiku, Prompt Caching有効 ($80/月) — 分類器+生成
  • DynamoDB: On-Demand ($10/月) — ファクトグラフ保存
  • CloudWatch: 基本監視 ($5/月)

コスト削減テクニック:

  • Bedrock Prompt Caching: 分類器のシステムプロンプトを固定化し30-90%削減
  • Simple分類クエリの検索スキップ: 全体の30-40%の検索コストを節約
  • DynamoDB TTL: 古いファクトグラフエントリを自動削除

コスト試算の注意事項:

  • 上記は2026年2月時点のAWS ap-northeast-1(東京)リージョン料金に基づく概算値です
  • 実際のコストはトラフィックパターン、リージョン、バースト使用量により変動します
  • 最新料金は AWS料金計算ツール で確認してください

Terraformインフラコード

Small構成 (Serverless): Lambda + Bedrock + DynamoDB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# --- IAMロール(最小権限) ---
resource "aws_iam_role" "adaptive_rag_lambda" {
  name = "adaptive-rag-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action    = "sts:AssumeRole"
      Effect    = "Allow"
      Principal = { Service = "lambda.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy" "bedrock_invoke" {
  role = aws_iam_role.adaptive_rag_lambda.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
      Resource = "arn:aws:bedrock:ap-northeast-1::foundation-model/anthropic.claude-3-5-haiku*"
    }]
  })
}

# --- Lambda関数 ---
resource "aws_lambda_function" "adaptive_rag" {
  filename      = "lambda.zip"
  function_name = "adaptive-rag-handler"
  role          = aws_iam_role.adaptive_rag_lambda.arn
  handler       = "index.handler"
  runtime       = "python3.12"
  timeout       = 60
  memory_size   = 1024

  environment {
    variables = {
      BEDROCK_MODEL_ID    = "anthropic.claude-3-5-haiku-20241022-v1:0"
      DYNAMODB_TABLE      = aws_dynamodb_table.fact_graph.name
      CLASSIFICATION_MODEL = "anthropic.claude-3-5-haiku-20241022-v1:0"
    }
  }
}

# --- DynamoDB(ファクトグラフ保存) ---
resource "aws_dynamodb_table" "fact_graph" {
  name         = "adaptive-rag-fact-graph"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "session_id"
  range_key    = "entity_id"

  attribute {
    name = "session_id"
    type = "S"
  }
  attribute {
    name = "entity_id"
    type = "S"
  }

  ttl {
    attribute_name = "expire_at"
    enabled        = true
  }
}

運用・監視設定

CloudWatch Logs Insights クエリ:

1
2
3
4
5
6
7
8
9
10
-- クエリ分類の分布を確認(コスト最適化の効果測定)
fields @timestamp, query_category, latency_ms
| stats count(*) as total,
        count_distinct(query_category) as categories
        by query_category
| sort total desc

-- Simple分類による検索スキップ率
fields @timestamp, query_category, retrieval_skipped
| stats avg(retrieval_skipped) as skip_rate by bin(1h)

コスト最適化チェックリスト

  • クエリ分類によるSimpleクエリの検索スキップ有効化
  • Bedrock Prompt Caching: 分類器のシステムプロンプト固定
  • DynamoDB TTL: ファクトグラフの自動期限切れ設定
  • Lambda メモリサイズ最適化(CloudWatch Insights分析)
  • AWS Budgets: 月額予算設定(80%で警告)

実験結果(Results)

マルチターン会話QAベンチマークでの評価結果です:

手法EM (ConvQA)F1 (ConvQA)検索呼び出し回数
Naive RAG52.361.71.0x
Self-RAG58.167.41.8x
IRCoT61.270.33.2x
AdaptiveRAG65.474.81.4x

分析ポイント:

  • Naive RAGと比較してEM +13.1pt向上、かつ検索呼び出し回数は1.4倍に抑制
  • IRCoTはEM 61.2%を達成するものの検索呼び出しが3.2倍と高コスト。AdaptiveRAGは同等以上の精度を半分以下のコストで実現
  • Simpleクエリの検索スキップにより、全体のレイテンシが38%削減
  • Complexクエリでは多段階検索により、単一ステップ検索と比較してRecall@5が22%向上

実運用への応用(Practical Applications)

Zenn記事で紹介した事実型/概念型/複合型/汎用知識の4分類パターンと、本論文のSimple/Conversational/Complexの3分類は相補的です。実務では以下の統合が有効です:

  1. まずAdaptiveRAGの3分類で検索深度を決定: Simpleなら検索スキップ、Complexなら多段階検索
  2. 検索実行時にZenn記事の4分類でリトリーバーを選択: 事実型→BM25、概念型→ベクトル検索、複合型→ハイブリッド
  3. 結果として2段階ルーティング: 検索深度×検索手法の組み合わせ最適化

この2段階アプローチにより、Zenn記事で報告されたMRR 18%向上に加え、検索コストの大幅削減が期待できます。

スケーリング上の考慮事項:

  • ファクトグラフはセッション単位で管理し、セッション終了時に破棄(TTL設定)
  • 分類器のモデルは最小サイズ(Haiku相当)で十分。Sonnet以上はオーバースペック
  • BM25インデックスは事前構築し、ベクトルインデックスはバッチ更新

関連研究(Related Work)

  • Self-RAG (Asai et al., 2023): 検索の必要性を自己判断するRAG。AdaptiveRAGはこれを会話コンテキスト対応に拡張
  • FLARE (Jiang et al., 2023): 生成中の低確信度トークンで動的に検索をトリガー。トークンレベルの細粒度制御が特徴
  • IRCoT (Trivedi et al., 2023): Chain-of-Thoughtの各ステップで検索を実行。高精度だが検索コストが3倍以上

まとめと今後の展望

AdaptiveRAGは、クエリ分類による適応的検索戦略でRAGの精度とコストのトレードオフを大幅に改善しました。EM +13.1pt向上と検索コスト57%削減の両立は、Zenn記事で紹介した動的検索ルーティングの理論的裏付けとなります。

今後の研究方向として、分類器の軽量化(distillation)、リアルタイムのファクトグラフ更新、マルチモーダルクエリへの拡張が挙げられます。

参考文献

この投稿は CC BY 4.0 でライセンスされています。

AWS公式解説: Amazon Bedrock Intelligent Prompt Routing — マネージドLLMルーティングの実装と活用

論文解説: Query Routing for Homogeneous Tools — 同種ツール間の軽量クエリルーティング手法