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

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):
    """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

from langchain.agents.middleware import before_model


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

from langchain.agents.middleware import after_model


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.agents.middleware import after_model
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

from langchain.agents.middleware import before_model


@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 ValueMeaningApplicable Scenarios
["end"]Can jump to endConditional exit, security interception
["model"]Can jump back to modelWhen the model needs to reprocess
["tools"]Can jump to tool nodeSkip the model and execute the tool directly
["model", "end"]Can jump to model or endMultiple conditional branches

If 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.

Other Extensions