Skip to content

create_agent 中间件

概要

create_agentmiddleware 参数是 Agent 执行循环的拦截器体系——在模型调用前后、工具执行前后插入自定义逻辑,不改 prompt 也不改工具代码。日志记录、Token 预算控制、敏感信息过滤、人工审批、任务拆解……这些横切关注点全部收敛到 middleware 列表中,与业务逻辑彻底解耦。LangChain 内置了十余种预置中间件,也可以通过 AgentMiddleware 基类或装饰器编写自定义中间件,并通过 can_jump_to 声明条件跳转能力。本文按 常用内置中间件 → 其他内置中间件 → 组合与执行顺序 → 自定义中间件 递进展开。

一、常用内置中间件

LangChain 在 langchain.agents.middleware 下提供了一批开箱即用的中间件。以下四种覆盖了生产环境最常见的横切需求。

1. SummarizationMiddleware — 对话摘要

长对话的 Token 消耗随消息数线性增长。SummarizationMiddleware 在上下文接近模型窗口上限时自动将历史对话压缩为摘要,保留最近 N 条消息不动,旧消息替换为摘要文本:

python
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """搜索知识库。"""
    return f"关于'{query}'的结果..."

agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[search],
    middleware=[
        SummarizationMiddleware(
            model=ChatOpenAI(model="gpt-4o-mini"),  # 用便宜模型做摘要
            trigger=("tokens", 4000),                # Token 数达 4000 时触发
            keep=("messages", 20),                   # 保留最近 20 条消息
        ),
    ],
)
参数类型默认值说明
modelstr | BaseChatModel必填执行摘要的模型,建议用便宜的轻量模型
triggerContextSize | listNone触发条件,支持 ("tokens", N) / ("messages", N) / ("fraction", f),传列表为 OR 关系
keepContextSize("messages", 20)摘要后保留多少最近上下文不压缩
summary_promptstr内置默认值自定义摘要提示词,需含 {messages} 占位符

trigger 的高级用法——列表实现 OR,字典实现 AND,列表套字典混合:

python
# OR 逻辑:任一条件满足即触发
trigger=[("tokens", 3000), ("messages", 6)]

# AND 逻辑:必须同时满足(tokens>=5000 且 messages>=3)
trigger={"tokens": 5000, "messages": 3}

# 混合:(A AND B) OR (C AND D)
trigger=[{"tokens": 5000, "messages": 3}, {"tokens": 3000, "messages": 6}]

WARNING

trigger 只计算 state["messages"] 中的消息 Token——不包含 system prompt 和工具 Schema 的开销。因此触发值要留足余量,建议设在模型上限的 70%~80%。

2. HumanInTheLoopMiddleware — 人工审批

HumanInTheLoopMiddleware 在敏感工具执行前暂停 Agent,等待人工批准、修改或拒绝。文件删除、邮件发送、数据库写入等不可逆操作的标准安全护栏:

python
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import MemorySaver

agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[send_email, delete_file, read_file],
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "send_email": {
                    "allowed_decisions": ["approve", "edit", "reject"],
                },
                "delete_file": {
                    "allowed_decisions": ["approve", "reject"],
                },
                # 未列出的工具(如 read_file)自动放行
            },
        ),
    ],
    checkpointer=MemorySaver(),  # HITL 必须配 checkpointer
)
决策类型效果
approve用 Agent 填写的原始参数执行工具
edit人工修改参数后再执行
reject跳过执行,返回拒绝理由给 Agent
respond直接用人工回复作为工具结果("问用户"模式)

条件拦截 when 谓词——不是每次都拦,只在参数命中特定条件时暂停:

python
def writes_outside_workspace(request) -> bool:
    """仅拦截写入工作区外的操作。"""
    path = request.tool_call["args"].get("file_path", "")
    return not path.startswith("/workspace/")

HumanInTheLoopMiddleware(interrupt_on={
    "write_file": {
        "allowed_decisions": ["approve", "reject"],
        "when": writes_outside_workspace,    # 工作区内直接放行
    },
})

Agent 暂停后,通过 Command(resume=...) 提供决策并继续执行:

python
from langgraph.types import Command

# 批准 → 按原始参数执行
agent.invoke(
    Command(resume={"decisions": [{"type": "approve"}]}),
    config={"configurable": {"thread_id": "user-session-1"}},
)

# 修改参数后执行
agent.invoke(Command(resume={
    "decisions": [{
        "type": "edit",
        "edited_action": {"name": "send_email", "args": {"to": "admin@corp.com"}},
    }]
}), config={"configurable": {"thread_id": "user-session-1"}})

# 拒绝
agent.invoke(Command(resume={
    "decisions": [{"type": "reject", "message": "不允许向外部邮箱发信"}]
}), config={"configurable": {"thread_id": "user-session-1"}})

