0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Agentic RAGの自律判断を深掘り:LangGraphの思考プロセスとツール連携

0
Posted at

「従来のRAGでは、一度検索をミスしたら終わり」「検索結果が不十分でも、LLMはそのまま回答を生成してしまう」――多くのエンジニアがこの問題に直面しているのではないでしょうか。単なる情報検索ではなく、LLMエージェントが自律的に「思考」し、最適な情報源を判断、さらには失敗からリカバリする。そんな次世代のRAG、それがAgentic RAGです。

この記事では、Agentic RAGにおけるLLMの自律的な判断プロセスと、複数のツールを連携させる仕組みを、LangGraphを使った具体的なコード例と共に深掘りします。特に、内部的な思考プロセスや、失敗からのリカバリ戦略までを解説し、実践的なAIアプリケーション開発に役立つ知見を提供します。


Agentic RAGとは何か? 従来のRAGとの決定的な違い

まず、Agentic RAGが従来のRAGとどう異なるのか、その本質を理解しましょう。

従来のRAGの限界とAgentic RAGの登場背景

従来のRAG(Retrieval Augmented Generation)は、ユーザーのクエリに基づいて一度ベクトルデータベースを検索し、その結果をLLMに渡して回答を生成する、という単一パスのパイプラインでした。シンプルで効果的である一方、以下のような課題がありました。

  • 検索の一発勝負: 初期検索で適切な情報が得られなかった場合、その後の生成品質も低下します。検索クエリの最適化や再試行は自動で行われません。
  • セマンティック検索の意図誤解: ユーザーの質問意図をセマンティック検索が正確に捉えきれない場合、関連性の低いドキュメントが取得されることがあります。
  • 検索結果の評価不足: 取得したコンテキストが質問に十分答えているか、LLMが「自己評価」するメカニズムがありません。情報不足でもそのまま回答を生成してしまう「サイレントハルシネーション」のリスクがあります。
  • マルチホップ質問への対応困難: 複数ステップの推論や複数回の情報収集が必要な複雑な質問には、単一の検索では対応できません。

これらの課題を解決するために登場したのがAgentic RAGです。Agentic RAGは、LLMを単なる回答生成器ではなく、自律的なエージェントとして位置づけます。エージェントは、質問に対する最適な回答を生成するために、計画、ルーティング、検証、再試行といった一連の思考プロセスをループ内で実行します。

Agentic RAGの定義と特徴

LangChain Blogによると、Agentic RAGは「AIエージェントが実行ループ内で検索の意思決定を制御するRAGシステム」と定義されています。その特徴は以下の通りです。

  1. 自律的な推論と多段階の計画: LLMが自身の「思考」を逐次展開し、「次に何をすべきか」「どの情報源を当たるか」を決定します。
  2. 動的なツールオーケストレーション: ベクトル検索だけでなく、Web検索、SQLデータベース、API呼び出しなど、多様なツールを状況に応じて使い分けます。
  3. 検索結果の検証とフィードバックループ: 取得した情報が質問に十分答えているか、LLM自身が評価します。情報が不足していれば、検索クエリを最適化したり、別の情報源にアクセスしたりと、リカバリ戦略を実行します。
  4. 適応的なRAGパイプライン: 固定されたパイプラインではなく、質問の複雑さや検索結果の品質に応じて、動的に情報収集・生成プロセスを調整します。

これにより、Agentic RAGは「単一の検索では答えられない質問」や「情報の信頼性を高めたい」といった要求に応えることが可能になります。

LangGraphがAgentic RAGの自律判断をどう実現するか

LangGraphは、Agentic RAGのようなステートフルなLLMエージェントを構築するための強力なフレームワークです。ここでは、LangGraphがどのようにエージェントの「思考プロセス」と「ツール連携」を実現するのかを掘り下げます。

LangGraphの基本概念:グラフ、ノード、エッジ、状態

