上級 12 分上級

ローカルでの Agentic RAG:その価値があるのは peine

Agentic RAG では、検索の制御をモデルに任せます。モデルは何を検索するかを判断し、見つけた内容を読み、足りない情報があれば再度クエリを実行します。このループにはコストがかかり、特にローカルマシンでは、ターンごとに生成時間という形でその負担が生じます。このガイドでは、従来型の RAG との違いと、この追加コストに見合うケースを説明し、Ollama で提供するモデルと LangGraph を使った完全な例を紹介します。

著者 Samir K.·更新 2026-11-02·Windows・macOS・Linuxでテスト済み

#Agentic RAG の概要

従来の RAG は固定された経路をたどります。質問が検索を起動し、見つかった箇所がプロンプトに入り、モデルが回答します。Agentic RAG は、この経路をループに置き換えます。モデルは検索ツールを利用でき、回答できる、または断念すべきだと判断するまで、ツールを呼び出すかどうか、どのクエリで何回呼び出すかを自ら選択します。

Microsoft のコース「AI Agents for Beginners」では、これを、モデルが外部ソースから情報を取得しながら次のステップを計画する動作として説明しており、生成、検証、修正を行う「maker-checker」型のループを伴います。同じコースでは、この自律性の限界にも触れています。自律性が発揮されるのは、与えられたタスクとツールの範囲内に限られます。

実践的な結論は一文にまとめられます。一度の検索では回答全体に必要な情報を集められないことがRAGの失敗原因であれば、ループが役立つ可能性があります。インジェストやチャンク分割の不備が原因であれば、ループは同じ問題をより遅く繰り返すだけです。

i
このガイドが断言していないこと
お使いのデータでの測定がないため、ここでは精度の向上を一切謳っていません。このガイドのためのベンチマークは一切実施していません。おおよその規模を示す数値は説明用の計算であり、その旨を明記しています。

#Agentic RAG とワンパス RAG の違い

自分だけのローカルRAGを作るためのキット一式。

私たちの経験とガイド付きルートを活かして、ローカルRAGを構築し、自分のドキュメントで活用しましょう。

  • 永久に利用できるオンラインスペース
  • PDF + ファイル
  • 30日間返金対応

基本的な構成要素は、埋め込み、ベクトルデータベース、生成モデルで、これらは変わりません。なじみがない場合は、ローカル RAG の入門ガイドから始めてください。変わるのは、処理の流れを決める主体です。従来型の RAG ではコードが決めますが、エージェント型の RAG では、コードが課す制限の範囲内でモデルが決めます。

クエリ
従来型の RAG:ユーザーの質問をそのまま使い、場合によっては一度だけ書き換えます。エージェント型:モデルが独自にクエリを作成し、一つの質問を複数に分解することもあります。
検索回数
従来型の RAG:一回で、事前にわかっています。エージェント型:質問ごとに変動し、設定した上限によって制限されます。
出典
従来の RAG:通常はインデックスが一つだけです。エージェント型:複数のツール(ベクトルインデックス、キーワード検索、SQL データベース)を利用でき、その中からモデルが選択します。
結果の制御
従来型 RAG:なし。モデルは受け取った内容で文章を作成します。エージェント型:モデルは箇所が不十分と判断して再検索できますが、これは保証ではありません。モデルがこの判断を誤ることもあります。
モデルへの要件
従来型 RAG:抜粋をもとに文章を作成できるモデルであれば、どれでも使えます。エージェント型:ツール呼び出しを正しく処理できるモデルが必要です。
予測可能性
従来型 RAG:応答時間は比較的安定しています。エージェント型:応答時間は変動し、経路をトレースで確認する必要があります。

ループは三つの仕組みで構成されます。計画:質問をサブ質問に分解します。ツールの利用:各サブ質問を呼び出しに変換します。再試行:結果を読み、クエリを言い換えるか結論を出すかを判断します。エージェントの全体的なアーキテクチャ(メモリ、ガードレール、監視)については、専用のガイドを参照してください。ここで扱うのは検索ループのみです。

