LangChain Middleware Hooks -- @before_model and @after_model
before_model and after_model are the two most commonly used middleware hooks. They execute before and after each model call, suitable for content filtering, message preprocessing, response review, etc.
@before_model — Interception Before Model Call
@before_model executes before each model call. Here you can modify messages, inject context conditions, or skip the model call entirely.
Scenario 1: Message Preprocessing — Limiting Conversation Length
Example
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):
"""Limit message history length to prevent excessively long context"""
messages = state.get("messages", [])
# Keep system message + the most recent 6 messages
MAX_MESSAGES = 6
if len(messages) > MAX_MESSAGES:
# Trim to the most recent messages
trimmed = messages[-MAX_MESSAGES:]
# Ensure the first message is a user message
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="You are the assistant of EXAMPLE tutorial.",
)
# Simulate multi-turn conversation
result = agent.invoke({
"messages": [
HumanMessage(content="Round 1"),
HumanMessage(content="Round 2"),
HumanMessage(content="Round 3"),
HumanMessage(content="Round 4"),
HumanMessage(content="Round 5"),
HumanMessage(content="Round 6"),
HumanMessage(content="Round 7"),
]
})
print(f"Message count: {len(result['messages'])}")
print(f"Reply: {result['messages'][-1].content}")
Output:
Message count: 8 Reply: Hello! It looks like you are testing multi-turn conversation. How can I help you?
Scenario 2: Content Filtering — Blocking Sensitive Words
Example
SENSITIVE_WORDS = ["password", "bank card number", "ID card number"]
@before_model
def content_filter(state, runtime):
"""Check whether user messages contain sensitive words, and intercept if they do"""
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"[Blocked] Sensitive word detected: {word}")
# jump_to="end" ends directly, preventing the model from replying
return {
"jump_to": "end",
"messages": [
HumanMessage(content=f"Sorry, for security reasons, requests containing '{word}' cannot be processed.")
]
}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[content_filter],
system_prompt="You are the assistant of EXAMPLE tutorial.",
)
# Normal question
result = agent.invoke({
"messages": [HumanMessage(content="How do I get started with Python?")]
})
print(f"Normal question: {result['messages'][-1].content[:80]}")
# Sensitive question
result = agent.invoke({
"messages": [HumanMessage(content="What is my bank card number? Can you help me check it?")]
})
print(f"\nSensitive question: {result['messages'][-1].content}")
Output:
[拦截] 检测到敏感词: 银行卡号 Normal question: Python 入门可以从安装 Python 环境开始... Sensitive question: 抱歉,为了安全,不能处理包含「银行卡号」的请求。
@after_model — Processing After Model Call
@after_model executes after the model replies, suitable for reviewing model output, extracting key information, appending follow-up instructions, etc.
Scenario 3: Response Content Review
Example
FORBIDDEN_TOPICS = ["politics", "violence", "pornography"]
@after_model
def response_audit(state, runtime):
"""Review model replies and replace them if they involve prohibited topics"""
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"Reply detected as containing '{topic}' related content, has been replaced"
})
# Return an overwriting message (via add_messages reducer)
from langchain.messages import AIMessage
return {
"messages": [
AIMessage(content="Sorry, I cannot answer this question."
"Please ask about content related to programming learning.")
]
}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[response_audit],
system_prompt="You are the assistant of EXAMPLE tutorial.",
)
result = agent.invoke({
"messages": [HumanMessage(content="What learning resources are available for Python?")]
})
print(f"Normal reply: {result['messages'][-1].content}")
Scenario 4: Automatically Appending Prompt Information
Example
from langchain.messages import AIMessage
@after_model
def append_disclaimer(state, runtime):
"""Automatically append a disclaimer after each model reply"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
# Only append when it is the AI's final reply (no tool_calls)
if (last_msg.type == "ai"
and last_msg.content
and not (hasattr(last_msg, 'tool_calls') and last_msg.tool_calls)):
# Replace the last AI message, adding the disclaimer
return {
"messages": [
AIMessage(
content=(
last_msg.content
+ "\n\n---\n*The above content was generated by the EXAMPLE AI assistant and is for reference only.*
)
)
]
}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[append_disclaimer],
system_prompt="You are the assistant of EXAMPLE.",
)
result = agent.invoke({
"messages": [HumanMessage(content="Is Python easy to learn?")]
})
print(result["messages"][-1].content)
Output:
Python 是一门非常适合初学者的编程语言,它的语法简洁、贴近自然语言... --- *The above content was generated by the Example AI assistant and is for reference only.*
can_jump_to — Flow Jump Control
In before_model and after_model, you can control the Agent's flow through the can_jump_to parameter and the jump_to state:
Example
@before_model(can_jump_to=["end"]) # Declare the targets that can be jumped to
def conditional_exit(state, runtime):
"""End the Agent directly under specific conditions"""
messages = state.get("messages", [])
if not messages:
return None
# If the user says "goodbye", end directly
last_content = str(messages[-1].content)
if last_content.strip() in ["Goodbye", "Bye", "bye"]:
return {
"jump_to": "end", # End the Agent directly
"messages": [{"role": "assistant", "content": "Goodbye! Looking forward to serving you next time."}]
}
return None
| can_jump_to Value | Meaning | Applicable Scenarios |
|---|---|---|
| ["end"] | Can jump to end | Conditional exit, security interception |
| ["model"] | Can jump back to model | When the model needs to reprocess |
| ["tools"] | Can jump to tool node | Skip the model and execute the tool directly |
| ["model", "end"] | Can jump to model or end | Multiple conditional branches |
Other ExtensionsIf the target is not declared in can_jump_to, jump_to will be ignored. This is a security mechanism to prevent middleware from accidentally jumping to illegal nodes.