LangGraphは、LLMエージェントの振る舞いを「グラフ」として表現します。このグラフは、以下の要素で構成されます。

  • 状態 (State): エージェントの現在の状況を表すデータ構造です。ユーザーの質問、検索されたドキュメント、生成された回答、エラー情報、再試行回数など、エージェントが判断を下すために必要なすべての情報を含みます。
  • ノード (Node): グラフ内の各ステップに対応します。LLMの呼び出し、外部ツールの実行(Web検索、DB検索など)、ドキュメントの評価、条件分岐の判断ロジックなどがノードとして定義されます。
  • エッジ (Edge): ノード間の遷移を示します。エージェントの判断に応じて、どのノードに次に進むかを決定します。条件付きエッジ(Conditional Edge)を使用することで、LLMの出力やノードの実行結果に基づいて動的に経路を切り替えることができます。

このグラフ構造により、LLMエージェントは「質問を受け取る」→「検索する」→「検索結果を評価する」→「不十分なら再検索する」→「十分なら回答を生成する」といった複雑な思考プロセスを、明確なフローとして表現し、実行できます。

Agentic RAGにおけるLangGraphの役割

LangGraphは、特に以下の点でAgentic RAGの中核を担います。

  1. ステートフルな実行: エージェントの状態を永続的に保持し、各ノードの処理結果を次のノードに引き継ぎます。これにより、多段階の思考やループ処理が可能になります。
  2. 条件分岐とループ制御: LLMの出力やツール実行結果に基づいて、次に実行すべきノードを動的に決定します。これにより、検索結果が不十分な場合の再検索や、回答の自己評価に基づく修正ループを簡単に実装できます。
  3. モジュール化された設計: 各ステップをノードとして独立させることで、システムの保守性や拡張性が向上します。特定のノードだけを入れ替えたり、新しいツールを追加したりするのが容易になります。
  4. Human-in-the-Loop (HITL) の組み込み: 特定の条件(例: 再試行回数超過、重要な意思決定)で人間の介入を待機するノードを組み込むことで、システムと人間の協調を設計できます。

LangGraphを用いたAgentic RAGのワークフロー例

具体的なAgentic RAGのワークフローをLangGraphで設計する例を見てみましょう。

状態定義 (AgentState)

エージェントの状態を定義します。これは、各ノードが共有し、更新していく情報です。

from typing import List, TypedDict, Optional

class AgentState(TypedDict):
    """
    Agentic RAGワークフローの状態を定義するTypedDict。
    各ノードはこの状態を読み書きする。
    """
    question: str  # ユーザーの元の質問
    documents: List[str]  # 検索されたドキュメントのリスト(内容を文字列として保持)
    generation: str  # 生成された最終回答
    grade: Optional[str]  # ドキュメントまたは回答の評価結果(例: "complete", "incomplete", "irrelevant", "hallucination")
    retry_count: int  # 特定のステップの再試行回数
    # エラーハンドリングのためのフィールド
    error_message: Optional[str] # エラーが発生した場合のメッセージ
    tool_calls: Optional[List[dict]] # 実行されたツールとその引数
    # Web検索用のクエリなど、中間状態を追加することも可能
    web_search_query: Optional[str]

このAgentStateは、エージェントが「思考」し、次の行動を決定するための「記憶」となります。

ワークフロー (Graph Topology) の設計

