LangChain ミドルウェアフック -- @before_model と @after_model
before_model と after_model は最もよく使われる2つの ミドルウェア(Middleware) フックです。これらは毎回のモデル呼び出しの前後に実行され、コンテンツフィルタリング、メッセージ前処理、レスポンス審査などに適しています。
@before_model——モデル呼び出し前のインターセプト
@before_model は毎回モデルを呼び出す前に実行されます。ここでメッセージを変更したり、コンテキスト条件を注入したり、モデル呼び出しを直接スキップしたりできます。
シナリオ 1:メッセージ前処理——会話の長さを制限
例
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain.agents.middleware import before_model
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
@before_model
def limit_context(state, runtime):
"""メッセージ履歴の長さを制限し、コンテキストが長すぎるのを防ぐ"""
messages = state.get("messages", [])
# system message + 最新の6件のメッセージを保持
MAX_MESSAGES = 6
if len(messages) > MAX_MESSAGES:
# 最新のメッセージに切り詰める
trimmed = messages[-MAX_MESSAGES:]
# 最初のメッセージがユーザーメッセージであることを確認
if trimmed and trimmed[0].type != "human":
trimmed = trimmed[1:]
return {"messages": trimmed}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[limit_context],
system_prompt="あなたはサンプル のアシスタントです。",
)
# 複数ターンの会話をシミュレーション
result = agent.invoke({
"messages": [
HumanMessage(content="1ターン目"),
HumanMessage(content="2ターン目"),
HumanMessage(content="3ターン目"),
HumanMessage(content="4ターン目"),
HumanMessage(content="5ターン目"),
HumanMessage(content="6ターン目"),
HumanMessage(content="7ターン目"),
]
})
print(f"メッセージ数: {len(result['messages'])}")
print(f"返信: {result['messages'][-1].content}")
load_dotenv()
from langchain.agents import create_agent
from langchain.agents.middleware import before_model
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
@before_model
def limit_context(state, runtime):
"""メッセージ履歴の長さを制限し、コンテキストが長すぎるのを防ぐ"""
messages = state.get("messages", [])
# system message + 最新の6件のメッセージを保持
MAX_MESSAGES = 6
if len(messages) > MAX_MESSAGES:
# 最新のメッセージに切り詰める
trimmed = messages[-MAX_MESSAGES:]
# 最初のメッセージがユーザーメッセージであることを確認
if trimmed and trimmed[0].type != "human":
trimmed = trimmed[1:]
return {"messages": trimmed}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[limit_context],
system_prompt="あなたはサンプル のアシスタントです。",
)
# 複数ターンの会話をシミュレーション
result = agent.invoke({
"messages": [
HumanMessage(content="1ターン目"),
HumanMessage(content="2ターン目"),
HumanMessage(content="3ターン目"),
HumanMessage(content="4ターン目"),
HumanMessage(content="5ターン目"),
HumanMessage(content="6ターン目"),
HumanMessage(content="7ターン目"),
]
})
print(f"メッセージ数: {len(result['messages'])}")
print(f"返信: {result['messages'][-1].content}")
実行結果:
メッセージ数: 8 返答: こんにちは!複数ターンの会話テストを行っているようですね。何かお手伝いできることはありますか?
シナリオ 2:コンテンツフィルタリング——NGワードをブロック
例
from langchain.agents.middleware import before_model
SENSITIVE_WORDS = ["パスワード", "銀行カード番号", "身分証番号"]
@before_model
def content_filter(state, runtime):
"""ユーザーメッセージにNGワードが含まれているか確認し、含まれていればインターセプトする"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
content = str(last_msg.content) if hasattr(last_msg, 'content') else ""
for word in SENSITIVE_WORDS:
if word in content:
print(f"[インターセプト] NGワードを検出: {word}")
# jump_to="end" で直接終了し、モデルに返信させない
return {
"jump_to": "end",
"messages": [
HumanMessage(content=f"申し訳ありませんが、安全のため、「{word}」を含むリクエストは処理できません。")
]
}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[content_filter],
system_prompt="あなたはサンプル のアシスタントです。",
)
# 正常な質問
result = agent.invoke({
"messages": [HumanMessage(content="Python はどうやって始めればいいですか?")]
})
print(f"正常な質問: {result['messages'][-1].content[:80]}")
# NGワードを含む質問
result = agent.invoke({
"messages": [HumanMessage(content="私の銀行カード番号は何ですか?調べてもらえますか?")]
})
print(f"\n"NGワードを含む質問: {result['messages'][-1].content}")
SENSITIVE_WORDS = ["パスワード", "銀行カード番号", "身分証番号"]
@before_model
def content_filter(state, runtime):
"""ユーザーメッセージにNGワードが含まれているか確認し、含まれていればインターセプトする"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
content = str(last_msg.content) if hasattr(last_msg, 'content') else ""
for word in SENSITIVE_WORDS:
if word in content:
print(f"[インターセプト] NGワードを検出: {word}")
# jump_to="end" で直接終了し、モデルに返信させない
return {
"jump_to": "end",
"messages": [
HumanMessage(content=f"申し訳ありませんが、安全のため、「{word}」を含むリクエストは処理できません。")
]
}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[content_filter],
system_prompt="あなたはサンプル のアシスタントです。",
)
# 正常な質問
result = agent.invoke({
"messages": [HumanMessage(content="Python はどうやって始めればいいですか?")]
})
print(f"正常な質問: {result['messages'][-1].content[:80]}")
# NGワードを含む質問
result = agent.invoke({
"messages": [HumanMessage(content="私の銀行カード番号は何ですか?調べてもらえますか?")]
})
print(f"\n"NGワードを含む質問: {result['messages'][-1].content}")
実行結果:
[拦截] 检测到敏感词: 银行卡号 通常の質問: Python 入门可以从安装 Python 环境开始... 機密の質問: 抱歉,为了安全,不能处理包含「银行卡号」的请求。
@after_model——モデル呼び出し後の処理
@after_model はモデルの返信後に実行され、モデル出力の審査、重要な情報の抽出、後続の指示の追加などに適しています。
シナリオ 3:レスポンス内容の審査
例
from langchain.agents.middleware import after_model
FORBIDDEN_TOPICS = ["政治", "暴力", "ポルノ"]
@after_model
def response_audit(state, runtime):
"""モデルの返信を審査し、禁止トピックに関連する場合は置き換える"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
content = str(last_msg.content) if hasattr(last_msg, 'content') else ""
for topic in FORBIDDEN_TOPICS:
if topic in content:
runtime.stream_writer({
"type": "warning",
"message": f"返信に「{topic}」関連の内容が含まれていることを検出し、置き換えました"
})
# 上書きするメッセージを返す(add_messages reducer 経由)
from langchain.messages import AIMessage
return {
"messages": [
AIMessage(content="申し訳ありませんが、この質問にはお答えできません。"
"プログラミング学習に関する内容を質問してください。")
]
}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[response_audit],
system_prompt="あなたはサンプル のアシスタントです。",
)
result = agent.invoke({
"messages": [HumanMessage(content="Python にはどのような学習リソースがありますか?")]
})
print(f"正常な返信: {result['messages'][-1].content}")
FORBIDDEN_TOPICS = ["政治", "暴力", "ポルノ"]
@after_model
def response_audit(state, runtime):
"""モデルの返信を審査し、禁止トピックに関連する場合は置き換える"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
content = str(last_msg.content) if hasattr(last_msg, 'content') else ""
for topic in FORBIDDEN_TOPICS:
if topic in content:
runtime.stream_writer({
"type": "warning",
"message": f"返信に「{topic}」関連の内容が含まれていることを検出し、置き換えました"
})
# 上書きするメッセージを返す(add_messages reducer 経由)
from langchain.messages import AIMessage
return {
"messages": [
AIMessage(content="申し訳ありませんが、この質問にはお答えできません。"
"プログラミング学習に関する内容を質問してください。")
]
}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[response_audit],
system_prompt="あなたはサンプル のアシスタントです。",
)
result = agent.invoke({
"messages": [HumanMessage(content="Python にはどのような学習リソースがありますか?")]
})
print(f"正常な返信: {result['messages'][-1].content}")
シナリオ 4:プロンプト情報を自動追加
例
from langchain.agents.middleware import after_model
from langchain.messages import AIMessage
@after_model
def append_disclaimer(state, runtime):
"""毎回のモデル返信後に免責事項を自動的に追加する"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
# AI の最終返信(tool_calls がないもの)のときのみ追加
if (last_msg.type == "ai"
and last_msg.content
and not (hasattr(last_msg, 'tool_calls') and last_msg.tool_calls)):
# 最後の AI メッセージを置き換えて、免責事項を追加する
return {
"messages": [
AIMessage(
content=(
last_msg.content
+ "\n\n---\n*上記の内容はサンプル AI アシスタントによって生成されたものであり、参考までに提供されています。*
)
)
]
}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[append_disclaimer],
system_prompt="あなたはサンプルのアシスタントです。",
)
result = agent.invoke({
"messages": [HumanMessage(content="Python は学びやすいですか?")]
})
print(result["messages"][-1].content)
from langchain.messages import AIMessage
@after_model
def append_disclaimer(state, runtime):
"""毎回のモデル返信後に免責事項を自動的に追加する"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
# AI の最終返信(tool_calls がないもの)のときのみ追加
if (last_msg.type == "ai"
and last_msg.content
and not (hasattr(last_msg, 'tool_calls') and last_msg.tool_calls)):
# 最後の AI メッセージを置き換えて、免責事項を追加する
return {
"messages": [
AIMessage(
content=(
last_msg.content
+ "\n\n---\n*上記の内容はサンプル AI アシスタントによって生成されたものであり、参考までに提供されています。*
)
)
]
}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[append_disclaimer],
system_prompt="あなたはサンプルのアシスタントです。",
)
result = agent.invoke({
"messages": [HumanMessage(content="Python は学びやすいですか?")]
})
print(result["messages"][-1].content)
実行結果:
Python 是一门非常适合初学者的编程语言,它的语法简洁、贴近自然语言... --- *上記の内容はサンプルのAIアシスタントが生成したもので、参考までにご覧ください。*
can_jump_to——フロー遷移制御
before_model と after_model では、can_jump_to パラメータと jump_to 状態を使用して Agent のフローを制御できます。
例
from langchain.agents.middleware import before_model
@before_model(can_jump_to=["end"]) # ジャンプ可能なターゲットを宣言
def conditional_exit(state, runtime):
"""特定の条件下で Agent を直接終了する"""
messages = state.get("messages", [])
if not messages:
return None
# ユーザーが「さようなら」と言ったら、直接終了
last_content = str(messages[-1].content)
if last_content.strip() in ["さようなら", "バイバイ", "bye"]:
return {
"jump_to": "end", # Agent を直接終了
"messages": [{"role": "assistant", "content": "さようなら!またのご利用をお待ちしております。"}]
}
return None
@before_model(can_jump_to=["end"]) # ジャンプ可能なターゲットを宣言
def conditional_exit(state, runtime):
"""特定の条件下で Agent を直接終了する"""
messages = state.get("messages", [])
if not messages:
return None
# ユーザーが「さようなら」と言ったら、直接終了
last_content = str(messages[-1].content)
if last_content.strip() in ["さようなら", "バイバイ", "bye"]:
return {
"jump_to": "end", # Agent を直接終了
"messages": [{"role": "assistant", "content": "さようなら!またのご利用をお待ちしております。"}]
}
return None
| can_jump_to の値 | 意味 | 適用シナリオ |
|---|---|---|
| ["end"] | 終了にジャンプ可能 | 条件付き終了、安全インターセプト |
| ["model"] | モデルにジャンプバック可能 | モデルに再処理させる必要がある |
| ["tools"] | ツールノードにジャンプ可能 | モデルをスキップしてツールを直接実行 |
| ["model", "end"] | モデルまたは終了にジャンプ可能 | 複数の条件分岐 |
その他の拡張can_jump_to でターゲットを宣言しない場合、jump_to は無視されます。これは、ミドルウェアが不正なノードに誤ってジャンプするのを防ぐ安全メカニズムです。