見落とされがちな中間的な選択肢として、固定ステップのパイプラインもあります。クエリの書き換え、ハイブリッド検索、リランカーが、モデルの判断を介さず常に同じ順序で実行されます。これはエージェント型ではなく、多くの場合、最初に試すべきものです。

#agentic RAGを導入する価値があるケース

適切な判断基準は、求める高度さの水準ではなく、質問の形式にあります。現在の RAG で実際に起きた失敗を確認し、それらを分類してください。

マルチホップの質問
最初の検索の回答によって、二回目の検索内容が決まります。たとえば、契約の仕入先を特定してから、その仕入先に適用される条項を探す場合です。
ドキュメント間の比較
「契約 A と契約 B の予告期間を比較して」という質問には、二つの別々の検索が必要です。クエリが一つだけだと、同じ契約から五つのパッセージが返ってくることがあります。
性質の異なる複数のソース
SQL データベース内の数値と、PDF 内の説明。Microsoft のコースでデータベースとの複雑なやり取りについて説明されているとおり、モデルは質問に応じてツールを選択します。
待ち時間の許容度
文書検索、資料の準備、バックグラウンド処理:結果を人が読み直すのであれば、数十秒程度余分にかかっても誰も困りません。
速度より正確性を優先
不完全な回答のコストが高い場合は、検証ステップを設ける価値があります。ただし、責任を伴う判断については、人によるレビューを残すことが条件です。

逆に、次のような場合はシンプルな RAG のままにしてください。

FAQ と単一のソースで答えられる質問
回答が一つの箇所に含まれている場合、検索は一回で足り、ループは待ち時間を増やすだけです。
小規模なハードウェア上の対話型アシスタント
ユーザーがすばやい応答を期待しており、モデルがすでに限界近くで動作している場合。
適切な箇所を取りこぼす検索
まず、チャンク分割、埋め込み、ハイブリッド検索を修正してください。調整が不十分な検索をやり直しても、別の不適切な結果が返ってくるだけです。
構造化データに対する厳密な計算
作成してテスト済みの SQL クエリは、エージェントがその場で組み立てるクエリよりも信頼できます。
コーパス全体にわたるエンティティ間の複数の関連
大規模にリレーションをたどる必要がある場合、グラフベースの手法は別の課題に対応するものです。GraphRAG のガイドを参照してください。

#レイテンシとモデルにかかるコスト

クラウドでは、ループの各周回が請求額に表れます。ローカルでは、経過時間とメモリ使用量に表れます。重要な項目は三つあります。

#レイテンシが何倍にも増えます

N 回の検索に対して、モデルは N + 1 回生成を行います。各検索の前に一度ずつ、そして回答のために最後に一度です。シングルパスの RAG では、生成は一度だけです。プロンプトはターンごとに長くなるため、ランタイムのコンテキストキャッシュで再利用できる部分を除き、生成のたびにより多くのテキストを読み直すことになります。合計時間はモデル、量子化、マシンによって異なります。一般的な数値に頼るのではなく、ご自身の環境で測定してください。

#コンテキストはすぐにいっぱいになります

試算例:1 トークンあたり 3~4 文字とすると、1,200 文字のパッセージ 4 件は約 1,400 トークンに相当します。したがって、検索を五回行うと、質問、指示、モデルの思考を除いても約 7,000 トークンが加わります。num_ctx が 8192 の場合、余裕はほとんどなくなります。このとき Ollama はコンテキストを切り詰めるため、最初の検索結果や、場合によっては指示がウィンドウから外れることがありますが、アプリケーションがそれを明確に通知するとは限りません。

num_ctx を増やすとメモリ消費が増え、以降のターンが遅くなります。多くの場合、返すパッセージを減らすか、内容を要約してから再投入するほうが得策です。

#モデルはループを維持できる必要があります