以下は、Agentic RAGの典型的なワークフローをLangGraphで表現した際のノードとエッジの例です。

  1. Retrieve Node(検索):

    • 役割: ユーザーの質問に基づき、社内ドキュメントなどのベクトルデータベースから関連性の高いチャンクを取得します。
    • アクション: 質問 state["question"] を入力とし、ベクトル検索を実行。結果を state["documents"] に追加します。
    • 遷移: 常に Grade Documents Node へ。
  2. Grade Documents Node(ドキュメント評価 - CRAGの実装):

    • 役割: 取得したドキュメントが質問に答えるのに十分かをLLMに評価させます。Corrective RAG (CRAG) のアプローチを模倣します。
    • アクション: LLMに state["question"]state["documents"] を渡し、「ドキュメントは質問に完全な回答を提供しているか?」を判断させ、state["grade"] に"complete", "incomplete", "irrelevant"のいずれかを設定します。
    • 遷移 (条件付きエッジ):
      • state["grade"] == "complete" の場合 → Generate Node へ。
      • state["grade"] == "incomplete" の場合 → Expand / Rewrite Query Node へ(Web検索などでの情報拡張を試みる)。
      • state["grade"] == "irrelevant" の場合 → Rewrite Query Node へ(質問の意図を修正して再検索を試みる)。
  3. Expand / Rewrite Query Node(クエリ修正・情報拡張):

    • 役割: ドキュメントが不十分または関連性が低いと判断された場合、次の検索ステップのためにクエリを最適化したり、情報源を切り替えたりします。
    • アクション:
      • Expand の場合: 既存ドキュメントの文脈を広げるために、関連する追加チャンクを取得(例: 親ドキュメント検索)。
      • Rewrite の場合: state["question"] と現在の state["documents"] を基に、Web検索に最適な新しいクエリ state["web_search_query"] をLLMに生成させます。
    • 遷移: 常に Web Search Node へ。
  4. Web Search Node(Web検索):

    • 役割: 最適化されたクエリを使ってWeb検索を実行し、外部情報を取得します。
    • アクション: state["web_search_query"] を使ってTavilyなどのWeb検索ツールを実行。結果を state["documents"] に追加します。
    • 遷移: 常に Grade Documents Node へ(Web検索結果も再度評価するため、ループに戻ります)。
  5. Generate Node(回答生成):

    • 役割: 十分なコンテキストが揃った状態で、最終的な回答を生成します。
    • アクション: state["question"]state["documents"] をLLMに渡し、回答を生成。結果を state["generation"] に設定します。
    • 遷移: 常に Reflect Node へ。
  6. Reflect Node(自己反省 - Self-RAGの実装):

    • 役割: 生成された回答が質問に正確に答えているか、ハルシネーションを含んでいないかをLLMに自己評価させます。Self-RAGのコンセプトを模倣します。
    • アクション: LLMに state["question"]state["documents"]state["generation"] を渡し、「回答は正確で質問に完全に答えているか?」を判断させ、state["grade"] に "accurate", "hallucination", "incomplete" のいずれかを設定します。
    • 遷移 (条件付きエッジ):
      • state["grade"] == "accurate" の場合 → END (ワークフロー終了)。
      • state["grade"] == "hallucination" または state["grade"] == "incomplete" の場合 → Rewrite Query Node へ(回答を修正するために、再度情報収集・生成ループに入る)。この際、state["retry_count"] をインクリメントし、上限に達したら Error Handling Node へ。
  7. Error Handling / Human-in-the-Loop Node:

    • 役割: エラーが発生した場合や、再試行回数が上限を超えた場合など、特定の条件でエラーを処理したり、人間の介入を促したりします。
    • アクション: state["error_message"] を記録したり、外部システムに通知したり、ユーザーに確認を促すメッセージを生成したりします。
    • 遷移: 必要に応じて END や、特定のリカバリーノードへ。

このように、LangGraphを使うことで、LLMエージェントが自律的に状況を判断し、最適な行動を選択する多段階の思考プロセスを、視覚的にも分かりやすいグラフとして設計・実装できます。

実装例:LangGraphとTavilyを連携したAgentic RAG

具体的な実装例として、atsushi3hsgw/agentic_rag_sampleを参考に、LangGraphでWeb検索ツール(Tavily)と連携するAgentic RAGの主要部分を見ていきましょう。この例では、初期の社内ドキュメント検索で情報が不足している場合に、Web検索にフォールバックし、さらにその検索クエリをLLMが最適化するという挙動を実装しています。

環境準備とツールの定義

必要なライブラリをインストールし、APIキーを設定します。

pip install -U langchain langchain-openai langgraph tavily-python python-dotenv

