Skip to content

从零到进阶做 Agent(十)Agent 进阶:中间件

🕒 Published at:

上一篇的 Agent 已经会自主调工具了。但真实项目里,我们还想在它运行的关键节点插入自己的逻辑:调用模型前记日志、调用工具时做监控和权限校验、根据场景动态切换提示词、对话太长时自动摘要……这些「横切关注点」正是 中间件(middleware) 要解决的——它也是把一个 Agent 从「能跑」打磨到「可上生产」的核心机制。这一篇讲 LangChain 1.x 的 Agent 中间件。顶部可切换 Python / Node.js。

中间件是什么

回忆一下 Agent 的 ReAct 循环:模型思考 → 调用工具 → 拿到结果 → 再思考…。中间件让你在这个循环的每个关键点挂上钩子(hook),最常用的三个:

  • before_model / beforeModel:每次调用模型之前触发。适合记日志、裁剪历史、注入上下文。
  • wrap_tool_call / wrapToolCall包裹每次工具调用。适合监控、计时、鉴权,甚至拦截替换结果。
  • dynamic_prompt / dynamicSystemPrompt:每轮动态决定系统提示词。适合按用户身份或任务场景切换人设。

写法上,Python 用装饰器把普通函数变成中间件,Node 用 createMiddleware 传入对应的钩子函数;最后都通过 create_agent / createAgentmiddleware 参数挂上去。

一、日志与工具监控

先做两个最实用的:模型调用前打日志、每次工具调用时监控。

python
from langchain.agents import create_agent
from langchain.agents.middleware import before_model, wrap_tool_call
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
import os, time

@tool
def get_weather(city: str) -> str:
    """查询指定城市的天气。"""
    return f"{city}今天晴,26℃。"

# 钩子一:每次调模型前触发。参数 state 里有当前所有消息
@before_model
def log_before_model(state, runtime):
    print(f"[日志] 即将调用模型,当前消息数:{len(state['messages'])}")
    return None   # 返回 None 表示不修改状态,正常继续

# 钩子二:包裹工具调用。可在前后加逻辑,handler(request) 才是真正执行
@wrap_tool_call
def monitor_tool(request, handler):
    name = request.tool_call["name"]
    start = time.time()
    result = handler(request)          # 执行真正的工具
    print(f"[监控] 工具 {name} 耗时 {time.time() - start:.3f}s")
    return result

model = ChatOpenAI(
    model="qwen-plus",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    temperature=0,
)
agent = create_agent(
    model=model,
    tools=[get_weather],
    middleware=[log_before_model, monitor_tool],
)

agent.invoke({"messages": [{"role": "user", "content": "杭州天气怎么样?"}]})
javascript
import { createAgent, createMiddleware } from "langchain";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";

const getWeather = tool(async ({ city }) => `${city}今天晴,26℃。`, {
  name: "get_weather",
  description: "查询指定城市的天气。",
  schema: z.object({ city: z.string() }),
});

// 用 createMiddleware 一次性定义多个钩子
const observability = createMiddleware({
  name: "Observability",
  // 钩子一:每次调模型前触发,state 里有当前所有消息
  beforeModel: (state) => {
    console.log(`[日志] 即将调用模型,当前消息数:${state.messages.length}`);
    return undefined;   // 不修改状态,正常继续
  },
  // 钩子二:包裹工具调用,handler(request) 才是真正执行
  wrapToolCall: async (request, handler) => {
    const name = request.toolCall.name;
    const start = Date.now();
    const result = await handler(request);
    console.log(`[监控] 工具 ${name} 耗时 ${Date.now() - start}ms`);
    return result;
  },
});

