0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RAGの限界を超える!LangGraphで動くAIエージェントの内部動作を理解する

0
Posted at

「RAGで期待した回答が得られない」「複雑な問い合わせに対応しきれない」と感じていませんか?従来のRAGは強力ですが、単一の知識ソースに限定され、動的な状況判断や多段階の推論には限界があります。

この記事では、RAGの限界を克服し、より複雑なタスクや動的な状況に対応するAIエージェントへの移行戦略を、LangGraphを用いた具体的な実装例と合わせて深掘り解説します。LangGraphの基本からAgentic RAGのワークフローまでを理解し、次世代LLMアプリ開発の基礎を築きましょう。

RAGの限界とAIエージェントが必要とされる背景

多くのエンジニアがLLMアプリケーション開発で直面するのが、従来のRAG(Retrieval-Augmented Generation)が持つ限界です。このセクションでは、Basic RAGの課題を明確にし、なぜAIエージェント、特にAgentic RAGが求められているのかを解説します。

従来のRAGが抱える課題

RAGは、外部ドキュメントから関連情報を取得し、LLMにコンテキストとして与えることで、ハルシネーションを抑制し、特定の知識に基づいた回答を生成する強力な手法です。しかし、そのシンプルさゆえに、以下のような課題があります。

  • 単一の知識ソースに限定されがち: ユーザーの質問が複数のドキュメントや外部APIの情報を横断的に必要とする場合、従来のRAGでは対応が困難です。
  • ワンショット検索の限界: 最初に取得したコンテキストが不十分だったり、質問の意図とずれていたりしても、その後の処理で改善する仕組みがありません。検索結果の質を検証するステップがないため、誤った情報に基づいて回答を生成するリスクがあります。
  • 動的な意思決定の欠如: ユーザーの質問の種類や、これまでの会話履歴、利用可能なツールなどに応じて、LLM自身が最適な行動を選択する能力がありません。例えば、「関連情報が見つからなかったらWeb検索に切り替える」といった柔軟な対応はできません。
  • 専門用語や複合クエリへの対応の難しさ: 専門性の高い質問や、複数の要素が絡み合う複雑な質問に対して、単一の検索クエリでは適切な情報を取得しにくいことがあります。

これらの課題は、LLMアプリケーションを実用的なレベルに引き上げる上で避けては通れない壁となります。

AIエージェント、特にAgentic RAGが課題を解決する

これらのRAGの限界を打破するために登場したのがAIエージェントです。AIエージェントは、LLMが自律的に「推論(Reason)」し「行動(Act)」を繰り返すReActパターンを基盤とし、ユーザーの指示や環境からのフィードバックに基づいて、データの解析、意思決定、タスクの自動実行などを行います。

特に、Agentic RAGは、従来のRAGに「意思決定」の要素を組み込んだものです。これにより、以下のような高度な処理が可能になります。

  • 動的な情報源選択: 質問の内容に応じて、社内データベース、Web検索(Tavilyなど)、特定のAPIなど、最適な情報源をLLM自身が選択します。
  • 検索結果の評価と改善: 取得した情報が質問に適切かどうかをLLMが評価し、不十分であればクエリをリライトして再検索したり、別の情報源を試したりします。
  • 多段階の推論とタスク分解: 複雑な質問を複数のサブタスクに分解し、それぞれのタスクに対して適切なツールや情報源を使い分け、最終的な回答を導き出します。
  • Human-in-the-Loop (HITL): 重要な判断や不確実性の高いステップで人間の介入を挟むことで、自動化の暴走を防ぎ、信頼性を向上させます。

このように、AIエージェントはRAGの能力を飛躍的に向上させ、より複雑で動的なビジネス要件に対応するLLMアプリケーション開発の鍵となります。

LangGraphとは?AIエージェントを構築する強力なフレームワーク

このセクションでは、LangGraphがどのようなフレームワークであり、なぜAIエージェント構築に適しているのかを解説します。LangChainとの関係性も明確にします。

LangGraphの基本概念

LangGraphは、LangChainチームが開発したフレームワークで、AIエージェントの思考や処理を**状態遷移(ステートマシン)**として制御するために特化しています。LangChainのコンポーネントをそのまま利用しつつ、ループや条件分岐を含む複雑なワークフローを構築できる点が最大の特長です。