.envファイルにAPIキーを設定します。

OPENAI_API_KEY="sk-..."
TAVILY_API_KEY="tvly-..."

TavilyというWeb検索ツールをLangChainのToolとして定義します。

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from typing import List, TypedDict, Optional

load_dotenv()

# LLMの初期化
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# Tavily Web検索ツールを定義
# @toolデコレータを使うことで、LLMが関数呼び出しとして認識できるようになります
@tool
def tavily_search(query: str, max_results: int = 5) -> List[dict]:
    """
    Tavily APIを使用してWeb検索を実行します。
    指定されたクエリに基づいて関連性の高い検索結果を返します。
    """
    from langchain_community.tools.tavily_search import TavilySearchResults
    search_tool = TavilySearchResults(max_results=max_results)
    results = search_tool.invoke({"query": query})
    return results

# AgentStateは既に上記で定義済みとします
class AgentState(TypedDict):
    question: str
    documents: List[str]
    generation: str
    grade: Optional[str]
    retry_count: int
    error_message: Optional[str]
    web_search_query: Optional[str]

LangGraphノードの実装

ここでは、ドキュメント評価、Web検索、回答生成の主要なノードを実装します。

ドキュメント評価ノード (grade_documents)

LLMが検索されたドキュメントが質問に答えるのに十分かを評価します。これはCRAG (Corrective RAG) の考え方に基づいています。

from langchain_core.prompts import ChatPromptTemplate

def grade_documents(state: AgentState) -> AgentState:
    """
    検索されたドキュメントが質問に答えるのに十分かを評価するノード。
    LLMに「complete」「incomplete」「irrelevant」のいずれかを判断させる。
    """
    print("---GRADE DOCUMENTS---")
    question = state["question"]
    documents = "\n".join(state["documents"])

    prompt = ChatPromptTemplate.from_messages([
        ("system", """
         あなたはドキュメントの評価者です。
         提供されたドキュメントが、ユーザーの質問に完全に答えるのに十分な情報を含んでいるかを判断してください。
         判断結果は以下のいずれかの文字列で出力してください:
         - "complete": ドキュメントが質問に完全に答えている
         - "incomplete": ドキュメントが質問に答える情報を含んでいるが、不十分であるか、追加情報が必要
         - "irrelevant": ドキュメントが質問と全く関連がない

         ドキュメントの内容に基づいて厳密に判断し、正直に評価してください。
         """),
        ("human", "質問: {question}\n\nドキュメント:\n{documents}")
    ])

    try:
        grade_chain = prompt | llm
        grade_result = grade_chain.invoke({"question": question, "documents": documents})
        grade = grade_result.content.strip().lower()

        # 結果を正規化
        if "complete" in grade:
            state["grade"] = "complete"
        elif "incomplete" in grade:
            state["grade"] = "incomplete"
        elif "irrelevant" in grade:
            state["grade"] = "irrelevant"
        else:
            # LLMが予期しない出力をした場合のフォールバック
            print(f"Warning: Unexpected grade result: {grade}. Defaulting to 'incomplete'.")
            state["grade"] = "incomplete"

        print(f"Documents Grade: {state['grade']}")
        return state
    except Exception as e:
        state["error_message"] = f"Error in grading documents: {e}"
        print(f"Error in grade_documents: {e}")
        return state

Web検索ノード (web_search)

Tavilyツールを使ってWeb検索を実行します。

