AgentMiddleware is All You Need

AgentMiddleware is All You Need


业内对 Agent 的抽象已经存在三年了,现在社区中有众多 Agent 框架并采用了相似的核心概念------LangChain, Pydantic-AI, Agno, Google ADK, claude-agent-sdk。 但它们大部分都有相似的处境:在开发者需要精细控制 Agent 运行时,无法给开发者足够的掌控力,导致开发者遇到非简单场景时不得不舍弃框架这层抽象。

这篇文章是我对「如何搭好一个 Agent」的一份理解。


一、AI Workflow 与 ReAct Agent

生产环境里常见的有两种形态:

形态一:AI Workflows

LLM 被嵌入预设的固定流程,模型本身没有充足的自主性:

css 复制代码
用户问题
  → [程序 + LLM] 识别意图,命中预设场景
  → [程序] 执行该场景的固定流程
           (查员工信息 → 并行查业务单据 → 数据整合 → ...)
  → [LLM] 总结 + 话术引导

形态二:ReAct Agent

LLM 自主决策,工具按需调用:

css 复制代码
用户问题 → [LLM:决定调哪个工具] → [工具执行] → [LLM:决定下一步] → ...

两者的根本区别在于谁拥有控制流

AI Workflows 有它存在的充分理由:

  • 公司内部模型智力有限------业务数据敏感,通常无法调用外部闭源高阶模型
  • ReAct 一次回答需要 5+ 分钟(涉及 2+ 次 LLM 调用),延迟无法接受
  • 流程可控、可审计,符合合规要求

它不是糟粕,而是对现阶段模型能力的务实应对。但它的扩展性问题本质上是传统软件工程问题,不需要新的范式。

今天的生产环境大多是混合架构(Hybrid Agentic Workflow): 用 Workflow 把控全局框架和关键节点,在模糊决策处嵌入 Agent 做局部推理。 这既是当下的工程务实,也是通往纯 Agent 时代的必经之路------随着模型能力持续提升,Agent 的可靠性会逼近"确定性",固定编排的价值自然边际递减。

下文聚焦于 ReAct Agent。


二、ReAct Agent 的扩展性困境

一个 ReAct Agent 仅由几个简单的部分组成:

  • Model(大语言模型)
  • Prompt(模型的提示词)
  • Tools(一组供模型调用的工具)

其核心算法同样简单: 用户首先用某条输入消息调用 Agent,Agent 随即进入循环: 调用工具、将 Assistant 消息和 Tool 消息追加到其状态中,直到决定不再调用任何工具并最终结束。

graph TD A([request]) --> B(model) B -.->|action| C(tools) C -->|observation| B B -.-> D([result])

用 LangChain 写出来大概是这样:

python 复制代码
from langchain.agents import create_agent

# ① Model
model = ChatOpenAI(model="gpt-5")

# ② Prompt
system_prompt = "You are a helpful assistant."

# ③ Tools
@tool
def get_weather(location: str):
    """Call to get the weather from a specific location."""
    if any([city in location.lower() for city in ["sf", "san francisco"]]):
        return "It's sunny in San Francisco, but you better look out if you're a Gemini 😈."
    else:
        return f"I am not sure what the weather is in {location}"


backend_tools = [get_weather]

agent = create_agent(
    model=model,
    tools=backend_tools,
    system_prompt=system_prompt
)

与此同时,虽然搭建一个基础 agent 抽象并不难(几乎所有框架都做到了这一点),但要让这个抽象足够灵活以支撑生产环境,却并非易事。 形如 create_agent 的用法极简且优雅,但它对开发者几乎不透明------你难以干预它的内部运行机制。

以「让 Agent 调用前端工具」为例------前端可以注册一批在浏览器端执行的工具,Agent 调用它们时,逻辑运行在用户的浏览器里,可以读写 React 组件状态、触发 UI 动画、驱动 Generative UI 渲染。整个交互流程如下:

sequenceDiagram participant 前端 participant 后端 participant 模型 前端 ->> 后端: 上传前端工具定义(名称、参数 Schema) 后端 ->> 模型: 绑定后端工具 + 前端工具,发起对话 loop 推理循环 模型 ->> 后端: 决定调用后端工具 A 和前端工具 B 后端 ->> 后端: 执行后端工具 A 后端 -->> 前端: 中断执行,下发前端工具 B 的调用请求(含参数) 前端 ->> 前端: 在浏览器中执行前端工具 B 前端 ->> 后端: 返回前端工具 B 的执行结果 后端 ->> 模型: 汇总结果,继续推理 end 模型 ->> 后端: 生成最终回答 后端 -->> 前端: 返回结果

