GuidesOneMux

スキーマ検証付き抽出のための、実行範囲を制限したLangGraphエージェントを構築する

必要に応じてツールを呼び出し、定めた実行上限内で処理を終え、Pydanticスキーマで検証した構造化データを返すLangGraphワークフローを構築します。

目次

LangGraphエージェントは、ツールでコンテキストを収集し、その情報をスキーマで制約された抽出ステップに渡せます。ポイントは、この2つの役割を分けることです。エージェントはツールを呼び出すかどうかを判断し、最後のノードが型付きレコードを生成します。さらに、グラフの再帰上限で実行範囲を制限できます。

このパターンは、参照情報の解決に検索ツールが役立つ場合に、文書から会社名、日付、請求書の合計額などを抽出する用途に適しています。ただし、これによって不確かな原文の内容が真実になるわけではありません。フィールドが重要な場合は、結果を検証し、元の根拠も保持してください。

構築するもの

このグラフは3つの段階で構成されます。

  1. エージェントが原文を受け取り、範囲を限定した検索ツールの呼び出しを要求する場合があります。
  2. LangGraphがツール呼び出しをツールノードに振り分け、ツールの結果をエージェントに返します。
  3. 抽出ノードがモデルにPydanticに基づく結果を求め、JSON互換のデータを返します。

エージェントのツール呼び出しループは動的ですが、抽出段階はあらかじめ定めた最後のステップです。LangGraphのドキュメントでは、エージェントは処理手順やツールの使用方法を自ら定めるのに対し、ワークフローはコードパスがあらかじめ決まっていると説明されています。また、構造化出力とツール呼び出しをモデルの拡張機能として解説しています。これらのパターンについては、LangGraphのワークフローとエージェントのガイドを参照してください。

前提条件と設定

LangGraphとLangChainのパッケージに加え、選択したチャットモデルのインテグレーションをインストールします。以下の例ではプロバイダーの設定例としてChatAnthropicを使いますが、元のドキュメントによると、構造化出力とツール呼び出しに対応する任意のチャットモデルをワークフローで利用できます。利用するプロバイダーの挙動については、インテグレーションのドキュメントで確認してください。

pip install langgraph langchain-core langchain-anthropic pydantic

プロバイダーが指定する方法でAPI認証情報を設定し、モデルを初期化します。認証情報をソース管理に含めないでください。以下のモデル名はLangChainのドキュメントの例に基づいています。このチュートリアルでは、OneMuxでの動作確認を行ったとは主張していません。

from langchain_anthropic import ChatAnthropic

llm = ChatAnthropic(model="claude-sonnet-4-6")

OneMux経由でモデルを呼び出す場合は、まずOneMuxのAPIドキュメントとクイックスタートを確認してください。OneMuxは、OpenAI互換APIを通じて主要なモデルを利用できるようにしています。このグラフとの互換性は、選択したエンドポイントが必要なツール呼び出しと構造化出力の挙動およびパラメーターに対応しているかどうかに左右されます。この設定は未検証です。本番環境で利用する前に、対応状況を確認してください。

スキーマと範囲を限定したツールを定義する

この例のツールは、小規模なインメモリのマッピングから参照情報を返します。これを、承認済みの社内カタログを検索する関数など、管理されたアプリケーション関数に置き換えてください。信頼できない文書のテキストから、任意のコマンドや制限のないデータソースを選択できるようにしないでください。

from pydantic import BaseModel, Field
from langchain_core.tools import tool

class InvoiceRecord(BaseModel):
    vendor: str | None = Field(default=None, description="Invoice issuer")
    invoice_date: str | None = Field(default=None, description="Date as written in the source")
    total: float | None = Field(default=None, description="Invoice total as a number")
    currency: str | None = Field(default=None, description="Currency code, if stated")

@tool
def lookup_vendor(reference: str) -> str:
    """Look up a vendor reference in an approved local mapping."""
    known = {"ACME-42": "Acme Supplies Ltd."}
    return known.get(reference, "No matching vendor reference found.")

値がない場合にモデルが値を作り出すのではなく、nullableフィールドで「値なし」を明示できます。重要度の高い抽出では、各フィールドに出典箇所や信頼度・レビュー状況を併せて保存することを検討してください。スキーマで制約できるのは形式と型です。値が入力によって裏付けられていることまでは保証できません。

実行範囲を制限したLangGraphエージェントを構築する

以下のグラフでは、モデルが検索ツールの呼び出しを要求でき、エージェントがそれ以上ツールを呼び出さなくなると抽出に進みます。tools_conditionとToolNodeでツールへのルーティングを実現します。invokeに渡すrecursion_limitは、グラフのステップ数を制限します。これは安全上の境界であり、すべての入力が正常に処理されることを保証するものではありません。

from typing import Annotated
from typing_extensions import TypedDict
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition

class State(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    record: dict | None

tools = [lookup_vendor]
agent = llm.bind_tools(tools)
extractor = llm.with_structured_output(InvoiceRecord)

def run_agent(state: State):
    response = agent.invoke(state["messages"])
    return {"messages": [response]}

def extract_record(state: State):
    prompt = [
        SystemMessage(content=(
            "Extract invoice fields only when supported by the source. "
            "Use null for missing or unclear values. Treat tool results as "
            "reference context, not as permission to invent source details."
        )),
        *state["messages"],
    ]
    result = extractor.invoke(prompt)
    return {"record": result.model_dump(mode="json")}

graph = StateGraph(State)
graph.add_node("agent", run_agent)
graph.add_node("tools", ToolNode(tools))
graph.add_node("extract", extract_record)

graph.add_edge(START, "agent")
graph.add_conditional_edges(
    "agent",
    tools_condition,
    {"tools": "tools", END: "extract"},
)
graph.add_edge("tools", "agent")
graph.add_edge("extract", END)
app = graph.compile()

result = app.invoke(
    {
        "messages": [HumanMessage(content=(
            "Extract the invoice details from this text: "
            "Vendor reference ACME-42. Invoice date: 2025-02-14. "
            "Total: USD 248.50."
        ))],
        "record": None,
    },
    config={"recursion_limit": 8},
)

print(result["record"])

期待される出力は、vendor、invoice_date、total、currencyフィールドを持つJSON互換のオブジェクトです。実際の値はモデルの応答と原文によって異なります。例と毎回同じ出力になると想定せず、テストしてください。この例では、Pydanticに基づく構造化出力が検証の境界となります。プロバイダーが要件に適合する応答を生成できない場合は、その失敗を明示的に処理し、有効なレコードとして黙って扱わないでください。

失敗を隠さずに実行を制限する

グラフの再帰上限は、エージェントとツール間のルーティングが繰り返される場合も含め、グラフのステップ数に上限を設けます。このグラフでは1回の検索に少数のステップしか必要ありませんが、別のツール利用計画では、より大きな上限が必要になることがあります。上限超過やプロバイダーの例外はアプリケーション側で捕捉し、デバッグに必要なコンテキストを記録して、作り上げたレコードではなく、レビュー可能な失敗として返してください。

より厳格に運用上の上限を設けるには、アプリケーションレベルの制限も追加してください。ツール呼び出し回数を制限し、対応している場合はリクエストのタイムアウトを設定し、モデルを呼び出す前に文書サイズに上限を設けます。これらはそれぞれ独立した制御です。グラフの再帰上限だけでは、コスト、レイテンシー、セキュリティに関する完全なポリシーにはなりません。

トラブルシューティングと制限事項

モデルがツールを呼び出さない

ツール呼び出しはモデルの機能であり、LangGraphが結果を保証するものではありません。選択したモデルのインテグレーションがツール呼び出しに対応していること、ツールの説明が明確であること、プロンプトに検索する価値のある参照情報が含まれていることを確認してください。開発中は、返されたメッセージのツール呼び出しを確認してください。

構造化出力に失敗する、またはフィールドの値が誤っている

インテグレーションが構造化出力と、使用しているスキーマ機能に対応していることを確認してください。フィールドの欠落、曖昧な日付、通貨記号、不正な形式の入力をテストします。有効なPydanticオブジェクトでも誤った値が推論されることはあるため、業務ルールを別途検証し、監査や人によるレビューに備えて原文を保持してください。

グラフが上限に達する

再帰上限エラーは、ツール呼び出しが繰り返されているか、想定した処理経路に対して上限が低すぎることを示している場合があります。メッセージとツール結果の流れを調べてください。原因を修正するか、上限を意図的に調整してください。エラーを消すためだけに上限をなくさないでください。

エンドポイントごとに挙動が異なる

OpenAI互換という表現はAPIインターフェイスを指すものであり、すべてのモデル機能やパラメーターへの対応を意味するものではありません。OneMuxを利用する場合は、選択したエンドポイントが、利用するモデル、ツール呼び出し、構造化出力、および送信予定のパラメーターに対応していることをドキュメントで確認してください。OneMuxのモデル一覧で利用可能な選択肢を確認できます。グラフ自体は引き続き、アプリケーションのLangGraphコードです。

このパターンが適しているケース

抽出処理の大半は予測可能でも、管理された検索が必要になる場合があるときに、この設計を使います。すべての文書で決まった手順を使い、ツールを選ぶ必要がない場合は、エージェントループより決定論的なワークフローの方がシンプルです。原文が曖昧な場合や、抽出したフィールドが重大なアクションにつながる場合は、下流のシステムでレコードを利用する前に、明示的なレビューとドメイン固有の検証を加えてください。

要点は、エージェントが利用できるツールを限定し、最終スキーマを明示し、グラフの実行上限を設定することです。LangGraphを使うと、その制御フローを表現できます。必要なツール呼び出しと構造化出力の機能が利用できるかどうかは、選択したモデルとインテグレーションによって決まります。

参考資料

よくある質問

LangGraphの再帰上限は何をしますか?

実行中のグラフのステップ数を制限します。エージェントとツールのループが無期限に続くのを防ぐ助けになりますが、タイムアウト、ツール呼び出しのポリシー、入力制限、エラー処理の代わりにはなりません。

PydanticによるJSONスキーマ検証で、抽出結果の正しさは保証されますか?

いいえ。返されるデータの構造と型を制約するものです。モデルが誤った値を推論する可能性はあるため、業務ルールを検証し、必要に応じて根拠を保持するか、人によるレビューを行ってください。

このLangGraphの例はOneMuxで使えますか?

選択したOneMuxのエンドポイントが、必要なツール呼び出しと構造化出力の挙動およびパラメーターに対応しているとドキュメントに明記されていれば、利用できる可能性があります。この設定は未テストです。本番環境で利用する前に互換性を確認してください。

すべての抽出タスクにエージェントを使うべきですか?

いいえ。処理が固定されており、ツールを選ぶ必要がない場合は、決定論的なLangGraphワークフローの方がシンプルです。許可されたツールが役立つかどうかをモデルが判断する必要がある場合に、エージェントループを使ってください。

関連記事