def web_search(state: AgentState) -> AgentState:
    """
    Web検索ツール(Tavily)を実行し、検索結果をドキュメントに追加するノード。
    必要に応じて、質問をWeb検索用にリライトする。
    """
    print("---WEB SEARCH---")
    question = state["question"]
    current_documents = state["documents"]
    grade = state["grade"]

    search_query = question # 初期は元の質問を使用

    # ドキュメントが不十分または関連性がない場合、Web検索クエリをリライト
    if grade in ["incomplete", "irrelevant"]:
        print("---REWRITING WEB SEARCH QUERY---")
        rewrite_prompt = ChatPromptTemplate.from_messages([
            ("system", """
             あなたはクエリリライターです。
             ユーザーの質問と既存のドキュメントに基づいて、Web検索に最適な新しいクエリを生成してください。
             特に、既存のドキュメントで不足している情報や、質問の意図を明確にするようにリライトしてください。
             元の質問が曖昧な場合や、特定のキーワードが不足している場合に有効です。
             出力はWeb検索クエリのみとし、余計な説明は含めないでください。
             """),
            ("human", "元の質問: {question}\n\n既存のドキュメント:\n{documents}\n\n新しいWeb検索クエリ:")
        ])
        try:
            rewrite_chain = rewrite_prompt | llm
            rewritten_query = rewrite_chain.invoke({"question": question, "documents": "\n".join(current_documents)})
            search_query = rewritten_query.content.strip()
            state["web_search_query"] = search_query # リライトされたクエリを状態に保存
            print(f"Rewritten Web Search Query: {search_query}")
        except Exception as e:
            state["error_message"] = f"Error in rewriting query: {e}"
            print(f"Error in query rewrite: {e}. Using original question for search.")

    try:
        # Tavily検索ツールを実行
        # tavily_searchはLangChainのToolとして定義されているため、直接呼び出し可能
        search_results = tavily_search.invoke({"query": search_query})
        
        # 検索結果からコンテンツを抽出し、既存のドキュメントに追加
        new_docs = [r['content'] for r in search_results]
        state["documents"].extend(new_docs)
        print(f"Added {len(new_docs)} documents from web search.")
        return state
    except Exception as e:
        state["error_message"] = f"Error during web search: {e}"
        print(f"Error in web_search: {e}")
        return state

回答生成ノード (generate_answer)

最終的な回答を生成します。

def generate_answer(state: AgentState) -> AgentState:
    """
    取得したドキュメントと質問に基づいて回答を生成するノード。
    """
    print("---GENERATE ANSWER---")
    question = state["question"]
    documents = "\n".join(state["documents"])

    prompt = ChatPromptTemplate.from_messages([
        ("system", """
         あなたは熟練したアシスタントです。
         以下の質問に対し、提供されたドキュメントのみに基づいて、簡潔かつ正確に回答を生成してください。
         ドキュメントに情報がない場合は、その旨を正直に伝えてください。
         """),
        ("human", "質問: {question}\n\nドキュメント:\n{documents}")
    ])

    try:
        generation_chain = prompt | llm
        answer = generation_chain.invoke({"question": question, "documents": documents})
        state["generation"] = answer.content
        print("Answer Generated.")
        return state
    except Exception as e:
        state["error_message"] = f"Error in generating answer: {e}"
        print(f"Error in generate_answer: {e}")
        return state

グラフの構築と実行

LangGraphのStateGraphを使って、ノードとエッジを定義し、ワークフローを構築します。

from langgraph.graph import StateGraph, END

# グラフの構築を開始
workflow = StateGraph(AgentState)

# ノードの追加
# ここでは社内ドキュメント検索は省略し、直接Web検索フローに入る
# 実際にはここに`retrieve_internal_docs`のようなノードが入る
workflow.add_node("grade_documents", grade_documents)
workflow.add_node("web_search", web_search)
workflow.add_node("generate_answer", generate_answer)

# エッジの定義
# エントリポイントからドキュメント評価へ
workflow.set_entry_point("grade_documents")

# ドキュメント評価後の条件分岐
def route_grade(state: AgentState) -> str:
    """
    ドキュメントの評価結果に基づいて次のノードを決定するルーター関数。
    """
    print(f"---ROUTE GRADE: {state['grade']}---")
    if state["grade"] == "complete":
        return "generate"
    elif state["grade"] in ["incomplete", "irrelevant"]:
        # ドキュメントが不十分/関連性がない場合、Web検索に進む
        return "web_search"
    else:
        # 予期しない評価の場合、エラー処理またはデフォルトでWeb検索
        return "web_search" # または "error_handler" など