LangGraphの主要な構成要素は以下の通りです。

  • State: グラフ全体で共有される状態を表します。PythonのTypedDictでスキーマを明示的に定義することで、コードの可読性が向上し、LangGraph Studioでの視覚化やデバッグが容易になります。
  • Node: 実際の処理を行う関数です。LLM呼び出し、ツール実行、データ処理、APIコールなど、任意のPython関数をノードとして利用できます。各ノードは現在のStateを受け取り、更新されたStateを返します。
  • Edge: ノード間の接続を定義します。特に強力なのが条件付きエッジで、エージェントの実行結果(Stateの内容)に基づいて次に実行されるノードを動的に変更できます。これにより、柔軟な意思決定フローを実現します。
  • Graph: ノードとエッジをまとめたものです。StateGraphクラスを使用して構築され、最終的にcompile()メソッドで実行可能なアプリケーションになります。

LangGraph 0.2.x以降のバージョンが推奨されており、Python 3.10以上が必要です。

LangChainとの関係性

LangGraphはLangChainエコシステムの一部であり、LangChainのコンポーネント(LLM、Tools、Chainsなど)をシームレスに利用できます。

  • LangChain: LLMアプリケーション開発のデファクトスタンダードライブラリであり、LLMとの連携、プロンプト管理、ツール利用、チェーン構築など、幅広い機能を提供します。
  • LangGraph: LangChainのコンポーネントを「ノード」として利用し、それらを「状態遷移グラフ」としてオーケストレーションするためのフレームワークです。より複雑な、状態を持つAIエージェントやマルチステップのワークフローを構築する際に真価を発揮します。

簡単に言えば、LangChainは「個々の部品」を提供し、LangGraphはそれらの部品を使って「複雑な機械(AIエージェント)」を組み立てるための「設計図と組み立てライン」を提供するイメージです。

LangGraphによるAIエージェントの実装例

ここからは、LangGraphを使ったシンプルなAIエージェントの具体的な実装例を通して、その内部動作を理解していきます。Agentic RAGの基本的な考え方を盛り込んだワークフローを構築します。

セットアップ

まず、必要なライブラリをインストールします。

pip install langgraph langchain langchain-openai

langchain-openaiはOpenAIのLLMを使用する場合に必要です。他のLLMを利用する場合は適宜変更してください。

基本的なAgentic RAGワークフローの構築

ここでは、ユーザーの質問が「複雑な質問」と判断された場合にLLMで回答を生成し、そうでなければすぐに処理を終了する、非常にシンプルなAgentic RAGの骨格を作成します。

from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import BaseMessage

# 環境変数を設定(OpenAI APIキー)
# import os
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"

# 状態の定義
class AgentState(TypedDict):
    """
    グラフ全体で共有される状態を定義します。
    TypedDictを使用することで、スキーマが明確になり、デバッグが容易になります。
    """
    question: str  # ユーザーからの質問
    answer: str    # LLMが生成した回答
    # 実際には、ここに検索結果、ツール実行結果、LLMからの思考プロセスなどを追加します
    # search_results: list[str] = []
    # tool_calls: list[dict] = []
    # intermediate_steps: list[BaseMessage] = [] # ReActエージェントで思考プロセスを記録

# LLMの初期化
# 本番環境ではモデル名を環境変数などで管理することを推奨
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# ノードの定義 1: LLMを呼び出して回答を生成する
def call_llm_for_answer(state: AgentState) -> AgentState:
    """
    LLMを呼び出して回答を生成するノード。
    LangChainのChatModelなどを利用してLLMを呼び出します。
    """
    print(f"\n--- ノード: LLMが回答を生成中 ---")
    print(f"質問: {state['question']}")
    
    # ここでLLMにプロンプトを渡し、回答を生成させる
    # 簡易的な例として、質問をそのまま回答に含める
    response = llm.invoke(f"以下の質問に答えてください。\n質問: {state['question']}")
    generated_answer = response.content
    
    print(f"LLM生成回答: {generated_answer[:50]}...") # 回答の冒頭を表示
    return {"answer": generated_answer}