const model = new ChatOpenAI({
  model: "qwen-plus",
  apiKey: process.env.DASHSCOPE_API_KEY,
  temperature: 0,
  configuration: { baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
});
const agent = createAgent({ model, tools: [getWeather], middleware: [observability] });

await agent.invoke({ messages: [{ role: "user", content: "杭州天气怎么样?" }] });

运行时你会在控制台看到日志和工具耗时打印出来——业务逻辑(工具)和横切逻辑(日志/监控)干净地分开了,这正是中间件的价值。

二、动态切换提示词

举个典型需求:平时是普通客服人设,但当用户要「生成报告」时,切换成专门的报告生成提示词。用 dynamic_prompt / dynamicSystemPromptMiddleware 可以每轮根据状态动态返回系统提示词。

python
from langchain.agents.middleware import dynamic_prompt

@dynamic_prompt
def switch_prompt(request):
    # 根据最近的用户消息判断场景,返回不同的系统提示词
    last = request.state["messages"][-1].content if request.state["messages"] else ""
    if "报告" in last:
        return "你是专业的报告撰写助手,用正式、结构化的语言输出。"
    return "你是亲切的生活助手,回答简短口语化。"

agent = create_agent(model=model, tools=[get_weather], middleware=[switch_prompt])
javascript
import { dynamicSystemPromptMiddleware } from "langchain";

const switchPrompt = dynamicSystemPromptMiddleware((state) => {
  const msgs = state.messages;
  const last = msgs.length ? String(msgs[msgs.length - 1].content) : "";
  if (last.includes("报告")) {
    return "你是专业的报告撰写助手,用正式、结构化的语言输出。";
  }
  return "你是亲切的生活助手,回答简短口语化。";
});

const agent = createAgent({ model, tools: [getWeather], middleware: [switchPrompt] });

同一个 Agent,会根据用户是在闲聊还是要报告,自动换上不同的「人格」——而工具和主流程完全不用改。

三、别忘了还有一堆「开箱即用」的中间件

很多常见需求,官方已经内置了中间件,直接用即可,不用自己写。下面挑四个最实用的详细说说(括号里分别是 Python 类名 / Node 函数名):

摘要压缩 SummarizationMiddleware / summarizationMiddleware

对话越滚越长,token 越烧越多,还可能撑爆模型的上下文窗口。这个中间件会监控历史长度,一旦接近设定的阈值,就自动把靠前的旧对话总结成一小段摘要,用摘要替换掉原始的冗长消息,从而在保留关键信息的同时大幅压缩上下文。它需要你传入一个用来做摘要的模型(可以就用主模型),并指定触发时机——最稳妥的是按消息条数绝对 token 数触发(按窗口比例触发需要模型提供上下文窗口信息,通义千问这类模型可能不带,会报错)。做长对话客服、长文档助手时几乎必备。

工具调用限流 ToolCallLimitMiddleware / toolCallLimitMiddleware

Agent 是自主决策的,万一它陷入「反复调同一个工具」的死循环,就会白白烧钱、甚至卡死。这个中间件统计工具调用次数并强制上限thread_limit 限制整个会话累计调用次数、run_limit 限制单次运行内的调用次数,还能用 tool_name 只针对某个特定工具限流。触及上限后的行为可配(继续、结束或报错)。它是给自主 Agent 上的一道「保险丝」。

敏感信息处理 PIIMiddleware / piiMiddleware

用于检测并处理个人隐私信息(PII),比如邮箱、信用卡号、IP、URL 等。你为想防护的每种类型各挂一个,并选择处理策略:redact(替换成占位符,默认)、mask(部分打码,如只留后四位)、hash(换成哈希值)、或 block(直接拦截阻止)。默认作用于用户输入,也可配置成同时处理模型输出或工具返回结果。合规、脱敏场景用得上。

人工确认 HumanInTheLoopMiddleware / humanInTheLoopMiddleware

有些操作风险高(下单、退款、删数据),不该让 Agent 自己拍板。这个中间件让你指定哪些工具在执行前必须「暂停、等人工批准」:通过 interrupt_on 声明需要拦截的工具,命中时 Agent 会中断并抛出待确认请求,由你的程序(或人)决定放行、修改还是拒绝,再继续往下跑。这就是所谓「human-in-the-loop(人在回路)」,是高风险 Agent 的安全闸门。

下面把四个一次性挂上,看清每个怎么配、参数是什么意思。注意 pii为每种敏感类型各挂一个human_in_the_loop 想「暂停后还能恢复」需要配合 checkpointer(第五篇讲过):

python
from langchain.agents import create_agent
from langchain.agents.middleware import (
    SummarizationMiddleware, ToolCallLimitMiddleware,
    PIIMiddleware, HumanInTheLoopMiddleware,
)
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
    model=model,
    tools=[get_weather, delete_order],
    middleware=[
        # ① 对话超过 20 条消息就自动摘要压缩(也可用 ("tokens", 3000) 按 token 触发)
        SummarizationMiddleware(model=model, trigger=("messages", 20)),

        # ② 整个会话最多调 10 次工具、单次运行最多 5 次;超了就停,防止死循环烧钱
        ToolCallLimitMiddleware(thread_limit=10, run_limit=5),

        # ③ 把用户输入里的邮箱替换成占位符;信用卡号只留后四位(每种类型挂一个)
        PIIMiddleware("email", strategy="redact"),
        PIIMiddleware("credit_card", strategy="mask"),

        # ④ delete_order 这个危险工具,执行前必须暂停等人工批准
        HumanInTheLoopMiddleware(interrupt_on={"delete_order": True}),
    ],
    checkpointer=InMemorySaver(),   # ④ 需要它来保存「暂停点」以便之后恢复
)
javascript
import { createAgent,
  summarizationMiddleware, toolCallLimitMiddleware,
  piiMiddleware, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";

const agent = createAgent({
  model,
  tools: [getWeather, deleteOrder],
  middleware: [
    // ① 对话超过 20 条消息就自动摘要压缩(也可用 { model, tokens: 3000 } 按 token 触发)
    summarizationMiddleware({ model, messages: 20 }),

    // ② 整个会话最多调 10 次工具、单次运行最多 5 次;超了就停,防止死循环烧钱
    toolCallLimitMiddleware({ threadLimit: 10, runLimit: 5 }),

    // ③ 把用户输入里的邮箱替换成占位符;信用卡号只留后四位(每种类型挂一个)
    piiMiddleware("email", { strategy: "redact" }),
    piiMiddleware("credit_card", { strategy: "mask" }),

    // ④ deleteOrder 这个危险工具,执行前必须暂停等人工批准
    humanInTheLoopMiddleware({ interruptOn: { delete_order: true } }),
  ],
  checkpointer: new MemorySaver(),   // ④ 需要它来保存「暂停点」以便之后恢复
});

配好之后的实际效果串起来看:用户发来一句带邮箱的长消息 → ③ PII 先把邮箱抹成占位符 → 对话如果太长,① 摘要把旧消息压缩掉 → Agent 开始干活,② 限流盯着别让它调工具调疯 → 如果它想调用 delete_order④ 人工确认会把流程暂停、抛出一个待批准请求,你的程序确认后再继续。四道关卡各司其职,业务代码一行没动。

各中间件的参数以官方文档为准,不同版本可能微调。思路是:先找有没有现成的,没有再自己写。

小结与预告

这一篇你掌握了 Agent 的「进阶控制」:用 before_model 记日志、wrap_tool_call 监控和拦截工具、dynamic_prompt 动态切换人设,还认识了一批开箱即用的官方中间件。这些正是把一个「能跑」的 Agent 打磨成「可上生产」的关键。

至此,RAG 和 Agent 两条线的零件都齐了。下一篇是综合实战:我们把 RAG(查知识库)、多工具(查天气/定位/取数据)、中间件(日志/报告切换)全部整合,做一个「智扫通机器人智能客服」——一个真正麻雀虽小五脏俱全的 Agent 项目。