workflow.add_conditional_edges(
    "grade_documents",
    route_grade,
    {
        "generate": "generate_answer",
        "web_search": "web_search",
        # "error_handler": "error_handling_node" # エラーハンドリングノードを追加する場合
    },
)

# Web検索後の処理
# Web検索後も再度ドキュメント評価を行うことで、情報の質を保証
workflow.add_edge("web_search", "grade_documents")

# 回答生成後の処理
# 自己反省ノードを入れることもできるが、ここでは簡略化して終了
workflow.add_edge("generate_answer", END)

# グラフをコンパイル
app = workflow.compile()

# 実行例
if __name__ == "__main__":
    # ここに社内ドキュメント検索の結果を入れることもできる
    # 簡単のため、初期ドキュメントは空とする
    initial_state = {
        "question": "LangGraphの主な特徴と、Agentic RAGにおけるその役割について教えてください。",
        "documents": [],
        "generation": "",
        "grade": None,
        "retry_count": 0,
        "error_message": None,
        "web_search_query": None,
    }

    # グラフの実行
    for s in app.stream(initial_state):
        print(s)
        print("---")

    final_state = app.invoke(initial_state)
    print("\n---FINAL ANSWER---")
    print(final_state["generation"])
    if final_state["error_message"]:
        print(f"Error encountered: {final_state['error_message']}")

    print("\n---ANOTHER QUESTION---")
    initial_state_2 = {
        "question": "2023年の日本のGDP成長率について教えてください。",
        "documents": [],
        "generation": "",
        "grade": None,
        "retry_count": 0,
        "error_message": None,
        "web_search_query": None,
    }
    for s in app.stream(initial_state_2):
        print(s)
        print("---")

    final_state_2 = app.invoke(initial_state_2)
    print("\n---FINAL ANSWER 2---")
    print(final_state_2["generation"])
    if final_state_2["error_message"]:
        print(f"Error encountered: {final_state_2['error_message']}")

このコードを実行すると、LLMがまずドキュメントを評価し、情報が不足していると判断すればWeb検索クエリをリライトし、Tavilyで検索を実行。その結果を再度評価し、十分であれば回答を生成するという一連のAgentic RAGの思考プロセスを追うことができます。

よくあるエラーとハマりどころ、そして回避策

Agentic RAGは強力ですが、多段階の複雑なシステムであるため、開発中にいくつかの課題に直面することがあります。

1. 無限ループ

  • 問題: エージェントの判断によっては、同じノード間を繰り返し遷移し、無限ループに陥る可能性があります。例えば、「検索結果が常に不十分と判断され、再検索が繰り返される」ケースなどです。
  • 回避策:
    • カウンタの導入: AgentStateretry_countのようなカウンタを含め、特定のループ内で最大試行回数を超えたら強制的にループを終了させるか、Error Handling Nodeに遷移させます。
    • 明確な終了条件: 各ノードのLLMプロンプトに「このタスクが完了したと判断できる条件」を明確に記述させ、LLMが「完了」と判断したらループを抜けるような条件分岐を設けます。
    • Human-in-the-Loop (HITL): 再試行回数が閾値を超えた場合などに、人間の介入を促すノードを設け、手動で次のステップを指示できるようにします。

2. ステートの肥大化とエラー伝播の難しさ

  • 問題: LangGraphのAgentStateはエージェントのすべての情報を保持するため、複雑なワークフローではステートが肥大化しやすく、エラーが発生した際にその情報が適切に伝播せず、デバッグが困難になることがあります。
  • 回避策:
    • エラー情報の集約: AgentStateerror_message: Optional[str]のようなフィールドを設け、各ノードでエラーが発生した際にこのフィールドを更新します。
    • 中央エラーハンドリングノード: エラーが発生した場合に遷移する専用のError Handling Nodeを設け、そこでエラーログの記録、通知、リカバリー戦略の実行などを行います。これにより、エラー処理ロジックが一箇所に集約され、デバッグが容易になります。
    • 粒度の調整: ステートに含める情報は、そのエージェントの「現在の思考と行動」に直接必要なものに限定し、不必要な情報は含めないようにします。

