24_中间件系统源码分析_Middleware链的洋葱模型与异常处理

概述

Middleware 不是装饰器语法糖,而是被编译进 Agent 图里的节点和包装器

上一篇我们讲了 create_agent() 如何自动组装 LangGraph StateGraph

这一篇继续往下看 Middleware。

从使用者视角看,Middleware 很像 Web 框架里的拦截器:

python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import before_model, wrap_model_call


@before_model
def log_before_model(state, runtime):
    print("before model")
    return None


@wrap_model_call
def retry_model(request, handler):
    try:
        return handler(request)
    except Exception:
        return handler(request)


agent = create_agent(
    model="openai:gpt-4o-mini",
    tools=[],
    middleware=[log_before_model, retry_model],
)

看起来只是把几个函数塞进 middleware=[...]

但源码里发生的事情更具体:

text 复制代码
create_agent(...)
  -> 收集中间件 hook
  -> 把 before/after hook 变成 graph node
  -> 把 wrap_model_call 组合成模型调用洋葱链
  -> 把 wrap_tool_call 组合成工具调用洋葱链
  -> 添加 StateGraph 边和条件边
  -> compile 成 CompiledStateGraph

所以 Middleware 的本质不是"Agent 外面套一层函数",而是:

  • Node-style hook:被注册成 LangGraph 节点,参与图的状态更新和跳转。
  • Wrap-style hook:被组合成包装链,包住一次模型调用或一次工具调用。
  • State reducer :负责把 hook 返回的状态增量合并回 AgentState
  • Conditional edge :负责根据 jump_to、tool calls、结构化输出等条件继续走图或结束。

Middleware 是 create_agent() 在编译图时插入的生命周期扩展点,它既有图节点,也有函数包装链。

源码里的六类 Hook

但根据当前 LangChain OSS Python 文档和本地 langchain==1.3.9 源码,公开的 Middleware hook 分成六类:

类型 Hook 执行时机 是否每轮循环都会执行
Node-style before_agent Agent 调用开始前 否,只执行一次
Node-style before_model 每次模型调用前
Node-style after_model 每次模型返回后
Node-style after_agent Agent 调用结束前 否,只执行一次
Wrap-style wrap_model_call 包裹每次模型调用
Wrap-style wrap_tool_call 包裹每次工具调用

有些旧资料会提到 before_agent_execute,但当前源码中的基类 AgentMiddleware 并没有这个公开方法。生命周期入口已经拆成了 before_agentafter_agent

本文按当前源码写作,核心文件是:

text 复制代码
langchain/agents/middleware/types.py
langchain/agents/factory.py

其中:

  • types.py:定义 AgentStateAgentMiddlewareModelRequestModelResponse、各类 decorator。
  • factory.py:在 create_agent() 中收集 hook、组合 wrapper、添加 graph node 和 edge。

第一层源码:AgentState 是所有 Middleware 共享的状态底座

先看状态。

AgentState 位于:

text 复制代码
langchain/agents/middleware/types.py

核心结构可以简化成:

python 复制代码
class AgentState(TypedDict, Generic[ResponseT]):
    messages: Required[Annotated[list[AnyMessage], add_messages]]
    jump_to: NotRequired[Annotated[JumpTo | None, EphemeralValue, PrivateStateAttr]]
    structured_response: NotRequired[Annotated[ResponseT, OmitFromInput]]

三个字段非常关键:

字段 作用
messages Agent 主上下文,保存用户消息、AI 消息、Tool 消息
jump_to Middleware 用来请求跳转到 modeltoolsend
structured_response 结构化输出成功后的最终对象

这里最容易忽略的是 messages 上的 add_messages reducer。

这意味着 hook 返回:

python 复制代码
return {"messages": [AIMessage(content="blocked")]}

并不是把历史消息整体替换掉,而是通过 reducer 追加或合并消息。

jump_to 使用了 EphemeralValuePrivateStateAttr,表示它更像一次性的内部控制信号,不应该暴露成普通输入输出字段。

Middleware 能改变 Agent 行为,是因为它能返回状态增量,而这些状态增量会被 LangGraph reducer 合并进 AgentState

第二层源码:AgentMiddleware 基类定义了六类生命周期入口

AgentMiddleware 也在 types.py

它的形状可以简化成:

python 复制代码
class AgentMiddleware(Generic[StateT, ContextT, ResponseT]):
    state_schema: type[StateT] = _DefaultAgentState
    tools: Sequence[BaseTool]
    transformers: Sequence[TransformerFactory] = ()

    def before_agent(self, state, runtime): ...
    async def abefore_agent(self, state, runtime): ...

    def before_model(self, state, runtime): ...
    async def abefore_model(self, state, runtime): ...

    def after_model(self, state, runtime): ...
    async def aafter_model(self, state, runtime): ...

    def wrap_model_call(self, request, handler): ...
    async def awrap_model_call(self, request, handler): ...

    def after_agent(self, state, runtime): ...
    async def aafter_agent(self, state, runtime): ...

    def wrap_tool_call(self, request, handler): ...
    async def awrap_tool_call(self, request, handler): ...

它不强制你每个方法都实现。

create_agent() 会判断某个 middleware 类是否覆写了基类方法:

python 复制代码
m.__class__.before_model is not AgentMiddleware.before_model

如果覆写了,就认为这个 middleware 提供了对应 hook。

这也是为什么 class-based middleware 可以只实现自己需要的部分:

python 复制代码
from typing import Any

from langchain.agents.middleware import AgentMiddleware, AgentState
from langgraph.runtime import Runtime