要让这张图跑起来,两件事缺一不可:调用 LLM 前,需要把前端注册的工具动态注入工具列表,让模型感知到它们的存在;工具调用阶段,需要识别哪些是前端工具,拦截并中断后端执行,把调用参数发回客户端,等浏览器执行完毕再继续。 两件事分属不同层次------模型输入处理、工具调用拦截------必须同时生效才能形成完整功能。

这不是 create_agent 能直接支持的------它的内部运行机制对开发者不透明,你无法在"调用 LLM 时"或"执行工具时"插入自定义逻辑。

这也不是前端工具独有的问题,根本原因在于:复杂 Agent 的上下文信息来自四面八方------开发者写的 system prompt、用户的输入、历史交互记录、工具调用的结果...... 这些信息共同构成了模型做决策的全部依据。这就是近期被频繁讨论的 context engineering:Agent 系统性能不佳,十有八九是因为模型拿到的上下文不对------缺了关键信息、加了噪音、时机不对。 LLM 无法读懂人心,你需要向它提供正确的信息,在正确的时机,以正确的形式。Garbage in, garbage out.

而 ReAct Agent 的扩展性困境,本质上就是 context engineering 的逻辑无处安放。为了让模型表现可靠,开发者需要对模型输入做精细的控制:

  1. 希望调整 agent 的"状态",使其不仅仅包含消息
  2. 希望更精细地控制究竟向模型发送什么内容
  3. 希望更精细地控制执行步骤的顺序

这些控制逻辑会蔓延并散落在 Agent 的各个层次,而那个简单的 agent 循环虽然适合入门,当你不断突破 agent 能力的边界时,就不得不修改它的各个部分。

于是不少开发者纷纷将 LangChain 框架改用更底层的 LangGraph,以框架抽象为代价换取更强的上下文掌控能力。

我在公司内至少看到两个应用将 LangChain 退回到 LangGraph。
注:langgraph 依赖 langchain-core, langchain 依赖 langgraph.

用 LangGraph 写出来的标准 Agent 大概是这样:

python 复制代码
from typing import Annotated
from typing_extensions import TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langchain_core.messages import ToolMessage


# ① State
class State(TypedDict):
    messages: Annotated[list, add_messages]

# ② Model node
def call_model(state: State):
    model_with_tools = model.bind_tools(backend_tools)
    return {"messages": [model_with_tools.invoke(state["messages"])]}

# ③ Tool node
def tool_node(state: State):
    result = []
    for tool_call in state["messages"][-1].tool_calls:
        tool = tools_by_name[tool_call["name"]]
        observation = tool.invoke(tool_call["args"])
        result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
    return {"messages": result}

# ④ 条件边
def should_continue(state: State):
    return "tools" if state["messages"][-1].tool_calls else END

# ⑤ Graph 装配
graph = StateGraph(State)
graph.add_node("model", call_model)
graph.add_node("tools", tool_node)
graph.add_conditional_edges("model", should_continue)
graph.add_edge("tools", "model")
app = graph.compile()

这个骨架本身没问题。问题出在往上加功能的时候。

