👌

日本語PDFで「Keyword search is all you need」を試す:Agent RAGとVector RAGの精度・コスト

に公開

AAAI 2026に採択されたAmazonの研究チームの論文 Keyword search is all you need(arXiv:2602.23368)は、「LLMエージェントにシェルコマンドを持たせたキーワード検索によって、管理にコストがかかるベクトルDBを代替しつつも、ベクトルDB RAGの90%程度の精度を出すことが可能」と主張しています。
論文の評価は英語テキストPDF・Anthropic Claude 3 Sonnet(Amazon Bedrock)で行われています。

本稿は、この手法を日本語PDF・claude-sonnet-4-6(2026年5月時点)で個人的に試した記録です。
確認したかったのは以下の3点です。

  • 日本語でも同じように機能するのか
  • コスト面でどのような差があるのか
  • 精度に違いはあるのか

この記事では、論文で提案されている「キーワード検索ベースのAgentic RAG」を、日本語PDFに対して小規模に検証します。
結論から言うと、表や図表中の数値抽出ではAgent RAGが強い一方、APIコストとレイテンシはVector RAGの方が安定していました。
以降では実験設定や実装、結果とそれに対する解釈を記載しております。

実験設定

比較する 2 つの手法

Agent RAG(論文の手法): LangChain ReAct Agent に pdfgrep をツールとして持たせ、事前インデックスなしで PDF を直接全文検索します。クエリごとにLLMが検索キーワードを決定し、結果を見て次のキーワードを選択する反復検索を行います。最大反復回数はmax_iterations=15(LangChain AgentExecutor のデフォルト値)。

Vector RAG(比較対象): ChromaDB に intfloat/multilingual-e5-small で生成した埋め込みを格納し、クエリをベクトル化して近傍探索します。チャンク設定は chunk_size=600, overlap=120とします。論文中では chunk_size=300 とされていますが、本実験では日本語PDFで文脈が細切れになりすぎないよう、予備確認のうえ chunk_size=600 としました。

LLMは両手法とも claude-sonnet-4-6(入力 $3.0/1M tokens、出力 $15.0/1M tokens)を使用しました。

主要ライブラリのバージョン

# pyproject.toml(抜粋)
[project]
requires-python = ">=3.12"
dependencies = [
    "langchain>=0.3.0,<1.0",           # 実際に使用: 0.3.30
    "langchain-anthropic>=0.3.0,<1.0", # 実際に使用: 0.3.22
    "langchain-community>=0.3.0,<1.0", # 実際に使用: 0.3.31
    "anthropic>=0.102.0",
    "chromadb>=1.5.0",
    "sentence-transformers>=3.0.0",
    "pypdf>=4.0.0",
]

評価データ

日本語政府文書 5 ファイル(IPAのDX関連資料2本、情報通信白書、ものづくり白書2025、ダム・堰施設技術基準)を対象としました。

カテゴリ 問数
事実抽出 4 問
概念説明 1 問
比較問題 2 問
手順・基準 2 問
複数文書横断 1 問

評価は筆者による手動ラベリング(correct / incorrect / partial)で行いました。著者が実装・評価を兼ねているため確認バイアスが入りやすい点はご留意ください。

実装

pdfgrep ラッパーツール

LangChainのカスタムツールとして pdfgrep をラップします。

from langchain.tools import BaseTool
from pydantic import BaseModel, Field
import subprocess, json
from pathlib import Path

PDF_DIR = Path("data/japanese_pdfs")


class PdfgrepSearchInput(BaseModel):
    pattern: str = Field(description="検索パターン(Perl正規表現可、|でOR検索)")
    pdf_dir: str = Field(default=str(PDF_DIR), description="PDFディレクトリパス")