モデルには、正しい形式のツール呼び出しを生成し、読んだ内容で十分かどうかを判断することが求められます。対応状況は ollama show で確認してください。Capabilities セクションに tools と記載されている必要があります。この確認でわかるのは形式に対応していることであり、モデルが適切な判断を下すことではありません。

RTX 3060 12 GB または RTX 4070 12 GB
Q4_K_M の 7~9B モデル(当サイトの目安:7B ≈ 5 GB)であれば、コンテキストと埋め込みのための余裕が残ります。これが以下の例の出発点で、qwen3.5:9b を使用します。
RTX 4080 16 GB
Q4_K_M の 14B モデル(≈ 9 GB)であれば、拡張したコンテキストに対してより余裕を持って収まります。
RTX 4090 24 GB または M4 Pro 24~48 GB
qwen3.8:27b(約 18 GB)のような、より大きなモデルも検討できるようになります。24 GB の場合は、コンテキストを拡張した後の残りメモリに注意してください。
!
二つのモデル間の行き来
ループでは埋め込みと生成が交互に行われます。二つのモデルが同時にメモリに収まらない場合、Ollama がターンごとに一方をアンロードして他方をロードすることがあり、その時間が他のすべての時間に加算されます。実行中に ollama ps で確認し、必要に応じて OLLAMA_MAX_LOADED_MODELS を設定してください。

#オーケストレーターを選ぶ

オーケストレーターはループを実行します。ツール呼び出しを受け渡し、結果をモデルに返し、制限を適用します。方法は三つあり、いずれもローカルモデルに対応しています。

手書きのループ
Ollama のチャット API とその tools パラメーターを中心とした二十行ほどのコードです。仕組みの理解や、ごく単純なケースに適しています。上限とトレースはご自身で実装することになります。
LangGraph
ループは、ノードとエッジからなる明示的なグラフであり、状態、再帰上限、ステップのストリーミングを備えています。各遷移を制御したい場合に適しています。本ガイドの例でもこの方法を採用しています。
LlamaIndex
取り込み処理とインデックスがすでにそちらにある場合に適しています。そのエージェントは、既存のクエリエンジンをツールとして再利用します。

人気で選ばないでください。パイプラインがすでにいずれかのフレームワークで動作している場合は、それを使い続けてください。いずれの場合も、バージョンは固定してください。これらのライブラリは変化が速く、構築済みの関数も変更されます。そのため本ガイドの例ではグラフを明示的に記述しており、これによってループも読みやすくなっています。

#前提条件

Ollama
インストール済みで、http://localhost:11434 で待ち受けています。
Python
比較的新しい仮想環境。インストール時に、パッケージが要求するバージョンを確認してください。
ドキュメント
mes_documents フォルダーに入れた数個の Markdown ファイルまたはテキストファイル。PDF の場合は、ChromaDB チュートリアルの取り込み手順を利用してください。
メモリ
9Bのモデルとそのコンテキスト用に約6 GBの空き容量、num_ctxを広げる場合はそれ以上。

#LangGraph と Ollama を使った Agentic RAG の例

この例では、ローカルの ChromaDB インデックス上に置いた検索ツールを一つだけ持つエージェントを構築します。ループの動きが見えるよう、あえて最小限の構成にしています。

!
このガイドのためには実行していないコード
このコードは各ライブラリのドキュメントに記載された API に沿っていますが、執筆時には実行しておらず、出力も掲載していません。小規模なコーパスでテストし、インストールするバージョンのドキュメントを参照してください。

#ステップ1:モデルとパッケージをインストールする

ターミナル
ollama pull qwen3.5:9b
ollama pull nomic-embed-text
ollama show qwen3.5:9b

python -m venv .venv
source .venv/bin/activate
pip install langgraph langchain-ollama chromadb ollama
pip freeze > requirements.txt

#ステップ 2:ドキュメントのインデックス作成

文字数による分割は、例を短くするためだけに使用しています。実際のコーパスでは、文書構造を考慮した分割と、対象のフランス語テキストで検証済みの埋め込みモデルを使用してください。

