LangChain ツール呼び出しインターセプト -- @wrap_tool_call

@wrap_tool_call を使用すると、ツール実行レベルで @wrap_model_call と同様の制御機能(リトライ、キャッシュ、パラメータ書き換え、結果の後処理)を実装できます。


基本構造

@wrap_tool_call の構造は @wrap_model_call と似ており、request と handler の2つのパラメータを受け取ります:

実例

from langchain.agents.middleware import wrap_tool_call

@wrap_tool_call
def my_tool_wrapper(request, handler):
    # request.tool_call: ツール名とパラメータを含む
    # request.tool: ツールオブジェクト自体
    # request.state: Agent の現在の状態
    # request.runtime: ランタイムコンテキスト

    # handler(request) を呼び出して初めてツールが実際に実行される
    result = handler(request)

    # result は ToolMessage または Command
    return result

シナリオ1: ツール呼び出しのリトライ

ツールの実行は外部サービスの不安定さにより失敗する可能性があります。自動リトライにより信頼性を向上できます:

実例

from dotenv import load_dotenv
load_dotenv()

from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
from langchain.tools import tool


@wrap_tool_call
def retry_tool_on_error(request, handler):
    """ツール呼び出し失敗時に自動リトライ"""
    max_retries = 3
    last_result = None

    for attempt in range(max_retries):
        try:
            result = handler(request)
            # エラー結果かどうかを確認
            if hasattr(result, 'status') and result.status == "error":
                if attempt < max_retries - 1:
                    print(f" [リトライ] ツールがエラーを返しました。{attempt + 1} 回目のリトライ...")
                    continue
            if attempt > 0:
                print(f" [リトライ成功] {attempt + 1} 回目の試行")
            return result
        except Exception as e:
            if attempt < max_retries - 1:
                import time
                time.sleep((attempt + 1) * 2)
                print(f" [リトライ] 例外 {e}、{attempt + 1} 回目のリトライ...")
            else:
                raise

    return last_result


# 失敗する可能性のあるツールをシミュレート
call_count = 0

@tool
def fetch_course_data(course_id: str) -> str:
    """サンプルからコースデータを取得します。

    Args:
course_id: コースID
    """

    global call_count
    call_count += 1
    # 最初の2回は失敗、3回目は成功をシミュレート
    if call_count < 3:
        raise Exception(f"ネットワークエラー:コースサービスに接続できません({call_count} 回目の試行)")
    return f"コース {course_id}: Python3 基礎チュートリアル、30章、無料"


model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
    model=model,
    tools=[fetch_course_data],
    middleware=[retry_tool_on_error],
    system_prompt="あなたは サンプルのコースアシスタントです。",
)

result = agent.invoke({
    "messages": [HumanMessage(content="コース python-001 の情報を調べてください")]
})
print(f"\n最終応答: {result['messages'][-1].content}")

実行結果:

  [重试] 异常 网络错误:无法连接到课程服务(第 1 次尝试),1 次重试...
  [重试] 异常 网络错误:无法连接到课程服务(第 2 次尝试),2 次重试...
  [重试成功] 第 3 次尝试

最終返答: コースpython-001はPython3基礎チュートリアルで、全30章、無料で提供されています。

シナリオ2: ツールパラメータの変更

ツール実行前に動的にパラメータを変更することで、ツールコードを変更せずにパラメータ変換を実現できます:

実例

from langchain.agents.middleware import wrap_tool_call


@wrap_tool_call
def normalize_city_name(request, handler):
    """都市名を自動的に正規化します(全角を半角に変換、余分な空白を削除など)"""
    tool_call = request.tool_call

    # city パラメータを含むツール呼び出しのみ処理
    if "city" in tool_call.get("args", {}):
        city = tool_call["args"]["city"]
        # 都市名を正規化:スペースを削除、大文字小文字を統一
        normalized = city.strip().replace(" ", "")  # 全角スペースを削除

        # パラメータを置換
        new_args = {**tool_call["args"], "city": normalized}
        new_tool_call = {**tool_call, "args": new_args}
        request = request.override(tool_call=new_tool_call)

    return handler(request)