class PdfgrepSearchTool(BaseTool):
    name: str = "pdfgrep_search"
    description: str = (
        "pdfgrepを使ってPDFを全文検索する。"
        "Perl正規表現(-Pフラグ)を使用するため '目的|概要' のようなOR検索が可能。"
    )
    args_schema: type[BaseModel] = PdfgrepSearchInput

    def _run(self, pattern: str, pdf_dir: str = str(PDF_DIR)) -> str:
        # args_schema 定義時に引数が JSON 文字列として渡されることがあるため手動でパース
        try:
            parsed = json.loads(pattern.strip())
            if isinstance(parsed, dict):
                pdf_dir = parsed.get("pdf_dir", pdf_dir)
                pattern = parsed.get("pattern", pattern)
        except (ValueError, TypeError):
            pass
        # --------------------------------

        pdf_files = list(Path(pdf_dir).glob("*.pdf"))
        if not pdf_files:
            return f"No PDF files found in {pdf_dir}"

        all_output = []
        for pdf in pdf_files:
            cmd = [
                "pdfgrep",
                "-P",           # Perl 正規表現(| による OR 検索)
                "-i",           # 大文字小文字を区別しない
                "--context=2",  # マッチ行の前後 2 行を表示
                pattern,
                str(pdf),
            ]
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
            if result.stdout:
                all_output.append(f"[{pdf.name}]\n{result.stdout}")

        return "\n".join(all_output)[:3000] if all_output else "No matches found."

コスト監視

ReActループはLLMを複数回呼び出すため、呼び出しごとのコストを以下のコードで確認しました。

from langchain.callbacks.base import BaseCallbackHandler
from langchain_core.outputs import LLMResult

INPUT_COST_PER_M  = 3.0   # USD / 1M tokens (claude-sonnet-4-6)
OUTPUT_COST_PER_M = 15.0


class TokenUsageCallback(BaseCallbackHandler):
    def __init__(self) -> None:
        self.total_input_tokens = 0
        self.total_output_tokens = 0
        self.llm_call_count = 0

    @property
    def total_cost_usd(self) -> float:
        return (
            self.total_input_tokens  / 1_000_000 * INPUT_COST_PER_M
            + self.total_output_tokens / 1_000_000 * OUTPUT_COST_PER_M
        )

    def on_llm_end(self, response: LLMResult, **kwargs) -> None:
        self.llm_call_count += 1
        in_tok, out_tok = self._extract_tokens(response)
        self.total_input_tokens  += in_tok
        self.total_output_tokens += out_tok
        call_cost = (
            in_tok  / 1_000_000 * INPUT_COST_PER_M
            + out_tok / 1_000_000 * OUTPUT_COST_PER_M
        )
        print(
            f"  [LLM#{self.llm_call_count}] "
            f"in={in_tok:,} out={out_tok:,} tokens | "
            f"call=${call_cost:.5f} | 累計=${self.total_cost_usd:.5f}"
        )

    def _extract_tokens(self, response: LLMResult) -> tuple[int, int]:
        # langchain-anthropic 0.3.x: usage_metadata は AIMessage に格納される
        for gens in response.generations:
            for gen in gens:
                msg  = getattr(gen, "message", None)
                meta = getattr(msg, "usage_metadata", None) if msg else None
                if meta:
                    return meta.get("input_tokens", 0), meta.get("output_tokens", 0)
        return 0, 0

定量結果

総合比較

指標 Agent RAG Vector RAG
正解率(10 問中) 9/10(90%) 7/10(70%)
合計APIコスト(10 問) $1.0401 $0.1286
平均APIコスト / 質問 $0.1040 $0.0129
平均レイテンシ 32.2 秒 6.7 秒
平均ツール実行回数 5.7 回 / 質問

平均APIコストと平均レイテンシの比較(棒グラフ)

問題別詳細

ID カテゴリ Agent 結果 Agent 反復 Agent APIコスト RAG 結果 RAG APIコスト
q001 事実抽出 ✅ 正解 7 回 $0.1000 ✅ 正解 $0.0134
q002 比較問題 ✅ 正解 4 回 $0.0717 ✅ 正解 $0.0171
q003 事実抽出 ✅ 正解 5 回 $0.0565 ❌ 不正解 $0.0120
q004 事実抽出 ✅ 正解 4 回 $0.0326 ❌ 不正解 $0.0107
q005 手順・基準 ✅ 正解 4 回 $0.0513 ✅ 正解 $0.0130
q006 複数文書横断 ❌ 失敗 15 回 $0.4878 ⚠️ 部分的 $0.0135
q007 概念説明 ✅ 正解 5 回 $0.0405 ✅ 正解 $0.0098
q008 事実抽出 ✅ 正解 3 回 $0.0451 ✅ 正解 $0.0083
q009 手順・基準 ✅ 正解 4 回 $0.0710 ✅ 正解 $0.0164
q010 比較問題 ✅ 正解 6 回 $0.0835 ✅ 正解 $0.0144