3. ハルシネーション(幻覚)と不正確な回答

  • 問題: Agentic RAGは精度向上を期待できますが、それでもLLMのハルシネーションや、取得した情報が不正確・不十分な場合に誤った回答を生成するリスクは残ります。
  • 回避策:
    • 自己評価 (Reflection/Self-Critique): 生成された回答が質問に正確に答えているか、矛盾がないかを別のLLM呼び出しで評価するReflect Nodeを導入します。不十分なら再検索や回答書き直しに進むループを組み込みます。
    • Corrective RAG (CRAG): ドキュメントを「正確/曖昧/不正確」に分類し、不十分な場合はWeb検索などの別の情報源にフォールバックするロジックをGrade Documents Nodeに実装します。
    • 評価指標の導入: RAGAS、DeepEval、TruLensなどの評価ツールを開発・運用パイプラインに組み込み、オフライン評価、CIゲートでの評価、本番環境での監視を通じて、継続的に回答品質を測定・改善します。

4. LLMプロンプトエンジニアリングの難しさ

  • 問題: 各ノードでLLMに適切な判断(検索、評価、リライトなど)をさせるためのプロンプト設計が難しいです。曖昧な指示や不適切なFew-shot例は、エージェントのパフォーマンスを低下させます。
  • 回避策:
    • 明確な指示と役割分担: 各LLMプロンプトには、そのLLMが実行すべきタスクと期待される出力形式(例: {"grade": "complete"}のようなJSON形式)を具体的に指示します。
    • Few-shot Learning: 適切な入力と出力のペアをFew-shot例として提供し、LLMの判断をガイドします。
    • 出力フォーマットの強制: LangChainのwith_structured_outputやPydanticなどのライブラリを用いて、LLMの出力フォーマットを厳密に定義し、パースエラーを防ぎます。
    • プロンプトのバージョン管理とA/Bテスト: プロンプトもコードと同様にバージョン管理し、異なるプロンプトの効果を評価ツールで比較検討します。

これらの回避策を適切に設計に組み込むことで、より堅牢で信頼性の高いAgentic RAGシステムを構築できます。

設計上のトレードオフとベストプラクティス

Agentic RAGを導入する際には、そのメリットだけでなく、トレードオフを理解し、適切な設計判断を下すことが重要です。

トレードオフ: 精度 vs コスト・レイテンシ

Agentic RAGは、従来のRAGよりも高い精度と柔軟性を提供しますが、これはLLMの呼び出し回数の増加と引き換えになります。

  • コストの増加: LLMの呼び出し回数が増えるため、それに伴いAPI利用料が増加します。実測値として、従来のRAGと比較してトークン数が3〜10倍になる傾向があります。
  • レイテンシの増加: 複数のノードとループを経て回答が生成されるため、応答までの時間が長くなります。レイテンシが2〜5倍になることも珍しくありません。

ベストプラクティス:

  • ユースケースに応じた選択: 高い精度や複雑な情報収集が求められるユースケース(例: 法務・医療分野でのQ&A、複雑なデータ分析)にはAgentic RAGが適していますが、シンプルなFAQ応答など、レイテンシとコストが最優先される場合は、従来のRAGや軽量なRAGアプローチも検討するべきです。
  • 早期終了の最適化: 質問が単純で、初期検索で十分な情報が得られた場合は、不必要なループに入らずに早期に回答を生成するような条件分岐を設計します。
  • モデルサイズの選択: 各ノードで呼び出すLLMは、タスクの複雑性に応じて最適なモデルサイズを選択します。例えば、評価ノードには軽量なモデル、最終回答生成には高性能なモデルを使うなどです。

複雑性 vs 柔軟性

Agentic RAGは、動的な意思決定と多段階処理を可能にするため、実装の複雑性が増します。