シナリオ3: ツール結果のキャッシュ

同じツール呼び出し(同じツール + 同じパラメータ)の場合、結果をキャッシュできます:

実例

from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage

tool_cache = {}


@wrap_tool_call
def cache_tool_results(request, handler):
    """ツール実行結果をキャッシュします"""
    # キャッシュキーを生成:ツール名 + パラメータ
    tool_name = request.tool_call.get("name", "unknown")
    tool_args = str(request.tool_call.get("args", {}))

    cache_key = f"{tool_name}:{tool_args}"

    # キャッシュを確認
    if cache_key in tool_cache:
        print(f"[ツールキャッシュヒット] {tool_name}")
        cached_content = tool_cache[cache_key]
        return ToolMessage(
            content=cached_content,
            tool_call_id=request.tool_call.get("id", ""),
            name=tool_name,
        )

    # ツールを実行
    result = handler(request)

    # キャッシュに保存
    if hasattr(result, 'content'):
        tool_cache[cache_key] = result.content
        print(f[ツールキャッシュ書き込み] {tool_name}、現在 {len(tool_cache)} 件)

    return result

シナリオ4: ツール呼び出しのログとモニタリング

すべてのツール呼び出しの詳細情報を記録します:

実例

import time
from langchain.agents.middleware import wrap_tool_call


@wrap_tool_call
def monitor_tool_performance(request, handler):
    """ツール呼び出しのパフォーマンス指標を監視します"""
    tool_name = request.tool_call.get("name", "unknown")
    tool_args = request.tool_call.get("args", {})

    # 開始時間を記録
    start_time = time.time()

    try:
        result = handler(request)
        elapsed = time.time() - start_time

        # 成功した呼び出しを記録
        print(f"[モニタリング] {tool_name}({tool_args}) 成功、所要時間 {elapsed:.2f}s")
        return result
    except Exception as e:
        elapsed = time.time() - start_time
        # 失敗した呼び出しを記録
        print(f"[モニタリング] {tool_name}({tool_args}) 失敗、所要時間 {elapsed:.2f}s、エラー: {e}")
        raise

シナリオ5: 結果に基づいて後続フローを決定

ツールの実行結果に基づいて、Agent ループを継続するかどうかを決定できます:

実例

from langchain.agents.middleware import wrap_tool_call
from langgraph.types import Command


@wrap_tool_call
def check_empty_result(request, handler):
    """ツールが空の結果を返した場合、Agent を直接終了し、モデル呼び出しを無駄にしません"""
    result = handler(request)

    # 空の結果が返されたかどうかを確認
    if hasattr(result, 'content') and (
        "見つかりません" in str(result.content)
        or "結果なし" in str(result.content)
        or "ない" in str(result.content)
    ):
        # 直接 Command を返して状態を更新
        # 状況を説明する AI メッセージを追加
        from langchain.messages import AIMessage
        return Command(update={
            "messages": [
                AIMessage(content="申し訳ありませんが、関連情報が見つかりませんでした。別のキーワードをお試しください。")
            ]
        })

    return result

wrap_tool_call が Command を返した場合、update パラメータを使用して Agent の状態を変更できます。Command を使用すると、メッセージリストに AI メッセージを直接追加でき、その後 Agent ループは自然に終了します。


@wrap_model_call vs @wrap_tool_call

次元@wrap_model_call@wrap_tool_call
インターセプト対象モデル呼び出しツール実行
request の内容model、messages、tools、system_prompttool_call、tool、state、runtime
戻り値の型ModelResponse または AIMessageToolMessage または Command
適用シナリオモデルのリトライ、フォールバック、キャッシュ、プロンプト変更ツールのリトライ、キャッシュ、パラメータ書き換え、結果処理
その他の拡張