問題別 APIコスト(横棒グラフ)— q006 の Agent コストが突出

コストの整理:API 呼び出しコストと運用コストは別の問題

2 つの手法のコスト比較は、API呼び出しコストインフラ・運用コストを分けて評価する必要があります。

API 呼び出しコスト:Agent RAG が 8.1 倍高い

今回の検証では、1 問あたりの平均APIコストは Agent RAG $0.1040、Vector RAG $0.0129 で、Vector RAG が 8.1 倍安くなりました。

Agent RAGのコストが高い理由は構造的なものです。ReActループでLLMを複数回呼び出すたびにAPI費用が発生します。1質問あたり平均5.7回のツール呼び出しが行われており、q006(複数文書横断)では15回に達し$0.4878を消費しました。この1問だけでAgent RAGの全APIコストの約 47% を占めています。

コストの分散も大きく異なります。Vector RAGは 10 問全問で $0.0083〜$0.0171(約2倍の範囲)に収まったのに対し、Agent RAGは $0.0326〜$0.4878(約15倍の範囲)でした。クエリの複雑さによってコストが大きく変動するため、本番サービスへの組み込みでは上限設計が必要になります。

なお、論文がコスト優位性を主張する際に比較しているのは「ベクトルDBのインフラコスト」であり、LLM API呼び出しコストではない点に注意が必要です。

インフラ・運用コスト:Vector RAG は事前構築と継続的メンテナンスが必要

Vector RAG は運用上の固定コストを持ちます。今回の実験では5ファイル・351チャンクのインデックス構築に約30秒(CPU、multilingual-e5-small)かかりました。本番環境では以下の運用作業が継続的に発生します。

  • ドキュメントの追加・削除・更新のたびにインデックスの差分更新が必要
  • 埋め込みモデルのバージョン管理(モデル変更時は全件再構築)
  • ベクトルDBのストレージ・メモリスケーリング

Agent RAGにはこれらのインフラコストが発生しません。クエリ時にPDFファイルを直接参照するため、ドキュメントの追加・更新は元ファイルの差し替えだけで完結します。

論文の「コスト削減」主張は、このインフラ・運用コストの削減を指しています。ただし、同一文書への問い合わせ頻度が高いシステムでは、インデックス構築の固定コストは早期に回収できます。

回答品質の分析

Vector RAGが失敗したケース:表・図表中の具体的数値(q003, q004)

q004「ものづくり白書 2025 において、従業員の自己啓発に対する支援を行っている製造業事業所の割合は?」

Agent RAG の回答(正解):

支援を行っている事業所の割合:80.7%。最も多い支援形態:「受講料などの金銭的援助」。
出典: monodzukuri_hakusho_2025_gaiyo.pdf「従業員の自己啓発に対する支援を行っている事業所の割合は80.7%であり」

Vector RAG の回答(不正解):

提供されたコンテキストには具体的な情報は記載されていません。コンテキストに含まれている情報はこれのみです:「製造業における自己啓発を行った労働者の割合は、正社員が4割前後...」これは労働者側が自己啓発を行った割合であり、情報が見つかりませんでした。

Vector RAGは「自己啓発」「割合」という語に意味的に近い別のチャンク(労働者側の自己啓発割合4割)を上位5件として取得し、正解の記述箇所(事業所側の支援割合 80.7%)はランク外となりました。Agent RAGはキーワードを変えながら反復検索することで正解の記述箇所に到達しました。

q003「DX 推進の枠組みで全体平均を特に大きく下回る項目は?」でも同様の傾向がありました。Vector RAG は「マインドセット・企業文化と投資意思決定」(別の指標)を返したのに対し、Agent RAGは「経営トップのコミットメント、ビジョンがマイナス」と正確に引用しました。

Agent RAGが失敗したケース:複数文書横断クエリ(q006)

q006「DX 動向 2025 と DX 推進指標自己診断レポート 2024 年版の両方で指摘されている DX 推進の共通課題は?」では、Agent RAGはmax_iterations=15に到達し回答できませんでした。

失敗の根本原因はツール設計の制約にあります。pdfgrep_search はディレクトリ全体を検索するため、特定ファイルの結果を分離できません。エージェントは「どちらのドキュメントの結果か」を区別できないまま証拠収集を続け、合成フェーズに移れませんでした。実際のイテレーションを見ると、その様子がよくわかります。