同样以「让 Agent 调用前端工具」为例,在 LangGraph 里实现它,逻辑会散落在两处:

  • Model node :调用 LLM 前,把前端注册的工具动态注入工具列表(model.bind_tools(backend_tools + client_tools)
  • Tool node :识别前端工具,通过 interrupt() 暂停后端执行,把调用参数发回客户端,等浏览器执行完毕再继续

一个功能,散落在两个节点。每加一个这样的功能,model node 和 tool node 就各自膨胀一点,最终变成两坨谁都不敢动的代码。

类似的问题反复出现:

「Agent 自主任务规划」 :Agent 自主维护一份 todo list,追踪复杂任务的执行进度。 要实现它,需要在 State 里新增 todos 字段、在 model node 里注入 write_todos 工具和任务管理提示词、在 tool node 里处理 write_todos 的调用并把结果写回 State------三处都要改。

「高危操作人工确认」:Agent 在执行删除文件、执行 shell 命令等敏感操作前,需要暂停并等待人工审批。 要实现它,需要在 model node 之后、tool node 之前插入一段判断逻辑------识别本次 LLM 返回中是否有高危工具调用,若有则触发中断等待人工决策(通过、编辑参数或拒绝),再把结果交给 tool node 执行。这段逻辑同样游离在 Agent 骨架的各个节点之间。


三、答案:Middleware

LangChain 引入了 AgentMiddleware,解决的正是这个问题。

核心思想:一个功能涉及的所有层次(state、工具定义、模型输入处理、工具调用),全部封装在同一个 Middleware 类里,而不是散落在 graph 的各个角落。Agent 的骨架固定不变,功能通过 Middleware 进行模块化的插拔和组合,互不干扰。

六个钩子

AgentMiddleware 提供了六个钩子,覆盖 Agent 执行的全生命周期:

graph TD Req([request]) BA(before_agent) BM(before_model) WMC(wrap_model_call) AM(after_model) WTC(wrap_tool_call) AA(after_agent) Res([result]) Req --> BA BA --> BM BM --> WMC WMC --> AM AM -.-> WTC WTC --> BM AM -.-> AA AA --> Res

回到前端工具的例子

上一节里,「让 Agent 调用前端工具」在 LangGraph 里的逻辑散落在两处。用 Middleware 的思路,把它们全部收进一个类------ClientToolMiddleware

python 复制代码
import json
from contextvars import ContextVar
from dataclasses import dataclass
from typing import Any, Callable, Awaitable, TypedDict

from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
from langchain_core.tools import BaseTool, StructuredTool
from langgraph.prebuilt.tool_node import ToolCallRequest
from langgraph.types import Command, interrupt

_CLIENT_TOOL_CALL_INTERRUPT_KEY = "client_tool_call"
_client_tools_var: ContextVar[tuple[BaseTool, ...]] = ContextVar("client_tools", default=())


@dataclass
class Tool:
    name: str
    description: str
    inputSchema: dict


def set_client_tools(tools: list[Tool]) -> None:
    """前端调用,注册本次会话的客户端工具"""
    _client_tools_var.set(tuple(_to_langchain_tool(t) for t in tools))


def _to_langchain_tool(tool: Tool) -> BaseTool:
    """将 MCP Tool 定义转换为 LangChain StructuredTool"""

    def invoke_tool(**kwargs: Any) -> str:
        # 触发 interrupt,暂停后端执行,把调用请求下发给前端
        result = interrupt({_CLIENT_TOOL_CALL_INTERRUPT_KEY: {"name": tool.name, "args": kwargs}})
        return json.dumps(result, ensure_ascii=False) if not isinstance(result, str) else result

    async def ainvoke_tool(**kwargs: Any) -> str:
        result = interrupt({_CLIENT_TOOL_CALL_INTERRUPT_KEY: {"name": tool.name, "args": kwargs}})
        return json.dumps(result, ensure_ascii=False) if not isinstance(result, str) else result

    return StructuredTool.from_function(
        name=tool.name,
        description=tool.description or "",
        args_schema=tool.inputSchema,
        func=invoke_tool,
        coroutine=ainvoke_tool,
    )


class ClientToolMiddleware(AgentMiddleware):

    def wrap_model_call(self, request: ModelRequest, handler) -> ModelResponse:
        # ① 把前端工具合并进工具列表,让模型感知到它们的存在
        client_tools = _client_tools_var.get()
        if client_tools:
            request = request.override(tools=[*request.tools, *client_tools])
        return handler(request)

    async def awrap_model_call(self, request: ModelRequest, handler) -> ModelResponse:
        ...  # 此处省略 async 样板代码

    def wrap_tool_call(self, request: ToolCallRequest, handler) -> ToolMessage | Command:
        # ② 识别前端工具,调用时内部触发 interrupt 暂停,等待前端响应
        tool_name = request.tool_call.get("name", "")
        client_tool = next((t for t in _client_tools_var.get() if t.name == tool_name), None)
        if client_tool:
            return client_tool.invoke(request.tool_call, request.runtime.config)
        return handler(request)

    async def awrap_tool_call(self, request: ToolCallRequest, handler) -> ToolMessage | Command:
        ...  # 此处省略 async 样板代码

原本散落在 model node、tool node 两处的逻辑,现在全收进了一个类:

  • wrap_model_call:每次调用 LLM 前,把前端工具动态合并进工具列表
  • wrap_tool_call:每次工具调用时,识别前端工具并触发 interrupt() 暂停,等待前端响应

使用时,只需一行:

python 复制代码
agent = create_agent(model, tools=backend_tools, middleware=[ClientToolMiddleware()])

开箱即用的 Middleware

LangChain 官方提供了不少开箱即用的 Middleware:

Middleware 能力
SummarizationMiddleware 上下文超限自动压缩
ToolRetryMiddleware 工具调用失败自动重试
ModelRetryMiddleware LLM 调用失败重试
ModelFallbackMiddleware LLM 调用失败降级
HumanInTheLoopMiddleware 人工确认
PIIMiddleware PII 安全过滤
TodoListMiddleware Agent 任务规划
ShellToolMiddleware 终端执行
FilesystemFileSearchMiddleware 文件搜索

DeepAgents 是 LangChain 团队做的「Agent 开箱即用套件」,它的 create_deep_agent 本质上就是在 create_agent 之上预装了一套 Middleware 栈:

内置 Middleware 能力
TodoListMiddleware Agent 自主规划和追踪任务进度
FilesystemMiddleware 虚拟文件系统(ls/read/write/edit),防止上下文溢出
SummarizationMiddleware 上下文自动压缩
SubAgentMiddleware 通过 task 工具派生子 Agent
SkillsMiddleware 技能注入(可选)
HumanInTheLoopMiddleware 高危操作人工确认(可选)
MemoryMiddleware 跨会话长期记忆(可选)

Middleware 的执行顺序

当你挂载多个 Middleware 时,它们的执行顺序遵循一套直觉上很自然的规则------和 Express/Koa 的洋葱模型一致:

python 复制代码
agent = create_agent(
    model="gpt-4.1",
    tools=[...],
    middleware=[middleware1, middleware2, middleware3],
)
  • before_* 钩子:按注册顺序执行(1 → 2 → 3)
  • after_* 钩子:按注册逆序执行(3 → 2 → 1)
  • wrap_* 钩子:嵌套执行,像函数调用栈一样层层包裹
scss 复制代码
middleware1.wrap_model_call()
  → middleware2.wrap_model_call()
    → middleware3.wrap_model_call()
      → 实际调用模型
    ← middleware3
  ← middleware2
← middleware1

这意味着排在前面的 Middleware 拥有最外层的控制权------它最先看到请求,最后看到响应。 如果你需要某个 Middleware(比如 PIIMiddleware)作为最终的安全兜底,把它放在列表第一位就好。

实际使用中,大多数 Middleware 之间是正交的,顺序无所谓。 这也是 LangChain 官方推荐的最佳实践「Keep middleware focused - each should do one thing well」。


ReAct Agent 的骨架很简单,复杂的是骨架上长出来的那些能力。

Middleware 的价值在于:把「复杂」模块化,让每一块功能都可以独立开发、独立测试、自由组合。

Middleware 的思想不是 LangChain 专有的,你可以在任何框架的基础上参考这个设计自己实现一套。

相关推荐
LLM精进之路1 小时前
频域+特征融合:深度学习的黄金组合,顶会顶刊的快速通道
人工智能·计算机视觉·目标跟踪
大橙子打游戏1 小时前
我做了一个 A2A 协议的 Postman —— A2A-Forge 开源了
人工智能
L-影2 小时前
Agent中的ReAct:类型、作用与避坑指南(下篇)
人工智能·ai·react
itwangyang5202 小时前
AIDD-人工智能药物发现与设计-利用深度学习从头设计药物,实现逆转疾病相关转录表型
人工智能·深度学习
陈天伟教授2 小时前
人工智能应用- 预测新冠病毒传染性:07. 预测不同类型病毒的传播能力
人工智能·神经网络·安全·cnn·xss
初学者↑2 小时前
知识图谱可视化系统源码分享
人工智能·知识图谱
志栋智能2 小时前
告别高昂投入:超自动化IT运维的轻量化实践
大数据·运维·网络·人工智能·自动化
腾视科技TENSORTEC2 小时前
腾视科技TS-SG-SM7系列AI算力模组:32TOPS算力引擎,开启边缘智能新纪元
大数据·人工智能·科技·ai·ai算力模组·ai模组·ainas
红色石头本尊2 小时前
3-输出解析器outputParsers
人工智能