# ノードの定義 2: 次のステップを決定する条件付きエッジの関数
def decide_next_step(state: AgentState) -> Literal["answer_node", "end_node"]:
    """
    ユーザーの質問に基づいて、次のステップを決定します。
    Agentic RAGでは、ここで検索の必要性、使用するツールなどを判断します。
    """
    print(f"\n--- ノード: 次のステップを決定中 ---")
    question = state["question"]
    
    # 質問の内容に基づいて条件分岐を行う(ここでは簡易的なキーワード判定)
    if "Agentic RAG" in question or "LangGraph" in question or "複雑" in question:
        print("複雑な質問と判断し、回答生成ノードへ進みます。")
        return "answer_node" 
    else:
        print("シンプルな質問と判断し、処理を終了します。")
        return "end_node" 

# グラフの構築
graph = StateGraph(AgentState)

# ノードの追加
graph.add_node("answer_node", call_llm_for_answer)

# エッジの追加
# STARTからdecide_next_step関数で条件分岐
# decide_next_stepの戻り値に応じて次のノードが決まる
graph.add_conditional_edges(
    START,                   # グラフの開始点
    decide_next_step,        # 次のノードを決定する関数
    {
        "answer_node": "answer_node", # decide_next_stepが"answer_node"を返したらanswer_nodeへ
        "end_node": END              # decide_next_stepが"end_node"を返したらグラフを終了
    }
)
# answer_nodeの処理が終わった後は、無条件にグラフを終了
graph.add_edge("answer_node", END) 

# グラフのコンパイル: 実行可能なLangGraphアプリケーションを作成
app = graph.compile()

# 実行例1: シンプルな質問
print("\n--- 実行例1: シンプルな質問 ---")
initial_state_simple = {"question": "こんにちは", "answer": ""}
result_simple = app.invoke(initial_state_simple)
print(f"最終結果: {result_simple}")

# 実行例2: 複雑な質問
print("\n--- 実行例2: 複雑な質問 ---")
initial_state_complex = {"question": "LangGraphのAgentic RAGにおける利点を複雑な言葉で教えてください。", "answer": ""}
result_complex = app.invoke(initial_state_complex)
print(f"最終結果: {result_complex}")

# 実行例3: LangGraphに関する質問
print("\n--- 実行例3: LangGraphに関する質問 ---")
initial_state_langgraph = {"question": "LangGraphとは何ですか?", "answer": ""}
result_langgraph = app.invoke(initial_state_langgraph)
print(f"最終結果: {result_langgraph}")

このコードでは、AgentStateというTypedDictで共有状態を定義し、call_llm_for_answerdecide_next_stepという2つのノード(関数)を定義しています。add_conditional_edgesを使うことで、decide_next_step関数の戻り値によって処理フローが動的に変化する様子がわかります。

Agentic RAGのワークフロー例とLangGraphでの表現

実際のAgentic RAGでは、より多くのノードと複雑な条件分岐が必要になります。以下に、一般的なAgentic RAGのワークフローと、それをLangGraphのノードとしてどのように表現できるかの例を示します。

  1. 質問分析ノード (analyze_question_node):
    • ユーザーの質問をLLMで分析し、その意図(例: 事実確認、比較、手順説明など)を把握します。
    • 必要に応じて、質問を複数のサブクエリに分解します。
    • Stateに分析結果(query_type, sub_queriesなど)を追加します。
  2. 情報源選択ノード (select_datasource_node):
    • 質問分析の結果に基づき、最適な情報源(ベクトルDB、Web検索、特定のAPIなど)をLLMに判断させます。
    • Stateに選択された情報源(selected_source)を記録し、条件付きエッジで次のノードに遷移します。
  3. 文書取得ノード (retrieve_documents_node):
    • 選択された情報源(例: Pinecone, QdrantなどのベクトルDB、TavilyなどのWeb検索ツール)から関連文書を取得します。
    • LangChainのRetrieverコンポーネントをここで利用します。
    • 取得した文書をStateに追加します(retrieved_docs)。
  4. 結果評価ノード (evaluate_results_node):
    • 取得した文書が質問に関連しているか、回答に必要な情報が十分かをLLMに評価させます。
    • 不十分と判断された場合、Stateneeds_re_query = Trueなどを設定し、クエリ改善ノードへ遷移する条件付きエッジを定義します。
  5. クエリ改善ノード (improve_query_node):
    • 検索結果が不十分だった場合、元の質問やこれまでの検索結果を基に、LLMで新しい検索クエリを生成します。
    • Statequestionを更新し、文書取得ノードに戻るループを形成します。
  6. 回答生成ノード (generate_answer_node):
    • 最終的に取得された文書と元の質問を基に、LLMが回答を生成します。
    • LangChainのcreate_retrieval_chainのようなチェーンも、このノード内部で利用できます。
    • 生成された回答をStateanswerに格納します。
  7. Human-in-the-loopノード (human_review_node):
    • 特に重要な質問や、回答の信頼性が求められる場合に、人間の確認・承認を挟むノードを設けます。
    • LangGraphのinterrupt()関数を活用し、ユーザーからの入力(承認、修正指示など)を待機します。