Iteration 検索パターン 結果の状況
2〜6 課題|障壁|阻害 特定ファイルの指定を試みるが失敗、全PDF混在でヒット
7〜14 人材不足|人材の確保経営トップ|コミットメント部分最適|全体最適 など 多角的に検索を続けるが、どちらの文書からの結果か区別できない
15 共通|主要な課題|重要な課題 イテレーション上限到達

コンテキストウィンドウに蓄積された検索結果は最終的に入力 22,103 tokens に膨張し、累計コスト $0.47、レイテンシ 56.9 秒に達しました。一方 Vector RAGは $0.0135、8.4 秒で部分的な回答を返しています。

「まだ十分な証拠がない」と判断し続けるエージェントの挙動は、ツール設計と切り離せません。複数文書横断クエリをAgent RAGで扱う場合は、ファイルを指定できるツールに分けるか、事前に文書ごとの要約を用意するなどの設計が必要です。

手法の選択基準

実験結果を踏まえた選択基準を以下に示します。

Agent RAGが適しているケース:

  • 同一文書への問い合わせが少なく、インデックス構築コストを回収できない用途(単発の調査・分析など)
  • 表・図表に含まれる具体的数値の抽出精度が重要な用途
  • 文書の更新頻度が高く、インデックスの継続的メンテナンスが困難なシステム
  • 回答の根拠となったキーワードと参照箇所の追跡が必要な用途

Vector RAGが適しているケース:

  • 同一文書セットへの問い合わせが繰り返し発生するシステム(APIコストの単価優位性を活かせる)
  • APIコスト・レイテンシの予測可能性が重要な本番サービス
  • 複数文書を横断した情報統合が求められるクエリ
  • キーワードに現れない概念的な類似性を問うクエリ

両手法の共通の弱点:

複数文書を横断して情報を統合するクエリは、今回の実装では両手法とも苦手でした。Agent RAGは反復コストが増大し、Vector RAGはチャンク競合により部分的な回答に留まりました。

ただし、失敗の性質は異なると考えています。
Vector RAGは、今回のような単純なTop-kチャンク検索では、チャンク単位の独立検索に起因する限界が見えました。
対して、Agent RAGの失敗は主因はツール設計にあると考えています。特に、pdfgrep_search がディレクトリ全体を検索し、文書単位で結果を整理しにくい設計だったことが影響しました。文書を指定できるツールに分けるか、文書ごとの要約インデックスを用意するといった設計変更により、Agent RAG側は改善の余地があります。

なお、複数文書の横断はRAG研究において2025〜2026年時点で活発に取り組まれている課題であり、GraphRAGやIndexRAGのような専用手法が多数提案されています。標準的な実装の限界として広く認識されている一方で、適切に設計されたAgentic RAGは克服可能な領域でもあります。

まとめ

本実験(日本語 PDF 5 ファイル・10 問・手動評価)の結果を論文の主張と対照させると以下の通りです。

論文の主張 本実験の結果
キーワード検索で RAG と同等以上の精度 部分的に一致(90% vs 70%)。複数文書横断クエリでは失敗
インデックス不要によるコスト削減 インフラ・運用コストは削減できる。ただしAPI呼び出しコストは Agent RAG が 8.1 倍高い
日本語対応 pdfgrep -P フラグ(Perl 正規表現)で動作確認。キーワードの同義表現カバレッジは英語より設計が必要

精度面では特定の質問タイプ(表・図表内の数値抽出)で Agent RAG が優位を示しました。
ただし、本検証は5ファイル・10問・手動評価という小規模な実験であり、結果は一般化された結論ではありません。実運用では、問い合わせ頻度、文書更新頻度、検索対象の構造、許容レイテンシに応じて再評価する必要があります。


参考リンク

使用PDF出典

[1] IPA「DX動向2025」2025年7月、IPA経営企画センター 国際・産業調査部産業調査室
[2] IPA「DX推進指標 自己診断結果 分析レポート(2024年版)」2025年8月
[3] 経済産業省・厚生労働省・文部科学省「2025年版ものづくり白書」概要、2025年5月
[4] 総務省「情報通信白書 令和6年版」第1章、2024年
[5] 国土交通省「ダム・堰施設技術基準(案)」2016年3月

Discussion