class AuditMiddleware(AgentMiddleware):
    def before_agent(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
        print("agent starts")
        return None

    def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
        print("model returned")
        return None

源码里还有三个 class attribute 值得记住:

属性 作用
state_schema 声明该 middleware 需要扩展哪些 state 字段
tools 声明 middleware 自带的工具
transformers 声明 stream transformer,用于流式事件投影

本篇文章将重点讲 hook 和执行顺序,transformers 后面讲流式系统时再展开。

AgentMiddleware 是一个可选择实现的生命周期接口,真正的"是否启用"由 create_agent() 检查方法是否被覆写来决定。

第三层源码:decorator 不是魔法,只是动态创建 AgentMiddleware 子类

我们常写:

python 复制代码
from langchain.agents.middleware import before_model


@before_model
def check_input(state, runtime):
    return None

这看起来像普通函数装饰器。

types.py 里的 decorator 会动态构造一个 AgentMiddleware 子类,并把你的函数放到对应方法上。

wrap_model_call 为例,源码逻辑可以简化成:

python 复制代码
def wrap_model_call(func=None, *, state_schema=None, tools=None, name=None):
    def decorator(func):
        def wrapped(self, request, handler):
            return func(request, handler)

        middleware_name = name or func.__name__

        return type(
            middleware_name,
            (AgentMiddleware,),
            {
                "state_schema": state_schema or AgentState,
                "tools": tools or [],
                "wrap_model_call": wrapped,
            },
        )()

    if func is not None:
        return decorator(func)
    return decorator

before_modelafter_modelbefore_agentafter_agentwrap_tool_call 也是同一个思路:

text 复制代码
函数
  -> decorator 包装
  -> 动态生成 AgentMiddleware 子类
  -> 返回 middleware 实例
  -> create_agent(middleware=[...]) 统一处理

所以 decorator-based middleware 和 class-based middleware 在进入 create_agent() 之后没有本质区别。

写法 适合场景
decorator-based 单个 hook、快速实验、逻辑简单
class-based 多个 hook、需要配置、需要同步/异步双实现、需要复用

装饰器只是把函数适配成 AgentMiddleware 实例,核心运行机制仍然是 AgentMiddleware

执行时序:before 顺序执行,after 逆序执行,wrap 洋葱嵌套

假设我们这样注册 middleware:

python 复制代码
agent = create_agent(
    model="openai:gpt-4o-mini",
    tools=[search, calculator],
    middleware=[
        auth_middleware,
        cache_middleware,
        retry_middleware,
    ],
)

执行顺序不是简单地从上到下全部跑完。

当前规则是:

text 复制代码
before_agent:
  auth.before_agent
  cache.before_agent
  retry.before_agent

每一轮 Agent loop:
  before_model:
    auth.before_model
    cache.before_model
    retry.before_model

  wrap_model_call:
    auth.wrap_model_call(
      cache.wrap_model_call(
        retry.wrap_model_call(
          model.invoke(...)
        )
      )
    )

  after_model:
    retry.after_model
    cache.after_model
    auth.after_model

  wrap_tool_call:
    auth.wrap_tool_call(
      cache.wrap_tool_call(
        retry.wrap_tool_call(
          tool.invoke(...)
        )
      )
    )

after_agent:
  retry.after_agent
  cache.after_agent
  auth.after_agent

可以用一张图理解:

text 复制代码
START
  |
  v
auth.before_agent -> cache.before_agent -> retry.before_agent
  |
  v
auth.before_model -> cache.before_model -> retry.before_model
  |
  v
auth.wrap_model_call
  |
  +-- cache.wrap_model_call
        |
        +-- retry.wrap_model_call
              |
              +-- model.invoke()
              |
        <-----+
  <-----------+
  |
  v
retry.after_model -> cache.after_model -> auth.after_model
  |
  +-- 有工具调用 --> tools -> 回到 before_model
  |
  +-- 无工具调用 --> retry.after_agent -> cache.after_agent -> auth.after_agent -> END

这个顺序和很多 Web 中间件类似:

  • 进入阶段:从外到内。
  • 核心调用:最内层执行真实模型或工具。
  • 返回阶段:从内到外。

区别是 LangChain 还有显式的 LangGraph 节点和条件边。

before_* 按注册顺序跑,after_* 按注册逆序跑,wrap_* 是第一项包住后面所有项的洋葱模型。

factory.py 第一步:收集哪些 middleware 实现了哪些 hook

create_agent()factory.py 里会先把 middleware 分组。

源码可以简化为:

python 复制代码
middleware_w_before_agent = [
    m for m in middleware
    if m.__class__.before_agent is not AgentMiddleware.before_agent
    or m.__class__.abefore_agent is not AgentMiddleware.abefore_agent
]

middleware_w_before_model = [
    m for m in middleware
    if m.__class__.before_model is not AgentMiddleware.before_model
    or m.__class__.abefore_model is not AgentMiddleware.abefore_model
]

middleware_w_after_model = [...]
middleware_w_after_agent = [...]
middleware_w_wrap_model_call = [...]
middleware_w_wrap_tool_call = [...]

这里有两个设计点:

第一,它同时检查同步和异步版本。

比如只实现了 awrap_model_call,也会被认为提供了 model wrap hook。之后如果你用同步 agent.invoke() 调用,基类同步方法会抛出 NotImplementedError,提示你当前 middleware 只支持异步路径。

第二,它会检查 middleware 名称是否重复:

python 复制代码
if len({m.name for m in middleware}) != len(middleware):
    raise AssertionError("Please remove duplicate middleware instances.")

因为 graph node 名称会使用:

text 复制代码
{m.name}.before_model
{m.name}.after_model

如果同名,图节点会冲突。

所以自定义 middleware 时,最好保证每个实例的 name 唯一。

python 复制代码
class NamedMiddleware(AgentMiddleware):
    def __init__(self, name: str):
        self._name = name

    @property
    def name(self) -> str:
        return self._name

create_agent() 不是盲目执行所有 middleware,而是先按覆写的方法把它们分组,再分别接入图节点或包装链。

factory.py 第二步:把 wrap_model_call 组合成洋葱链

wrap_model_call 的核心能力是控制模型调用:

  • 可以修改请求。
  • 可以调用 handler(request) 执行真实模型。
  • 可以不调用 handler,直接短路返回。
  • 可以多次调用 handler,实现重试。
  • 可以捕获异常后切换模型。
  • 可以改写模型响应。

factory.py 使用 _chain_model_call_handlers() 把多个 wrapper 组合起来。

简化逻辑是:

python 复制代码
def _chain_model_call_handlers(handlers):
    if not handlers:
        return None

    def compose_two(outer, inner):
        def composed(request, handler):
            def inner_handler(req):
                inner_result = inner(req, handler)
                return normalize(inner_result)

            outer_result = outer(request, inner_handler)
            return normalize(outer_result)

        return composed

    composed_handler = compose_two(handlers[-2], handlers[-1])
    for h in reversed(handlers[:-2]):
        composed_handler = compose_two(h, composed_handler)

    return composed_handler

假设有三个 wrapper:

text 复制代码
[auth, cache, retry]

最终得到:

text 复制代码
auth(
  cache(
    retry(
      model
    )
  )
)

请求进入时:

text 复制代码
auth -> cache -> retry -> model

响应返回时:

text 复制代码
model -> retry -> cache -> auth

这就是"洋葱模型"。

示例:模型 fallback middleware。

python 复制代码
from collections.abc import Callable

from langchain.agents.middleware import ModelRequest, ModelResponse, wrap_model_call
from langchain.chat_models import init_chat_model


fallback = init_chat_model("openai:gpt-4o-mini")


@wrap_model_call
def fallback_on_error(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    try:
        return handler(request)
    except Exception:
        return handler(request.override(model=fallback))

request.override(...) 很重要。

ModelRequest 当前源码已经不推荐直接改属性,而是推荐创建一个带覆盖值的新请求:

python 复制代码
new_request = request.override(model=fallback)

这样 wrapper 层之间的数据传递更清晰,也更不容易产生隐式副作用。

wrap_model_call 的组合方式是"第一个 middleware 最外层",它拿到控制权最大,适合鉴权、缓存、fallback、重试和动态模型选择。

ModelRequestModelResponse:wrapper 能改什么、返回什么

wrap_model_call 的入参是 ModelRequest

它包含:

字段 含义
model 当前要调用的 BaseChatModel
messages 不包含 system message 的对话消息
system_message 系统消息
tool_choice 工具选择策略
tools 当前模型可见的工具
response_format 结构化输出配置
state 当前 Agent state
runtime LangGraph runtime
model_settings 模型调用参数

真实模型调用发生在 factory.py_execute_model_sync()

python 复制代码
def _execute_model_sync(request):
    model_, effective_response_format = _get_bound_model(request)
    messages = request.messages
    if request.system_message:
        messages = [request.system_message, *messages]

    output = model_.invoke(messages)
    handled_output = _handle_model_output(output, effective_response_format)

    return ModelResponse(
        result=handled_output["messages"],
        structured_response=handled_output.get("structured_response"),
    )

也就是说:

text 复制代码
wrap_model_call
  -> handler(request)
  -> _execute_model_sync(request)
  -> model.invoke(messages)
  -> ModelResponse

wrap_model_call 可以返回三类结果:

返回类型 适合场景
ModelResponse 完整保留 messages 和 structured_response
AIMessage 简单短路或简单改写
ExtendedModelResponse 除模型响应外,还要返回一个 Command 更新 state

比如记录 token 用量:

python 复制代码
from collections.abc import Callable
from typing_extensions import NotRequired

from langchain.agents.middleware import (
    AgentState,
    ExtendedModelResponse,
    ModelRequest,
    ModelResponse,
    wrap_model_call,
)
from langgraph.types import Command


class UsageState(AgentState):
    last_model_tokens: NotRequired[int]


@wrap_model_call(state_schema=UsageState)
def track_usage(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ExtendedModelResponse:
    response = handler(request)
    usage = response.result[0].usage_metadata or {}
    total_tokens = usage.get("total_tokens", 0)

    return ExtendedModelResponse(
        model_response=response,
        command=Command(update={"last_model_tokens": total_tokens}),
    )

这里不能直接 return {"last_model_tokens": total_tokens},因为 wrap_model_call 不是 graph node,它是模型调用的包装函数。

要从 wrapper 层更新 state,需要通过:

python 复制代码
ExtendedModelResponse(
    model_response=response,
    command=Command(update={...}),
)

Node-style hook 直接返回 dict 更新 state;wrap_model_call 要更新 state,需要把 Command 包进 ExtendedModelResponse

factory.py 第三步:把 wrap_tool_call 交给 ToolNode

工具调用的 wrapper 走另一条路径。

create_agent() 会先收集实现了 wrap_tool_callawrap_tool_call 的 middleware,然后组合:

python 复制代码
wrap_tool_call_wrapper = _chain_tool_call_wrappers(wrappers)

组合规则同样是第一个最外层。

接着创建 ToolNode

python 复制代码
tool_node = ToolNode(
    tools=available_tools,
    wrap_tool_call=wrap_tool_call_wrapper,
    awrap_tool_call=awrap_tool_call_wrapper,
)

所以 wrap_tool_call 实际不是 model node 的一部分,而是 tools 节点执行每个工具时的包装逻辑。

示例:工具调用监控。

python 复制代码
from collections.abc import Callable

from langchain.agents.middleware import wrap_tool_call
from langchain_core.messages import ToolMessage
from langgraph.prebuilt.tool_node import ToolCallRequest


@wrap_tool_call
def monitor_tool(
    request: ToolCallRequest,
    handler: Callable[[ToolCallRequest], ToolMessage],
) -> ToolMessage:
    print(f"tool starts: {request.tool_call['name']}")
    result = handler(request)
    print(f"tool ends: {request.tool_call['name']}, status={result.status}")
    return result

wrap_tool_call 可以:

  • 修改 tool call 参数。
  • 捕获工具异常。
  • 重试工具调用。
  • 返回一个替代 ToolMessage
  • 返回 Command 来更新 state 或控制图行为。

但要注意一个边界:

text 复制代码
wrap_model_call 控制模型调用。
wrap_tool_call 控制工具调用。

如果你在 wrap_model_call 里动态给 request.tools 增加工具,但这个工具没有注册到 create_agent(tools=[...]) 或 middleware 的 tools 属性里,ToolNode 默认不知道怎么执行它。

源码里也有对应的错误提示,大意是:

text 复制代码
Middleware added tools that the agent doesn't know how to execute.

解决方式通常有两个:

方式 说明
启动时注册 把工具放进 create_agent(tools=[...]) 或 middleware 的 tools
动态处理 wrap_tool_call 里识别动态工具并自己执行

wrap_tool_call 不是包住整个 Agent,而是被传给 ToolNode,在每一次具体工具执行时生效。

factory.py 第四步:Node-style hook 被注册成 LangGraph 节点

before_agentbefore_modelafter_modelafter_agent 不是 wrapper,而是图节点。

factory.py 里会给每个 hook 加节点:

python 复制代码
graph.add_node(
    f"{m.name}.before_model",
    RunnableCallable(sync_before, async_before, trace=False),
    input_schema=resolved_state_schema,
)

节点名形如:

text 复制代码
MyMiddleware.before_agent
MyMiddleware.before_model
MyMiddleware.after_model
MyMiddleware.after_agent

然后 create_agent() 决定四个关键位置:

python 复制代码
entry_node = first_before_agent or first_before_model or "model"
loop_entry_node = first_before_model or "model"
loop_exit_node = first_after_model or "model"
exit_node = last_after_agent or END

这四个位置决定了整张图的骨架:

名称 含义
entry_node 图从 START 进入的第一个节点
loop_entry_node 每一轮 Agent loop 的入口
loop_exit_node 每一轮模型调用后的出口
exit_node Agent 结束前要走的节点

如果存在 before_agent,入口是第一个 before_agent

text 复制代码
START -> middleware1.before_agent

如果不存在 before_agent,但存在 before_model

text 复制代码
START -> middleware1.before_model

如果都没有:

text 复制代码
START -> model

Node-style hook 的本质就是 StateGraph 节点,create_agent() 通过 entry_nodeloop_entry_nodeloop_exit_nodeexit_node 把它们插进 Agent 主循环。

图编排细节:为什么 after_modelafter_agent 是逆序

before_model 的连边逻辑是顺序:

text 复制代码
m1.before_model -> m2.before_model -> m3.before_model -> model

after_model 是反向接:

text 复制代码
model -> m3.after_model -> m2.after_model -> m1.after_model

这和 wrapper 的返回路径一致。

假设 middleware 列表是:

python 复制代码
[auth, cache, retry]

那么:

text 复制代码
before_model:
  auth -> cache -> retry -> model

after_model:
  model -> retry -> cache -> auth

这样的设计可以让一组 middleware 的进入和退出更像成对括号:

text 复制代码
auth enter
  cache enter
    retry enter
      model
    retry exit
  cache exit
auth exit

after_agent 也是逆序:

text 复制代码
retry.after_agent -> cache.after_agent -> auth.after_agent -> END

这对资源释放、审计收尾、耗时统计很有用。

比如:

python 复制代码
class TimerMiddleware(AgentMiddleware):
    def before_agent(self, state, runtime):
        return {"started_at": time.perf_counter()}

    def after_agent(self, state, runtime):
        cost = time.perf_counter() - state["started_at"]
        print(f"agent cost: {cost:.2f}s")
        return None

如果外层 middleware 在进入时先建立上下文,退出时最后收尾,逆序就很自然。

after_* 逆序不是偶然,而是为了让 middleware 的进入和退出形成结构化嵌套。

Agent 跳转:jump_to 如何从状态字段变成条件边

Middleware 可以提前结束 Agent,或者要求回到模型、跳到工具节点。

示例:

python 复制代码
from typing import Any

from langchain.agents.middleware import AgentState, before_model
from langchain_core.messages import AIMessage
from langgraph.runtime import Runtime


@before_model(can_jump_to=["end"])
def stop_if_too_long(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    if len(state["messages"]) > 50:
        return {
            "messages": [AIMessage(content="Conversation limit reached.")],
            "jump_to": "end",
        }
    return None

关键是 can_jump_to=["end"]

decorator 会把可跳转目标记录到 hook 函数上,class-based middleware 可以用:

python 复制代码
from langchain.agents.middleware import hook_config


class LimitMiddleware(AgentMiddleware):
    @hook_config(can_jump_to=["end"])
    def before_model(self, state, runtime):
        ...

factory.py_add_middleware_edge() 会根据 can_jump_to 决定添加普通边还是条件边。

简化逻辑:

python 复制代码
if can_jump_to:
    def jump_edge(state):
        return resolve_jump(state.get("jump_to")) or default_destination

    graph.add_conditional_edges(name, jump_edge, destinations)
else:
    graph.add_edge(name, default_destination)

jump_to 的解析规则是:

jump_to 目标
"model" 回到 loop_entry_node,通常是第一个 before_modelmodel
"tools" 跳到 tools 节点
"end" 跳到 exit_node,如果有 after_agent 会先走 after_agent

这里有一个容易踩坑的点:

text 复制代码
返回 {"jump_to": "end"} 之前,hook 必须声明 can_jump_to=["end"]。

否则 factory.py 不会为这个 middleware 节点建立对应条件边。

jump_to 是状态里的跳转意图,can_jump_to 是编译图时允许哪些跳转的声明,两者必须配合。

Agent 主循环:模型节点如何决定去工具、回模型还是结束

模型节点执行完之后,不是固定去 tools

factory.py_make_model_to_tools_edge() 会看当前 state。

逻辑可以简化成:

python 复制代码
def model_to_tools(state):
    if state.get("jump_to"):
        return resolve_jump(state["jump_to"])

    last_ai_message, tool_messages = fetch_last_ai_and_tool_messages(state["messages"])

    if last_ai_message is None:
        return end

    if len(last_ai_message.tool_calls) == 0:
        return end

    pending_tool_calls = [
        call for call in last_ai_message.tool_calls
        if call["id"] not in executed_tool_message_ids
    ]

    if pending_tool_calls:
        return [Send("tools", [tool_call]) for tool_call in pending_tool_calls]

    if "structured_response" in state:
        return end

    return model

这个条件边对应 Agent 的核心循环:

text 复制代码
model
  |
  +-- 没有 tool_calls -------------> end
  |
  +-- 有未执行 tool_calls ----------> tools
  |
  +-- structured_response 已生成 ---> end
  |
  +-- 有人工注入 ToolMessage 等情况 -> model

工具节点执行完之后,_make_tools_to_model_edge() 决定下一步:

text 复制代码
tools
  |
  +-- 工具 return_direct=True ------> end
  |
  +-- 结构化输出工具完成 -----------> end
  |
  +-- 默认 ------------------------> model

这解释了为什么 after_model hook 很强:

它运行在模型返回之后、路由到工具之前。

因此它可以:

  • 检查模型是否产生危险 tool call。
  • 注入一条人工审批后的 ToolMessage。
  • 设置 jump_to="end" 提前结束。
  • 设置 jump_to="model" 让模型重新思考。

Agent 是否继续循环,不是由 while True 写死,而是由 LangGraph 条件边根据 state 动态路由。

异常处理:默认传播,控制权交给 wrap hook 和预置 middleware

Middleware 的异常处理要分两层看。

第一层:Node-style hook。

before_modelafter_model 这类 hook 本身就是 graph node。如果 hook 内部抛异常,默认会沿着图执行传播出去。

所以不要在 node-style hook 里写脆弱逻辑:

python 复制代码
@after_model
def unsafe_log(state, runtime):
    # 如果最后一条消息不是你预期的结构,这里可能抛异常
    print(state["messages"][-1].response_metadata["token_usage"])
    return None

更稳妥:

python 复制代码
@after_model
def safe_log(state, runtime):
    last = state["messages"][-1]
    usage = getattr(last, "usage_metadata", None)
    if usage:
        print(usage)
    return None

第二层:Wrap-style hook。

wrap_model_callwrap_tool_call 明确把 handler 交给你,因此异常控制权也交给你:

python 复制代码
@wrap_model_call
def retry_model(request, handler):
    for attempt in range(3):
        try:
            return handler(request)
        except Exception:
            if attempt == 2:
                raise

工具调用同理:

python 复制代码
@wrap_tool_call
def retry_tool(request, handler):
    for attempt in range(3):
        try:
            return handler(request)
        except Exception:
            if attempt == 2:
                raise

这也是 LangChain 提供预置 middleware 的原因。

例如:

  • ModelRetryMiddleware:模型调用重试。
  • ModelFallbackMiddleware:模型故障切换。
  • HumanInTheLoopMiddleware:工具调用前中断审批。
  • SummarizationMiddleware:长上下文摘要。
  • PIIMiddleware:敏感信息检测和处理。

它们不是特殊运行时能力,本质上也是基于这些 hook 做的组合。

LangChain 不会自动吞掉所有 middleware 异常;真正需要重试、fallback、降级时,应放进 wrap hook 或使用预置 middleware。

ExtendedModelResponse 的细节:多个 wrapper 同时更新 state 怎么合并

wrap_model_call 返回 ExtendedModelResponse 时,可以带一个 Command(update=...)

问题来了:如果多个 wrapper 都返回 Command,谁先应用?

factory.py_chain_model_call_handlers() 会把 commands 收集起来,并保持一个重要规则:

text 复制代码
inner-first, then outer

也就是内层先应用,外层后应用。

对 reducer 字段,例如 messages

text 复制代码
inner message + outer message

对非 reducer 字段,例如普通字符串字段:

text 复制代码
外层覆盖内层

示例:

python 复制代码
from typing import Annotated
from typing_extensions import NotRequired

from langchain.agents.middleware import AgentMiddleware, AgentState


def last_wins(_old: str, new: str) -> str:
    return new


class TraceState(AgentState):
    trace_layer: NotRequired[Annotated[str, last_wins]]

如果:

text 复制代码
inner -> {"trace_layer": "inner"}
outer -> {"trace_layer": "outer"}

最终是:

text 复制代码
trace_layer = "outer"

这符合洋葱模型的直觉:最外层 middleware 拥有最终解释权。

但有一个限制要注意。

factory.py 在构建 model node 返回命令时,会拒绝 wrap_model_call 里带 gotoresumegraphCommand

text 复制代码
Command goto is not yet supported in wrap_model_call middleware.
Use the jump_to state field with before_model/after_model hooks instead.

所以:

  • 想在 wrap_model_call 里补充 state:可以用 ExtendedModelResponse(command=Command(update=...))
  • 想控制图跳转:更推荐用 before_modelafter_model 返回 {"jump_to": ...}

ExtendedModelResponse 适合让 wrapper 更新 state,不适合让 wrapper 直接改图跳转;跳转应交给 node-style hook。

实战讲解:一个"审计 + 限流 + fallback"的组合 Middleware

下面写一个组合例子,把本文讲的机制串起来:

  • before_agent:记录请求开始。
  • before_model:超过最大模型调用次数则结束。
  • wrap_model_call:模型失败时切 fallback。
  • after_model:统计调用次数。
  • after_agent:输出审计日志。
python 复制代码
import time
from collections.abc import Callable
from typing import Any

from langchain.agents.middleware import (
    AgentMiddleware,
    AgentState,
    ModelRequest,
    ModelResponse,
    hook_config,
)
from langchain.chat_models import init_chat_model
from langchain_core.messages import AIMessage
from langgraph.runtime import Runtime
from typing_extensions import NotRequired


class AuditState(AgentState):
    started_at: NotRequired[float]
    model_call_count: NotRequired[int]


class AuditLimitFallbackMiddleware(AgentMiddleware[AuditState]):
    state_schema = AuditState

    def __init__(self, max_model_calls: int = 5):
        self.max_model_calls = max_model_calls
        self.fallback_model = init_chat_model("openai:gpt-4o-mini")

    def before_agent(
        self,
        state: AuditState,
        runtime: Runtime,
    ) -> dict[str, Any] | None:
        return {
            "started_at": time.perf_counter(),
            "model_call_count": state.get("model_call_count", 0),
        }

    @hook_config(can_jump_to=["end"])
    def before_model(
        self,
        state: AuditState,
        runtime: Runtime,
    ) -> dict[str, Any] | None:
        if state.get("model_call_count", 0) >= self.max_model_calls:
            return {
                "messages": [AIMessage(content="模型调用次数已达到上限。")],
                "jump_to": "end",
            }
        return None

    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ) -> ModelResponse:
        try:
            return handler(request)
        except Exception:
            return handler(request.override(model=self.fallback_model))

    def after_model(
        self,
        state: AuditState,
        runtime: Runtime,
    ) -> dict[str, Any] | None:
        return {
            "model_call_count": state.get("model_call_count", 0) + 1,
        }

    def after_agent(
        self,
        state: AuditState,
        runtime: Runtime,
    ) -> dict[str, Any] | None:
        started_at = state.get("started_at")
        if started_at is not None:
            print(f"agent cost: {time.perf_counter() - started_at:.2f}s")
        print(f"model calls: {state.get('model_call_count', 0)}")
        return None

注册:

python 复制代码
from langchain.agents import create_agent


agent = create_agent(
    model="openai:gpt-4o",
    tools=[],
    middleware=[
        AuditLimitFallbackMiddleware(max_model_calls=5),
    ],
)

这段代码背后对应的图大致是:

text 复制代码
START
  |
  v
Audit.before_agent
  |
  v
Audit.before_model
  |
  +-- jump_to=end --> Audit.after_agent -> END
  |
  v
model node
  |
  +-- wrap_model_call 包裹 model.invoke
  |
  v
Audit.after_model
  |
  +-- 没有工具调用 --> Audit.after_agent -> END

如果有工具,还会多一段:

text 复制代码
Audit.after_model
  |
  +-- 有 pending tool calls --> tools -> Audit.before_model -> model

复杂生产能力不是写进 Agent 主循环,而是拆成多个 hook,再由 create_agent() 编译进图。

常见误区

误区一:以为 Middleware 是 Agent 外层的一层普通函数。

不准确。

Node-style hook 是 graph node,wrap-style hook 是调用包装链。它们都在 create_agent() 编译时被接入。

误区二:以为 before_agent 每一轮都会执行。

不会。

before_agent 是每次 agent invocation 开始前一次;每轮模型调用前执行的是 before_model

误区三:以为 after_agentafter_model 后面每轮都会执行。

不会。

after_agent 是结束前一次。每轮模型返回后执行的是 after_model

误区四:以为 wrap_model_call 可以直接返回 dict 更新 state。

不行。

wrap_model_call 返回的是模型调用结果,更新 state 要用 ExtendedModelResponse(command=Command(update=...))

误区五:以为 jump_to 写了就一定生效。

不一定。

必须通过 can_jump_tohook_config(can_jump_to=[...]) 声明可跳转目标,让 factory.py 在编译图时添加条件边。

误区六:以为动态添加工具后 ToolNode 自动会执行。

不一定。

如果工具没有在 create_agent(tools=[...]) 或 middleware tools 里注册,ToolNode 默认不知道怎么执行。要么提前注册,要么用 wrap_tool_call 自己处理。

源码阅读路线:建议按这 5 步看

如果你想自己 debug Middleware,建议按这个顺序读源码。

第一步:看 types.py 的数据结构。

重点看:

text 复制代码
ModelRequest
ModelResponse
ExtendedModelResponse
AgentState
AgentMiddleware
hook_config
before_model / after_model / wrap_model_call / wrap_tool_call decorators

先弄清楚 hook 入参、返回值和状态字段。

第二步:看 factory.py 的 hook 收集。

搜索:

text 复制代码
middleware_w_before_agent
middleware_w_before_model
middleware_w_after_model
middleware_w_after_agent
middleware_w_wrap_model_call
middleware_w_wrap_tool_call

这一段告诉你哪些 middleware 会参与哪些阶段。

第三步:看 wrapper 组合。

搜索:

text 复制代码
_chain_model_call_handlers
_chain_tool_call_wrappers

这一段是洋葱模型的核心。

第四步:看 graph node 和 edge。

搜索:

text 复制代码
graph.add_node
graph.add_edge
graph.add_conditional_edges

重点理解:

text 复制代码
entry_node
loop_entry_node
loop_exit_node
exit_node

第五步:看条件边。

搜索:

text 复制代码
_make_model_to_tools_edge
_make_tools_to_model_edge
_add_middleware_edge
_resolve_jump

这一段解释 Agent 什么时候继续调用工具、什么时候回模型、什么时候结束。

读 Middleware 源码不要从预置 middleware 开始,先看 types.py 的接口,再看 factory.py 的编排。

总结

最后用一张总图收束:

text 复制代码
create_agent(middleware=[m1, m2, m3])
  |
  v
检查 hook 覆写情况
  |
  +-- before_agent  -> StateGraph node: m1.before_agent -> m2.before_agent -> m3.before_agent
  |
  +-- before_model  -> StateGraph node: m1.before_model -> m2.before_model -> m3.before_model
  |
  +-- wrap_model    -> m1(m2(m3(model.invoke)))
  |
  +-- after_model   -> StateGraph node: m3.after_model -> m2.after_model -> m1.after_model
  |
  +-- wrap_tool     -> m1(m2(m3(tool.invoke)))
  |
  +-- after_agent   -> StateGraph node: m3.after_agent -> m2.after_agent -> m1.after_agent
  |
  v
添加 model/tools 节点和条件边
  |
  v
graph.compile(...)

记住三个结论:

  • Node-style hook 是图节点 :返回 dict 更新 state,配合 jump_to 和条件边控制流程。
  • Wrap-style hook 是洋葱链 :通过 handler(request) 控制模型或工具调用,可重试、短路、fallback、改写响应。
  • 异常默认传播:真正的重试和降级要写在 wrap hook 里,或使用 LangChain 预置 middleware。
相关推荐
江华森1 小时前
03-python-包与模块异常处理
python
呆呆敲代码的小Y1 小时前
AI Agent 实战:last30days-skill-cn 一键搜索中国 8 大平台,30 秒生成深度研究报告
人工智能
AI行业学习1 小时前
2026 版 Notepad++ 完整图文安装指南|官方渠道无捆绑,一键切换中文界面
开发语言·人工智能·python·html·notepad++
aneasystone本尊1 小时前
OpenMontage 快速入门
人工智能
诚信定制8391 小时前
2025年CSDN年度技术趋势预测:AI、云原生与开发者工具的未来
人工智能·云原生
Bode_20021 小时前
生成式界面设计案例
人工智能
炘东5921 小时前
求助:在另一个 Reasonix 窗口或后台运行
python
apcipot_rain1 小时前
计科八股2026705——正态分布、独立与不相关、范数、列表元组集合、链表队列、sort函数、URL执行
python·计算机网络·概率论
IT_陈寒1 小时前
Vue的v-if和v-show,我竟然用错了这么久
前端·人工智能·后端