index.py
from pathlib import Path

import chromadb
import ollama

EMBED_MODEL = "nomic-embed-text"

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection(
    "docs", metadata={"hnsw:space": "cosine"}
)


def decouper(texte, taille=1200, recouvrement=150):
    pas = taille - recouvrement
    return [texte[i:i + taille] for i in range(0, len(texte), pas)]


for fichier in Path("mes_documents").glob("*.md"):
    morceaux = decouper(fichier.read_text(encoding="utf-8"))
    if not morceaux:
        continue
    vecteurs = ollama.embed(model=EMBED_MODEL, input=morceaux)["embeddings"]
    collection.upsert(
        ids=[f"{fichier.name}-{i}" for i in range(len(morceaux))],
        documents=morceaux,
        embeddings=vecteurs,
        metadatas=[{"source": fichier.name} for _ in morceaux],
    )

print(collection.count(), "passages indexés")

#ステップ 3:ツールとループを記述する

ツールの説明は重要です。モデルはこの説明を読んで、ツールを呼び出すかどうかを判断します。システムプロンプトでは、回答前に検索すること、サブ質問に分けること、情報がない場合はそれを認めることを義務付けています。

agent.py
import chromadb
import ollama
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.tools import tool
from langchain_ollama import ChatOllama
from langgraph.graph import START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition

EMBED_MODEL = "nomic-embed-text"
collection = chromadb.PersistentClient(path="./chroma_db").get_collection("docs")


@tool
def chercher_documents(requete: str) -> str:
    """Cherche dans les documents internes. Utiliser une requête courte
    portant sur un seul sujet. Renvoie des passages précédés de leur source."""
    vecteur = ollama.embed(model=EMBED_MODEL, input=requete)["embeddings"][0]
    res = collection.query(query_embeddings=[vecteur], n_results=4)
    passages = zip(res["documents"][0], res["metadatas"][0])
    return "\n\n".join(f"[{m['source']}] {d}" for d, m in passages) or "Aucun passage."


outils = [chercher_documents]
llm = ChatOllama(model="qwen3.5:9b", temperature=0, num_ctx=8192).bind_tools(outils)

CONSIGNE = SystemMessage(content=(
    "Tu réponds uniquement à partir des résultats de chercher_documents. "
    "Si la question porte sur plusieurs sujets, fais une recherche par sujet. "
    "Si les passages ne suffisent pas, reformule une fois puis dis clairement "
    "ce que tu n'as pas trouvé. Cite la source entre crochets."
))


def agent(state: MessagesState):
    return {"messages": [llm.invoke([CONSIGNE] + state["messages"])]}


graphe = StateGraph(MessagesState)
graphe.add_node("agent", agent)
graphe.add_node("tools", ToolNode(outils))
graphe.add_edge(START, "agent")
graphe.add_conditional_edges("agent", tools_condition)
graphe.add_edge("tools", "agent")
app = graphe.compile()

if __name__ == "__main__":
    question = "Compare la durée de préavis du contrat A et du contrat B."
    entree = {"messages": [HumanMessage(content=question)]}
    for etape in app.stream(entree, {"recursion_limit": 12}, stream_mode="values"):
        etape["messages"][-1].pretty_print()

#ステップ 4:トレースを読む

stream_mode="values" の利点は、各メッセージを表示できることです。最終回答の前に、次のチェック項目を参考にしてトレースを読んでください。

  1. 01
    モデルはツールを呼び出していますか?
    モデルが直接回答する場合、検索は行われていません。指示を強化するか、モデルを変更してください。
  2. 02
    クエリは意味の通るものになっていますか?
    比較の場合、理想的には契約書ごとに一つのクエリが発行されることが期待されます。質問全体をそのまま含む単一のクエリしかない場合は、分解が行われていないことを示しています。
  3. 03
    取得した箇所は質問に答えていますか?
    抜粋が質問と無関係な場合、問題はエージェントではなくインデックスにあります。
  4. 04
    回答は抜粋に基づいていますか?
    角括弧で示された各引用を、対応する箇所と照らし合わせて確認してください。

