LangChain ミドルウェア(Middleware)
LangChain Middleware(ミドルウェア)は LangChain の最も強力な機能です。Agent 実行の各段階にカスタムロジックを挿入でき、リトライ、フォールバック、キャッシュ、コンテンツフィルタリング、ログ記録などの機能を実現できます。Agent 自体のコードを変更する必要はありません。
Middleware とは
Middleware は Agent の実行フローにおけるフック(Hook)です。各フックを使用すると、特定の時点でカスタムコードを実行できます:
例
# Middleware の直感的な理解:
# Agent の実行フローが次のようになっていると仮定します:
# 1. ユーザー入力 → 2. モデル思考 → 3. 場合によりツール呼び出し → 4. モデル再思考 → 5. 結果出力
# Middleware を使用すると、この5つの段階の間にカスタムロジックを挿入できます:
# 1. ユーザー入力
# ↓ [before_agent フック:ログ記録、権限チェック]
# 2. モデル思考
# ↓ [before_model フック:メッセージ前処理]
# ↓ [wrap_model_call フック:リトライ、フォールバック、キャッシュ]
# ↓ [after_model フック:コンテンツ監査]
# 3. ツール実行
# ↓ [wrap_tool_call フック:ツール呼び出しリトライ]
# 4. モデル思考に戻る(完了までループ)
# ↓ [after_agent フック:結果のフォーマット、統計分析]
# 5. 結果出力
# Agent の実行フローが次のようになっていると仮定します:
# 1. ユーザー入力 → 2. モデル思考 → 3. 場合によりツール呼び出し → 4. モデル再思考 → 5. 結果出力
# Middleware を使用すると、この5つの段階の間にカスタムロジックを挿入できます:
# 1. ユーザー入力
# ↓ [before_agent フック:ログ記録、権限チェック]
# 2. モデル思考
# ↓ [before_model フック:メッセージ前処理]
# ↓ [wrap_model_call フック:リトライ、フォールバック、キャッシュ]
# ↓ [after_model フック:コンテンツ監査]
# 3. ツール実行
# ↓ [wrap_tool_call フック:ツール呼び出しリトライ]
# 4. モデル思考に戻る(完了までループ)
# ↓ [after_agent フック:結果のフォーマット、統計分析]
# 5. 結果出力
6つのフックポイント
LangChain の Middleware は6つのフックを提供し、実行タイミングによって2つのカテゴリに分類されます:
| フック | 実行頻度 | 実行位置 | 主な用途 |
|---|---|---|---|
| before_agent | 1回 | Agent 開始前 | 初期化、権限チェック、入力前処理 |
| before_model | 各ループ | モデル呼び出し前 | メッセージ前処理、動的コンテキスト注入 |
| wrap_model_call | 各ループ | モデル呼び出しのラップ | リトライ、フォールバック、キャッシュ、リクエスト書き換え |
| after_model | 各ループ | モデル呼び出し後 | コンテンツ監査、レスポンスフィルタリング、ログ |
| wrap_tool_call | ツール呼び出しごと | ツール実行のラップ | ツールリトライ、結果キャッシュ、パラメータ書き換え |
| after_agent | 1回 | Agent 終了後 | 出力のフォーマット、統計、リソースのクリーンアップ |
2つの使用方法
Middleware はクラス継承またはデコレータの2つの方法で使用できます:
方法1:デコレータ(推奨)
例
from langchain.agents.middleware import before_model, after_model
# デコレータ方式:シンプルで直感的
@before_model
def log_before(state, runtime):
"""モデル呼び出しのたびにログを記録"""
msg_count = len(state.get("messages", []))
print(f"[before_model] 現在のメッセージ数: {msg_count}")
return None
@after_model
def log_after(state, runtime):
"""モデル呼び出しのたびにログを記録"""
last_msg = state["messages"][-1] if state.get("messages") else None
if last_msg and hasattr(last_msg, 'tool_calls') and last_msg.tool_calls:
print(f"[after_model] モデルがツール呼び出しを要求しました")
return None
# デコレータ方式:シンプルで直感的
@before_model
def log_before(state, runtime):
"""モデル呼び出しのたびにログを記録"""
msg_count = len(state.get("messages", []))
print(f"[before_model] 現在のメッセージ数: {msg_count}")
return None
@after_model
def log_after(state, runtime):
"""モデル呼び出しのたびにログを記録"""
last_msg = state["messages"][-1] if state.get("messages") else None
if last_msg and hasattr(last_msg, 'tool_calls') and last_msg.tool_calls:
print(f"[after_model] モデルがツール呼び出しを要求しました")
return None
方法2:クラス継承(複雑なロジックに適している)
例
from langchain.agents.middleware import AgentMiddleware
class LoggingMiddleware(AgentMiddleware):
"""カスタムログミドルウェア"""
@property
def name(self) -> str:
# カスタムミドルウェア名(デフォルトはクラス名)
return "logging"
def before_agent(self, state, runtime):
"""Agent 開始前のロジック"""
print("[Logging] Agent の実行を開始")
return None
def before_model(self, state, runtime):
"""モデル呼び出し前のロジック"""
msg_count = len(state.get("messages", []))
print(f“[Logging] モデルを呼び出す準備中、現在 {msg_count} 件のメッセージ”)
return None
def after_model(self, state, runtime):
"""モデル呼び出し後のロジック"""
print("[Logging] モデル呼び出し完了")
return None
def after_agent(self, state, runtime):
"""Agent 終了後のロジック"""
print([Logging] Agent の実行が終了しました)
return None
class LoggingMiddleware(AgentMiddleware):
"""カスタムログミドルウェア"""
@property
def name(self) -> str:
# カスタムミドルウェア名(デフォルトはクラス名)
return "logging"
def before_agent(self, state, runtime):
"""Agent 開始前のロジック"""
print("[Logging] Agent の実行を開始")
return None
def before_model(self, state, runtime):
"""モデル呼び出し前のロジック"""
msg_count = len(state.get("messages", []))
print(f“[Logging] モデルを呼び出す準備中、現在 {msg_count} 件のメッセージ”)
return None
def after_model(self, state, runtime):
"""モデル呼び出し後のロジック"""
print("[Logging] モデル呼び出し完了")
return None
def after_agent(self, state, runtime):
"""Agent 終了後のロジック"""
print([Logging] Agent の実行が終了しました)
return None
完全なライフサイクル例
例
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain.agents.middleware import (
before_agent, after_agent,
before_model, after_model,
)
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
from langchain.tools import tool
@before_agent
def start_log(state, runtime):
"""Agent 開始前"""
print(">>> [before_agent] Agent 開始 <<<")
runtime.stream_writer({"type": "lifecycle", "phase": "start"})
return None
@before_model
def pre_model(state, runtime):
"""モデル呼び出しのたびに"""
msg_count = len(state.get("messages", []))
print(f" -> [before_model] {msg_count} 件目のメッセージ")
return None
@after_model
def post_model(state, runtime):
"""モデル呼び出しのたびに"""
last = state["messages"][-1] if state.get("messages") else None
if hasattr(last, 'tool_calls') and last.tool_calls:
tools = [tc['name'] for tc in last.tool_calls]
print(f" <- [after_model] ツール要求: {tools}")
else:
content = str(last.content)[:50] if last and hasattr(last, 'content') else ""
print(f" <- [after_model] 直接返信: {content}...")
return None
@after_agent
def end_log(state, runtime):
"""Agent 終了後"""
total_msgs = len(state.get("messages", []))
print(f<<< [after_agent] Agent 終了、合計 {total_msgs} 件のメッセージ <<<)
return None
@tool
def get_weather(city: str) -> str:
"""天気を検索"""
return f"{city}: 晴れ、25°C"
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
tools=[get_weather],
middleware=[start_log, pre_model, post_model, end_log],
system_prompt="あなたはアシスタントです。",
)
print("\n========== 1つ目の質問(ツールが必要) ==========")
result = agent.invoke({
"messages": [HumanMessage(content="杭州の天気は?")]
})
print(f"\n最終返信: {result['messages'][-1].content}")
print("\n========== 2つ目の質問(ツール不要) ==========")
result = agent.invoke({
"messages": [HumanMessage(content="こんにちは")]
})
print(f"\n最終返信: {result['messages'][-1].content}")
load_dotenv()
from langchain.agents import create_agent
from langchain.agents.middleware import (
before_agent, after_agent,
before_model, after_model,
)
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
from langchain.tools import tool
@before_agent
def start_log(state, runtime):
"""Agent 開始前"""
print(">>> [before_agent] Agent 開始 <<<")
runtime.stream_writer({"type": "lifecycle", "phase": "start"})
return None
@before_model
def pre_model(state, runtime):
"""モデル呼び出しのたびに"""
msg_count = len(state.get("messages", []))
print(f" -> [before_model] {msg_count} 件目のメッセージ")
return None
@after_model
def post_model(state, runtime):
"""モデル呼び出しのたびに"""
last = state["messages"][-1] if state.get("messages") else None
if hasattr(last, 'tool_calls') and last.tool_calls:
tools = [tc['name'] for tc in last.tool_calls]
print(f" <- [after_model] ツール要求: {tools}")
else:
content = str(last.content)[:50] if last and hasattr(last, 'content') else ""
print(f" <- [after_model] 直接返信: {content}...")
return None
@after_agent
def end_log(state, runtime):
"""Agent 終了後"""
total_msgs = len(state.get("messages", []))
print(f<<< [after_agent] Agent 終了、合計 {total_msgs} 件のメッセージ <<<)
return None
@tool
def get_weather(city: str) -> str:
"""天気を検索"""
return f"{city}: 晴れ、25°C"
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
tools=[get_weather],
middleware=[start_log, pre_model, post_model, end_log],
system_prompt="あなたはアシスタントです。",
)
print("\n========== 1つ目の質問(ツールが必要) ==========")
result = agent.invoke({
"messages": [HumanMessage(content="杭州の天気は?")]
})
print(f"\n最終返信: {result['messages'][-1].content}")
print("\n========== 2つ目の質問(ツール不要) ==========")
result = agent.invoke({
"messages": [HumanMessage(content="こんにちは")]
})
print(f"\n最終返信: {result['messages'][-1].content}")
実行結果:
========== 第一个问题(需要工具) ========== >>> [before_agent] Agent 开始 <<< -> [before_model] 第 2 条消息 <- [after_model] 请求工具: ['get_weather'] -> [before_model] 第 3 条消息 <- [after_model] 直接回复: 杭州今天晴,气温25°C。... <<< [after_agent] Agent 结束,共 4 条消息 <<< 最終返答: 杭州今天晴,气温25°C。 ========== 第二个问题(无需工具) ========== >>> [before_agent] Agent 开始 <<< -> [before_model] 第 1 条消息 <- [after_model] 直接回复: 你好!有什么可以帮你的?... <<< [after_agent] Agent 结束,共 2 条消息 <<< 最終返答: こんにちは!何かお手伝いできることはありますか?
出力からわかること:
- before_agent と after_agent:各質問で1回だけ実行される
- before_model と after_model:モデル呼び出しのたびに実行される(1つ目の質問ではモデルが2回呼び出されたため、それぞれ2回実行される)
Middleware の戻り値
Middleware の戻り値は、Agent の状態を変更するかどうか、またはフローを制御するかを決定します:
| 戻り値 | 効果 | 例 |
|---|---|---|
| None | 状態を変更せず、通常のフローを続行 | 純粋なログ記録 |
| dict | Agent の状態を更新(現在の状態にマージ) | {"custom_field": "value"} を返す |
| jump_to を含む dict | 指定されたノードにジャンプ | {"jump_to": "end"} を返す |
その他の拡張返された dict は Agent 状態の reducer を介してマージされます。messages フィールドには add_messages reducer が使用されるため、返された messages は上書きではなく追加されます。