WARNING

checkpointerthread_id 是 HITL 的硬性前提——没有 checkpointer,中断后的状态无法持久化,恢复执行会丢失上下文。

3. PIIMiddleware — 敏感信息检测

PIIMiddleware 在消息进入模型之前扫描并处理个人敏感信息,支持脱敏、遮罩、哈希、阻断四种策略:

python
from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware

agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[customer_lookup],
    middleware=[
        PIIMiddleware(
            built_in_types={
                "credit_card": "redact",     # 信用卡号 → 替换为 [REDACTED]
                "email": "mask",             # 邮箱 → u***@domain.com
                "phone": "hash",             # 手机 → SHA-256 哈希
                "ip": "redact",              # IP → [REDACTED]
            },
            custom_patterns={
                r"\b[A-Z]{2}\d{6}\b": "block",  # 自定义规则:工号格式 → 阻断请求
            },
        ),
    ],
)
策略效果适用场景
redact替换为 [REDACTED]完全不需要原始值的场景
mask部分遮盖(u***@domain.com需要保留部分格式信息的场景
hash单向哈希替换需要去重但不可逆的场景
block拒绝请求,抛异常严重违规,不允许继续执行

PIIMiddleware 工作在 before_model 阶段——消息在发送给 LLM 之前已经完成脱敏,模型根本看不到原始敏感信息。

4. TodoListMiddleware — 任务规划

TodoListMiddleware 为 Agent 注入 write_todos 工具,让模型在处理复杂任务时先拆解步骤、再逐步执行,完成时更新状态:

python
from langchain.agents import create_agent
from langchain.agents.middleware import TodoListMiddleware

agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[search, calculator],
    middleware=[
        TodoListMiddleware(
            system_prompt=(
                "拆分任务时确保每一步都可独立完成。"
                "每完成一步立即更新状态。"
            ),
        ),
    ],
)

Todo 结构:

字段类型说明
contentstr任务描述
status"pending" | "in_progress" | "completed"当前状态

TIP

write_todos全量替换语义——每次调用覆盖整个 Todo 列表,不能增量追加。模型在同一轮内不应并行调用多次 write_todos,否则可能出现状态竞争。

二、其他内置中间件

除上述四种外,LangChain 还提供以下内置中间件,按用途分为四类:

类别中间件用途
成本控制ModelCallLimitMiddleware限制模型调用次数,防无限循环
ToolCallLimitMiddleware限制工具调用次数(全局或按工具)
容错与重试ModelRetryMiddleware模型调用失败自动重试 + 指数退避
ToolRetryMiddleware工具调用失败自动重试 + 指数退避
ModelFallbackMiddleware主模型失败时自动切换备用模型
上下文优化LLMToolSelectorMiddleware用轻量 LLM 预选相关工具,减少主模型 Token 消耗
ContextEditingMiddleware修剪或清理旧的工具调用输出
测试与模拟LLMToolEmulator用 LLM 模拟工具返回,方便开发调试

全部从 langchain.agents.middleware 直接导入,用法与第一节四种完全一致——实例化后放入 middleware=[] 列表即可。

三、多个中间件组合及执行顺序

1. 列表顺序决定包装层级

create_agent(middleware=[A, B, C])列表顺序 = 包装层级:A 是最外层,C 是最内层。不同钩子类型的执行方向完全不同:

text
create_agent(model=..., middleware=[A, B, C])

before_* 钩子(正序):
  A.before_model → B.before_model → C.before_model
          │              │              │
          ▼              ▼              ▼
  ┌─────────────────────────────────────────┐
  │  A.wrap_model_call                      │
  │    └─ B.wrap_model_call                 │
  │         └─ C.wrap_model_call            │
  │              └─ 实际模型调用             │  ← 洋葱嵌套
  │         ──────────────────────────      │
  │              ──────────────────────      │
  │  ──────────────────────────────────      │
  └─────────────────────────────────────────┘
          │              │              │
          ▼              ▼              ▼
after_* 钩子(逆序):
  C.after_model → B.after_model → A.after_model
  • before_* 钩子按列表正序执行(A → B → C),适合逐层做前置检查
  • wrap_* 钩子按列表洋葱嵌套(A 包 B 包 C),最外层有最高控制权——A 可以短路跳过 B、C 和模型调用
  • after_* 钩子按列表逆序执行(C → B → A),与 before 对称,内层先收尾

2. 组合实战:限流 + 摘要 + 审批

三者组合,按容错在前、业务在中、安全在里的层次排列:

python
agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[send_email, search],
    middleware=[
        # ① 最外层:重试——所有操作的最后兜底
        ModelRetryMiddleware(stop_after_attempt=3),

        # ② 中层:摘要——长对话自动压缩
        SummarizationMiddleware(
            model=ChatOpenAI(model="gpt-4o-mini"),
            trigger=("tokens", 6000),
            keep=("messages", 20),
        ),

        # ③ 最内层:人工审批——在工具实际执行前拦截
        HumanInTheLoopMiddleware(
            interrupt_on={"send_email": {"allowed_decisions": ["approve", "reject"]}},
        ),
    ],
    checkpointer=MemorySaver(),
)

执行流:请求进入时重试包裹首先生效 → 触发模型调用前摘要检查 Token 数 → 模型返回工具调用后审批拦截 → 审批通过才真正执行工具。三层职责分明,互不耦合——去掉任何一个,其余照常工作。

四、自定义中间件

内置中间件覆盖不了所有场景时,通过钩子函数或 AgentMiddleware 子类实现定制逻辑。

1. 钩子分类:节点风格 vs 包装风格

中间件提供两种风格的钩子,选型取决于你需要"观察"还是"控制":

类型钩子触发频率能做什么不能做什么
节点before_agent每次调用 1 次初始化、注入消息到 state阻止调用
before_model每轮 1 次检查 state、记录日志、jump_to重试、修改请求
after_model每轮 1 次检查输出、记录日志、jump_to修改响应
after_agent每次调用 1 次清理、统计、通知
包装wrap_model_call每轮 1 次短路调用、重试、修改请求/响应
wrap_tool_call每次工具调用 1 次短路工具、修改参数、注入结果

节点风格返回 dict | None(修改 state 就返回 dict,不修改就返回 None)。包装风格通过 handler(request) 控制执行 0 次(短路)、1 次(正常)或 N 次(重试)。

2. 节点风格钩子

节点风格钩子——before_agent / before_model / after_model / after_agent——在固定时间点顺序触发。以下用同一个场景对比两种实现:记录消息数 + 限制调用次数

python
from langchain.agents.middleware import before_model, after_model
from langchain.agents.middleware.types import AgentState
from langgraph.runtime import Runtime
from typing import Any

@before_model
def log_and_limit(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    """调用前:记录消息数,超限时短路。"""
    count = len(state["messages"])
    print(f"[model] 第 {count} 条消息")
    if count > 50:
        from langchain_core.messages import AIMessage
        return {
            "messages": [AIMessage("对话过长,已终止。")],
            "jump_to": "end",
        }
    return None

@after_model
def log_result(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    """调用后:打印回复摘要。"""
    last = state["messages"][-1]
    print(f"[model] 回复前 60 字: {str(last.content)[:60]}")
    return None

agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[...],
    middleware=[log_and_limit, log_result],  # 函数直接传入
)
python
from langchain.agents.middleware import AgentMiddleware
from langchain.agents.middleware.types import AgentState
from langgraph.runtime import Runtime
from typing import Any

class BudgetMiddleware(AgentMiddleware):
    """限制模型调用次数,超出预算强制终止。"""

    def __init__(self, max_calls: int = 10):
        super().__init__()
        self.max_calls = max_calls
        self._count = 0                         # 实例属性,跨钩子共享

    def before_model(
        self, state: AgentState, runtime: Runtime
    ) -> dict[str, Any] | None:
        self._count += 1
        if self._count > self.max_calls:
            from langchain_core.messages import AIMessage
            return {
                "messages": [AIMessage("已达到最大调用次数。")],
                "jump_to": "end",
            }
        print(f"[Budget] 第{self._count}/{self.max_calls}次调用")
        return None

    def after_agent(
        self, state: AgentState, runtime: Runtime
    ) -> dict[str, Any] | None:
        print(f"[Budget] 本次共调用模型 {self._count} 次")
        return None

agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[...],
    middleware=[BudgetMiddleware(max_calls=5)],
)

装饰器干净利落——一个钩子一个函数,粒度最细。代价是 @before_model@after_model 是两个独立函数,共享计数器只能靠闭包。类的核心价值恰好在这里:多个钩子放在同一类体中,self._countbefore_modelafter_agent 之间自然流通。

3. 包装风格钩子

包装风格钩子——wrap_model_call / wrap_tool_call——包裹实际的模型或工具调用,拿到 handler 后可以决定不调用(短路)、调一次(正常)、调多次(重试)。

python
from langchain.agents.middleware import wrap_model_call
from langchain.agents.middleware.types import ModelRequest, ModelResponse

@wrap_model_call
def retry_on_error(
    request: ModelRequest,
    handler: callable,
) -> ModelResponse:
    """异常时自动重试最多 3 次。"""
    for attempt in range(3):
        try:
            return handler(request)             # handler(request) 触发真实调用
        except Exception as e:
            if attempt == 2:                    # 最后一次仍失败 → 抛出
                raise
            print(f"[retry] 第{attempt + 1}次重试: {e}")

agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[...],
    middleware=[retry_on_error],                # 装饰器函数直接传入
)
python
from langchain.agents.middleware import AgentMiddleware
from langchain.agents.middleware.types import ModelRequest, ModelResponse

class AdaptiveRetryMiddleware(AgentMiddleware):
    """根据剩余预算动态调整重试次数。"""

    def __init__(self, max_retries: int = 3):
        super().__init__()
        self.max_retries = max_retries
        self._budget = max_retries              # 全局预算,跨调用递减

    def wrap_model_call(
        self, request: ModelRequest, handler: callable
    ) -> ModelResponse:
        for attempt in range(self._budget + 1):
            try:
                result = handler(request)
                return result                   # 成功则恢复正常预算
            except Exception as e:
                self._budget -= 1
                if attempt >= self._budget:
                    raise
                print(f"[retry] 剩余预算 {self._budget}: {e}")

agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[...],
    middleware=[AdaptiveRetryMiddleware(max_retries=5)],
)

handler(request) 是关键的"控制杆"——不调用就是短路,调 N 次就是重试 N 次。装饰器版本适合单一职责:固定次数重试,简洁直接。类版本适合需要实例记忆的场景:self._budget 跨多轮调用递减,第一次把预算耗光后后续调用不再重试。这是类实现的核心价值——钩子间共享状态 + 初始化参数

节点 vs 包装 + 装饰器 vs 类的选型速查:

场景推荐方式
单钩子、无状态(日志、校验)装饰器(节点或包装均可)
单钩子、需要短路/重试@wrap_model_call 装饰器
多钩子共享状态(计数器、连接)类 + 节点风格钩子
需要初始化参数 + 运行时状态类(节点或包装均可)

4. can_jump_to — 条件跳转

can_jump_to 声明钩子有权跳转到哪些目标节点。不声明则钩子不能使用 jump_to。可跳转目标:

目标含义
"end"跳过剩余所有步骤,直接走 after_agent → 结束
"tools"跳过模型调用,直接进入工具执行阶段
"model"跳过工具执行,直接回到模型推理

装饰器声明——在 @before_model / @after_model 中传入参数:

python
from langchain_core.messages import AIMessage

@before_model(can_jump_to=["end"])
def enforce_message_limit(state: AgentState, runtime: Runtime) -> dict | None:
    """消息数超限直接终止。"""
    if len(state["messages"]) > 50:
        return {
            "messages": [AIMessage("对话已达上限,请重新开始。")],
            "jump_to": "end",
        }
    return None

类声明——通过 hook_config 装饰器标记方法:

python
from langchain.agents.middleware import AgentMiddleware, hook_config

class RoutingMiddleware(AgentMiddleware):
    @hook_config(can_jump_to=["tools", "end"])
    def after_model(self, state, runtime) -> dict | None:
        """模型回复为空时短路到 end,要求补充信息时跳到 tools。"""
        last_msg = state["messages"][-1]
        if not last_msg.content.strip():
            return {"jump_to": "end"}         # 空回复直接结束
        if last_msg.tool_calls:
            return None                        # 有工具调用,正常执行
        return None

WARNING

jump_to 跳过的钩子不会执行。例如 before_modeljump_to="end" 后,本轮 wrap_model_call、模型调用、after_model 全部跳过,直接走 after_agent。这在提前终止场景下是高价值的优化,但要注意after_agent 仍需处理被跳过的中间状态

小结

create_agent 的 middleware 是 Agent 的插件系统——在"推理→调用工具→回传→再推理"的循环中嵌入七个标准钩子点,通过装饰器或类实现自定义逻辑,按列表顺序控制执行层级

  • 四种常用内置中间件:Summarization 管 Token 预算、HITL 管安全审批、PII 管敏感数据、TodoList 管任务拆解——覆盖了长对话、高风险操作、合规和复杂任务四大痛点
  • 其他内置中间件按成本控制 / 容错重试 / 上下文优化 / 测试模拟四类归类,按需取用
  • 组合时列表顺序决定包装层级,before 正序、after 逆序、wrap 洋葱嵌套——理解这三者才能排出正确顺序
  • 自定义中间件分节点风格(观察)和包装风格(控制),装饰器适合简单单钩子、类适合有状态的复杂场景,can_jump_to 赋予钩子条件跳转能力

掌握了 middleware,就不需要为了加一行日志去改 prompt,也不需要为了加审批去改工具代码。Agent 只管"做什么",中间件只管"怎么做"。

📖 相关资源

CC BY-NC-SA 4.0 协议