これらのステップをLangGraphのノードと条件付きエッジで表現することで、従来のRAGでは実現できなかった、柔軟で賢いAgentic RAGを構築できます。

LangGraphでAIエージェントを構築する際の注意点とベストプラクティス

LangGraphを使ったAIエージェント開発は強力ですが、特有の課題とそれに伴う設計上の考慮点が存在します。このセクションでは、よくあるハマりどころとその回避策、そして設計上のベストプラクティスを解説します。

よくあるエラー・ハマりどころと回避策

  1. ステートの肥大化とエラー伝播:
    • ハマりどころ: 複数のノードやエージェントが連鎖する中で、AgentStateに情報が際限なく蓄積され、デバッグが困難になります。また、初期段階の微小なハルシネーションや誤りが後続のノードで致命的なエラーに拡大する「ハルシネーションの伝播」が発生することがあります。
    • 回避策:
      • 3段以内の連鎖: AIエージェントの連鎖は、状態爆発と推論精度の劣化(Lost in the Middle)を防ぐため、3段以内に抑えることを推奨します。
      • エラーポイントの単一化: 例外(Exception)で処理を中断するのではなく、「Stateが真実」の原則に従い、エラー情報をStateに乗せて特定のノードで集約・ハンドリングします。
      • Human-in-the-Loop (HITL): 自動化の暴走を防ぐため、各段に停止・承認・再開のポイントを設けます。LangGraphのinterrupt()関数を活用し、人間の確認・修正・承認を受けてからエージェントの実行を再開できるようにします。
  2. LangGraphの状態のシリアライズ問題:
    • ハマりどころ: LangGraphの状態は、永続化や分散環境での利用時に内部でシリアライズ(JSON化)されることがあります。この際、カスタムオブジェクトや複雑なデータ構造を直接Stateに保持すると、シリアライズエラーやデータ破損が発生する可能性があります。
    • 回避策: 参照ではなくIDで持つ、またはシリアライズ可能なシンプルなデータ型(文字列、数値、リスト、辞書)を使用するようにします。TypedDictでスキーマを明示的に定義し、許容されるデータ型を限定することも有効です。カスタムオブジェクトをStateに含める場合は、それらが適切にシリアライズ・デシリアライズされるようにカスタムエンコーダ/デコーダを実装する必要があります。
  3. LangChainとLangGraphのバージョンの不整合:
    • ハマりどころ: LangChainは更新が非常に頻繁なライブラリであり、LangGraphもLangChainエコシステムの一部です。そのため、バージョン間の不整合によってコードが動作しなくなる、あるいは予期せぬ挙動を示すことがあります。特に、LangChainのAPI変更がLangGraphの内部実装に影響を与える場合があります。
    • 回避策:
      • 公式ドキュメントとMigrationガイドを定期的に確認し、最新の変更点を把握します。
      • pip show langgraphpip show langchainなどでインストール済みのバージョンを管理する習慣をつけます。
      • requirements.txtなどで使用するライブラリのバージョンを固定し、開発環境と本番環境で一貫性を保ちます。

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