ベストプラクティス:

  • 段階的な導入: 最初から完璧なAgentic RAGを目指すのではなく、まずはClassic RAGやHybrid RAGで検索品質と評価基盤を固めます。その上で、検索失敗、意図誤解、根拠不一致といった課題が残る場合に、Corrective RAG、Self-RAG、そしてAgentic RAGを段階的に導入することを検討します。
  • モジュール化と役割分担:
    • Agentic RAGをRetrieval System、Generation Model、Agent Layerの3つの主要コンポーネントに明確に分けます。
    • LLMは単なる回答生成装置ではなく、思考エンジン兼制御タワーとして機能させるよう設計します。
    • 各ノードのLLMには、明確な役割と期待される出力を定義し、プロンプトをシンプルに保ちます。
  • ツール連携の最適化:
    • Function CallingやTool Useの仕組みを活用し、ベクトル検索だけでなく、SQLデータベース、Web検索、グラフデータベースなど、複数のツールを問いの性質に応じて使い分けます。
    • 将来的には、MCP (Model Context Protocol) のような標準プロトコルを利用して、社内DB、SaaS、Web検索といった多様なツール接続を標準化することも視野に入れます。(ただし、MCPはまだ発展途上の技術なので、公式ドキュメントで最新状況を確認しながら導入を検討してください。)

堅牢なエラーハンドリングと監視

Agentic RAGは失敗する可能性があるため、エラーハンドリングと監視は不可欠です。

ベストプラクティス:

  • Human-in-the-Loop (HITL): 重要度の高い操作の前や、エージェントが自律的に解決できないと判断した場合、あるいは再試行回数が上限を超えた場合などに、人間の判断を挟むノードを設計に組み込みます。
  • 永続化と状態管理: LangGraphのCheckpointer機能などを活用し、エージェントの途中状態を保存することで、長期実行タスク、承認待ちタスク、障害復旧、監査対応を可能にします。これにより、中断した処理を再開したり、エージェントの思考プロセスを後から検証したりできます。
  • 継続的な評価と監視: RAGAS、DeepEval、TruLensなどの評価ツールを組み合わせて、開発段階でのオフライン評価、CI/CDパイプラインでの自動評価、本番環境でのリアルタイム監視を行うことで、システムの信頼性と回答品質を継続的に高めます。

これらのベストプラクティスを適用することで、Agentic RAGの強力な機能を最大限に活用しつつ、実運用における課題を最小限に抑えることが可能になります。

まとめ

この記事では、Agentic RAGにおけるLLMエージェントの自律的な判断プロセスとツール連携の仕組みを、LangGraphを用いた具体的な実装例と共に解説しました。

  • Agentic RAGは、LLMを単なる回答生成器ではなく、計画・検索・検証・再試行をループ内で実行する「思考するエージェント」として位置づけることで、従来のRAGが抱える課題を解決します。
  • LangGraphは、この多段階の思考プロセスをグラフ構造で表現し、状態管理、条件分岐、ループ制御を容易にすることで、Agentic RAGの中核を担います。
  • 実装例では、Tavilyを使ったWeb検索ツールとLLMエージェントを連携させ、ドキュメントの評価に基づいたクエリのリライトと再検索のフローを示しました。
  • 無限ループ、ハルシネーション、ステートの肥大化といったAgentic RAG特有の課題に対する具体的な回避策も提示しました。
  • 精度とコスト・レイテンシのトレードオフを理解し、段階的な導入、モジュール化、堅牢なエラーハンドリングといったベストプラクティスを適用することが、成功の鍵となります。

Agentic RAGは、LLMアプリケーション開発における次の大きなステップです。LangGraphのようなフレームワークを使いこなし、LLMエージェントの自律的な能力を最大限に引き出すことで、より高度で信頼性の高いAIシステムを構築できるでしょう。

さらに深く学びたい方は、LangGraphの公式ドキュメントや、Agentic RAGに関するLangChainのブログ記事を参照することをお勧めします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?