#ステップ5:ループに上限を設ける

各検索はグラフのステップを二つ消費します(エージェント、次にツール)。したがって、上限を 12 にすると、五回の検索とその後の最終回答が可能です。これを超えると、LangGraph は GraphRecursionError(langgraph.errors モジュール)を送出します。ユーザーにエラートレースをそのまま見せるのではなく、アプリケーション側でこのエラーを捕捉し、内容が明確なメッセージを返してください。

#落とし穴とトラブルシューティング

モデルがツールをまったく呼び出さない
ollama show で tools を確認し、ツールの説明を書き直して、別のモデルを試してください。指示を長くしていくことで補おうとしないでください。
同じ検索を繰り返します
モデルが堂々巡りに陥っています。上限を下げ、ツール側では、そのクエリがすでに実行済みの場合に明確なメッセージを返すようにしてください。
回答が最初の検索結果を忘れてしまう
コンテキストが切り詰められている可能性が高い兆候です。n_results を減らすか、より細かく分割するか、メモリに余裕があれば num_ctx を増やしてください。
回答が非常に遅くなる
ollama ps を確認してください。モデルの一部が CPU で動作しているか、複数のモデルがメモリ上で入れ替わっている可能性があります。トレース内の周回数も数えてください。
何も見つからなかったのに、内容を捏造する
ループを使っても、内容の捏造は防げません。引用はプログラムで検証してください。引用された各ソースは、会話内のツール結果に含まれている必要があります。
強力すぎるツール
ドキュメントを読むエージェントは、その内容も受け取ります。その内容に悪意が含まれている可能性もあります。エージェントには読み取り専用のツールを与え、何かを変更する操作にはすべて人による承認を設けてください。
→
まずはエージェントなしで試してください
ループを追加する前に、固定の分解を試してください。質問をサブ質問に分割する生成、サブ質問ごとの検索、そして回答という流れです。これで比較の問題が解決するなら、予測可能な応答時間とよりシンプルなコードを維持できます。

#ご自身の質問をもとに判断する

他で公開されている数値は、別のコーパス、別のモデル、別の設定を対象としたものです。判断の決め手になるのはご自身のデータでの比較だけであり、その比較に多くのハードウェアは必要ありません。

  1. 01
    質問セットを作成する
    答えと出典箇所が分かっている実際の質問を用意し、単純な質問とマルチホップの質問を混ぜてください。各質問の種類を記録しておきます。
  2. 02
    基準を定める
    それらの質問を、エージェントなしで現在の RAG に通してください。正しい箇所が取得されたか、回答が正しいか、そして合計時間を記録します。
  3. 03
    変更は一つだけにする
    同じテストセットを、同じインデックス、同じモデル、同じマシンで、エージェントを使って実行してください。
  4. 04
    質問の種類別に読む
    単純な質問と複雑な質問で結果を分けて確認し、質問ごとの検索回数を記録してください。全体の平均だけでは、一方での改善と他方での時間の浪費が見えなくなることがあります。
  5. 05
    妥当なものを残す
    一部の質問でしか効果がない場合は、ルーティングで十分なことがあります。既定では直接 RAG を使い、複雑と判定された質問にはループを使います。

この比較の品質評価を自動化するには、Ragas に関するガイドで紹介している適切な指標を利用できます。パッケージのバージョン、モデル、量子化、num_ctx も記録してください。これらを記録しないと、翌月には結果を比較できなくなります。


#さらに詳しく

Agentic RAG は、すでに機能している RAG の上に構築するものです。これらのガイドでは、最初に安定させるべき基盤、関連するアプローチ、エージェントのアーキテクチャを取り上げています。

このガイドは役に立ちましたか?

ご意見、誤りのご指摘、補足はありますか?ぜひお知らせください。皆さんにとってより良いガイドにするために役立ちます。