設計上のトレードオフ

  • シンプルさと柔軟性:
    • シンプルなRAGは実装が容易で立ち上がりが速いですが、複雑なクエリや複数の情報源には対応しにくいです。
    • Agentic RAGは柔軟性が高く、複雑なワークフローに対応できますが、設計が複雑になり、デバッグも難しくなる傾向があります。ユースケースに応じて適切な複雑度を選択することが重要です。
  • LLMのコストと性能:
    • 高性能なLLM(例: GPT-4o, Claude 3.5 Sonnet)は推論能力が高く、複雑なタスク分解やFunction Callingに優れますが、API利用コストが高くなる傾向があります。
    • オープンソースLLM(例: Llama-3-ELYZA-JP-8B)はコストを抑えられますが、性能や日本語対応に課題がある場合があり、トレードオフを考慮する必要があります。
  • 自動化の度合いとHuman-in-the-Loop:
    • 完全自動化は効率的ですが、ハルシネーションや予期せぬ動作のリスクが伴います。
    • Human-in-the-Loopを導入することで信頼性は向上しますが、人間の介入が必要となるため、完全な自動化は達成できません。ビジネス要件とリスク許容度に応じてバランスを取ります。

ベストプラクティス

  • 段階的なアプローチ:
    • まずはシンプルなBasic RAGで成功体験を作り、そこから徐々にAgentic RAGへと拡張していくことを推奨します。最初の実装はシンプルに保ち、アプリケーション全体の見通しを立てやすくすることが重要です。
  • 状態(State)の明確な定義:
    • TypedDictを使用してStateのスキーマを明示的に定義することで、コードの可読性とデバッグのしやすさが飛躍的に向上します。すべてのノードが共通のStateを読み書きする仕組みにすることで、情報の流れが追いやすい設計になります。
  • モジュール分割とコンポーネント化:
    • LLMでSQL生成、SQLバリデーション、DWH問い合わせ、DataFrame格納など、各機能をワークフローまたはエージェントとしてコンポーネントに分割して設計・実装します。これにより、再利用性、保守性、テスト容易性が向上します。
  • エラーハンドリングとリトライ:
    • LLMからの出力が失敗した場合や外部ツール呼び出しが失敗した場合など、予期せぬ状況に備えたエラーハンドリングやリトライの仕組みを設計に組み込みます。LangGraphのグラフ構造内でエラーパスを定義することも可能です。
  • ReActパターンの採用:
    • AIエージェントの設計では、ReAct(Reason-Act)パターンが最も汎用性が高く、多くのユースケースに対応可能とされています。エージェントが「思考(Thought)→行動(Action)→観察(Observation)」のループを繰り返すことで目標に到達します。
  • LangSmithによる評価とデバッグ:
    • RAGアプリケーションやAIエージェントの評価には、LangSmithのようなトレーシング・モニタリングツールを積極的に活用します。これにより、実行フローの可視化、精度や信頼性の継続的な検証、デバッグを効率的に行うことができます。

まとめと次の一歩

この記事では、従来のRAGが持つ限界を乗り越え、より高度な問題に対応するためのAIエージェント、特にLangGraphを用いたAgentic RAGの構築方法について解説しました。

重要なポイントは以下の通りです。

  • RAGの限界: 単一知識ソース、ワンショット検索、動的な意思決定の欠如が課題でした。
  • AIエージェントの必要性: LLMが「推論」と「行動」を繰り返すことで、動的な情報源選択、検索結果の評価・改善、多段階の推論が可能になります。
  • LangGraphの役割: 状態遷移グラフとしてAIエージェントの複雑なワークフローを定義し、ノード、エッジ、ステートを通じて柔軟なロジックを実装できる強力なフレームワークです。
  • 実装のコツ: TypedDictによる状態の明確化、条件付きエッジによる動的なフロー制御が鍵となります。
  • 注意点とベストプラクティス: ステートの肥大化、シリアライズ問題、バージョン不整合に注意し、段階的アプローチやHuman-in-the-Loopの活用が推奨されます。

LangGraphとAgentic RAGは、LLMアプリケーション開発に新たな可能性をもたらします。ぜひ、この記事で得た知識を基に、ご自身のプロジェクトでAIエージェントの導入を検討してみてください。

次の一歩として、LangGraphの公式ドキュメントや、LangChainのAgent Conceptsをさらに深く読み込み、より複雑なツール利用やマルチエージェントシステムの構築に挑戦することをお勧めします。

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?