创见博客
常用内置中间件实战:Summarization / HumanInTheLoop / PII / To-do
七崽爱吃小饼干2026/10/18阅读 1专栏 LangChain 开发实战

上一篇文章讲了中间件是什么、有哪些分类(见 demo-14)。这一篇挑出最常用的四个内置中间件,每个都动手跑一遍:摘要(Summarization)、人在环审批(Human-in-the-Loop)、隐私信息处理(PII)、任务清单(To-do)。它们的用法高度统一——import 进来,放进 createAgent 的 middleware 数组即可。

本文代码基于 JavaScript 版的 langchain v1,模型用 DeepSeek。

一、summarizationMiddleware:对话太长自动摘要

要解决的问题

对话越聊越长,迟早撑爆模型的上下文窗口,而且每多一轮就越贵。手动裁剪又容易把关键信息裁掉。

它做什么

当消息数 / Token 数超过阈值时,自动把较早的对话压成一段摘要,只保留最近几条原始消息,再接上新的对话。这样上下文长度被控制住,关键事实又不丢。

用法

js
import { createAgent, summarizationMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";

const agent = createAgent({
  model,
  tools: [],
  checkpointer: new MemorySaver(),   // 摘要要跨多轮累积消息,需要记忆
  middleware: [
    summarizationMiddleware({
      model,                     // 用哪个模型生成摘要
      trigger: { messages: 6 },  // 消息数超过 6 就触发
      keep: { messages: 2 },     // 摘要后保留最近 2 条
    }),
  ],
});

const config = { configurable: { thread_id: "demo" } };
const turns = [
  "我叫小明,是一名后端工程师。",
  "我最喜欢的语言是 Go。",
  "我正在做一个 LangChain 的学习项目。",
  "我养了一只叫豆豆的猫。",
];
for (const t of turns) {
  await agent.invoke({ messages: [{ role: "user", content: t }] }, config);
}

const res = await agent.invoke(
  { messages: [{ role: "user", content: "复述一下你记得关于我的信息。" }] },
  config
);

console.log(res.messages.map((m) => m.type).join(" -> "));
// human -> ai -> human -> ai   ← 历史已被压缩

跑 4 轮之后再看 state,会发现消息序列里多了一条 human 类型的摘要消息,内容以 Here is a summary of the conversation to date: 开头;之前几轮原始对话已经被它替换掉了。而模型在回答「你记得我什么」时,仍然能说出生日、职业、宠物等信息——因为摘要把事实保住了。

原理:历史是怎么被「摘要 + 替换」的

它的核心其实很朴素——把较早的历史交给大模型总结成一段话,再用这段话替换掉那部分历史。具体发生在 beforeModel 钩子里(即每次真正调用模型之前):

  1. 判断是否触发:先统计当前消息的 Token 数(默认按字符近似计数),看是否达到 trigger 阈值。没达到就直接放行,什么都不做。
  2. 切分成两段:把消息拆成「系统提示」和「对话消息」,再按 keep 算出一个安全切点——切点之前是要被摘要的历史,之后是原样保留的最近消息。(安全切点会避免把 AI(tool_calls) 和它对应的 Tool 消息从中间切开。)
  3. 调用大模型做摘要:把「待摘要」的那段历史按紧凑格式拼成文本(形如 Human: ... / AI: ... / Tool: ...),填进一个内置的摘要提示词(大意是「你即将超出上下文,请提取最重要、最相关的信息,只输出提取结果」),交给 model 参数指定的模型,生成一段摘要。
  4. 替换历史:返回一个状态更新:先清空原来的消息(RemoveMessage(REMOVE_ALL_MESSAGES)),再插入一条内容为 Here is a summary of the conversation to date:\n\n{摘要} 的 HumanMessage,最后接上第 2 步保留的最近消息。

所以最终的历史从「一长串原始对话」变成了「一段摘要 + 最近若干条原始消息」——这既压掉了 Token,又用摘要兜住了关键事实。做完替换后,模型才被真正调用,它看到的就是这份压缩后的历史。

两点补充:负责摘要的是 model 参数指定的模型,可以比主模型更便宜、更小;想换摘要风格或语言,可以用 summaryPrompt 覆盖内置提示词。

默认摘要提示词长什么样

summarizationMiddleware 内置的默认提示词(DEFAULT_SUMMARY_PROMPT)是下面这段英文,{messages} 会被替换成拼好的历史:

text
<role>
Context Extraction Assistant
</role>

<primary_objective>
Your sole objective in this task is to extract the highest quality/most relevant context from the conversation history below.
</primary_objective>

<objective_information>
You're nearing the total number of input tokens you can accept, so you must extract the highest quality/most relevant pieces of information from your conversation history.
This context will then overwrite the conversation history presented below. Because of this, ensure the context you extract is only the most important information to your overall goal.
</objective_information>

<instructions>
The conversation history below will be replaced with the context you extract in this step. Because of this, you must do your very best to extract and record all of the most important context from the conversation history.
You want to ensure that you don't repeat any actions you've already completed, so the context you extract from the conversation history should be focused on the most important information to your overall goal.
</instructions>

The user will message you with the full message history you'll be extracting context from, to then replace. Carefully read over it all, and think deeply about what information is most important to your overall goal that should be saved:

With all of this in mind, please carefully read over the entire conversation history, and extract the most important and relevant context to replace it so that you can free up space in the conversation history.
Respond ONLY with the extracted context. Do not include any additional information, or text before or after the extracted context.

<messages>
Messages to summarize:
{messages}
</messages>

几点说明:

  • {messages} 的内容:先用 getBufferString() 把待摘要消息拼成紧凑文本,形如 Human: ... / AI: ... / Tool: ...,再替换进去。
  • 调用模型:用 summarizationMiddleware({ model }) 指定的模型调用这段 prompt;返回值只取正文作为摘要。
  • 结果回落:摘要会被包成一条 HumanMessage,前缀默认是 Here is a summary of the conversation to date:,然后替换掉旧历史。
  • 可自定义:传 summaryPrompt 覆盖,注意必须保留 {messages} 占位符。

关键参数

参数作用
model生成摘要用的模型(实例或 "provider:model" 字符串)
trigger何时触发:{ messages: 6 } / { tokens: 4000 } / { fraction: 0.8 }
keep摘要后保留多少最近上下文:{ messages: 2 } / { tokens: ... } / { fraction: ... }
summaryPrompt自定义摘要提示词
tokenCounter自定义 Token 计数函数

trigger 还支持数组(任一满足即触发,OR)和对象的多个字段(同时满足,AND)。

注意:fraction 依赖模型的 profile 信息(maxInputTokens);不确定时用绝对的 tokens / messages 更稳。

二、humanInTheLoopMiddleware:敏感操作等人审批

要解决的问题

有些工具带副作用——发邮件、转账、删库——不能让 Agent 自己直接执行。

它做什么

在工具真正执行前暂停 Agent,把请求抛给外部;人做完「批准 / 修改 / 拒绝」的决定后,再把决定喂回去继续。

用法(三步)

第一步:创建时对指定工具开启中断。

js
import { createAgent, tool, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
import { z } from "zod";

const sendEmail = tool(
  ({ to, subject }) => `邮件已发送给 ${to},主题「${subject}」`,
  {
    name: "send_email",
    description: "发送邮件(敏感操作,需要人工审批)",
    schema: z.object({ to: z.string(), subject: z.string() }),
  }
);

const agent = createAgent({
  model,
  tools: [sendEmail],
  checkpointer: new MemorySaver(),   // HITL 必须挂 checkpointer
  middleware: [
    humanInTheLoopMiddleware({
      interruptOn: {
        send_email: { allowedDecisions: ["approve", "edit", "reject"] },
      },
    }),
  ],
});

第二步:调用后拿到中断请求。

js
const config = { configurable: { thread_id: "hitl" } };
const res = await agent.invoke(
  { messages: [{ role: "user", content: "请发邮件给 boss@example.com,主题:周报" }] },
  config
);

if (res.__interrupt__) {
  const request = res.__interrupt__[0].value;
  console.log(request.actionRequests[0].args);          // { to: "boss@example.com", subject: "周报" }
  console.log(request.reviewConfigs[0].allowedDecisions); // ["approve","edit","reject"]
}

第三步:用 Command 恢复。

js
import { Command } from "@langchain/langgraph";

const resumed = await agent.invoke(
  new Command({
    resume: {
      decisions: [
        {
          type: "edit",
          editedAction: {
            name: "send_email",
            args: { to: "boss@example.com", subject: "【修改后】周报" },
          },
        },
      ],
    },
  }),
  config
);

console.log(resumed.messages.find((m) => m.type === "tool").content);
// 邮件已发送给 boss@example.com,主题「【修改后】周报」

三种决定:

决定含义
{ type: "approve" }直接执行原调用
{ type: "edit", editedAction: { name, args } }改成新参数再执行
{ type: "reject", message }不执行,给人一句话回执

interruptOn 的值还可以是:true(等价于允许全部决定)、false(自动放行)。此外每个工具还能配置 description(自定义审批文案)和 when(动态判断是否中断,例如只拦 /etc 下的写操作)。

必须挂 checkpointer:中断处需要把状态存下来,等人工决定后再恢复。

三、piiMiddleware:隐私信息检测与脱敏

要解决的问题

用户可能把邮箱、卡号、IP 等隐私信息发给模型,存在合规风险。

它做什么

在把消息发给模型前检测 PII,并按策略处理。内置类型有 email、credit_card、ip、mac_address、url,也可以自定义正则或函数。

用法

js
import { createAgent, piiMiddleware } from "langchain";

const agent = createAgent({
  model,
  tools: [],
  middleware: [
    piiMiddleware("email", { strategy: "redact" }),
    piiMiddleware("credit_card", { strategy: "mask" }),
  ],
});

const res = await agent.invoke({
  messages: [
    {
      role: "user",
      content:
        "我的邮箱是 zhangsan@example.com,信用卡号是 4111 1111 1111 1111,请帮我看看。",
    },
  ],
});

console.log(res.messages[0].content);
// "我的邮箱是 [REDACTED_EMAIL],信用卡号是 ****-****-****-1111,请帮我看看。"

可以看到,发给模型前,状态里的用户消息已经被改写,模型根本看不到原文。

四种策略

策略效果
block检测到就抛 PIIDetectionError,直接拦下
redact替换成 [REDACTED_EMAIL] 这类占位符
mask部分打码,如 ****-****-****-1111(便于人工阅读)
hash替换成确定性哈希,如 <email_hash:a1b2c3d4>(可分析又不泄露原文)

自定义类型:

js
piiMiddleware("api_key", {
  detector: "sk-[a-zA-Z0-9]{32}",   // 正则字符串
  strategy: "block",
});

作用范围

参数默认作用
applyToInputtrue检查用户消息(发给模型前)
applyToOutputfalse检查 AI 消息(返回给用户前)
applyToToolResultsfalse检查工具返回的结果

四、todoListMiddleware:给 Agent 一个任务清单

要解决的问题

复杂多步任务里,Agent 容易「忘记自己做到哪了」。

它做什么

自动给 Agent 装上两样东西:

  1. 一个 write_todos 工具(接收一组 { content, status },status 为 pending / in_progress / completed);
  2. 一段引导它「先列清单、做完一项就标记」的系统提示词。

同时把清单同步到 Agent state 的 result.todos,方便展示进度。

用法

js
import { createAgent, todoListMiddleware } from "langchain";

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

const res = await agent.invoke({
  messages: [
    { role: "user", content: "帮我规划一个去北京的三日游,请把步骤拆成待办清单。" },
  ],
});

console.log(res.todos);
// [
//   { content: "确定出行日期、人数、预算和住宿偏好", status: "in_progress" },
//   { content: "预订往返交通(机票/高铁)", status: "pending" },
//   ...
// ]

从 res.messages 里能看到 Agent 主动调用了一个 write_todos 工具;res.todos 则保存了清单的当前状态。

参数

参数作用
systemPrompt自定义引导 Agent 使用清单的系统提示词
toolDescription自定义 write_todos 工具的描述

五、Python 对应写法

Python 侧的内置中间件名字一样,只是首字母大写、参数换成下划线命名。定义在 langchain.agents.middleware。

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

摘要:

python
agent = create_agent(
    model="gpt-5.5",
    tools=[],
    checkpointer=InMemorySaver(),
    middleware=[
        SummarizationMiddleware(
            model="gpt-5.4-mini",
            trigger=("messages", 6),   # 元组表示单个阈值;也可用 {"tokens": 4000, "messages": 10}
            keep=("messages", 2),
        ),
    ],
)

人在环审批:

python
from langgraph.types import Command

agent = create_agent(
    model="gpt-5.5",
    tools=[send_email],
    checkpointer=InMemorySaver(),
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
            }
        ),
    ],
)

result = agent.invoke({"messages": [...]}, {"configurable": {"thread_id": "hitl"}})
if result.get("__interrupt__"):
    agent.invoke(
        Command(resume={"decisions": [{
            "type": "edit",
            "edited_action": {"name": "send_email", "args": {"to": "...", "subject": "..."}},
        }]}),
        {"configurable": {"thread_id": "hitl"}},
    )

PII:

python
agent = create_agent(
    model="gpt-5.5",
    tools=[],
    middleware=[
        PIIMiddleware("email", strategy="redact", apply_to_input=True),
        PIIMiddleware("credit_card", strategy="mask", apply_to_input=True),
    ],
)

任务清单:

python
agent = create_agent(model="gpt-5.5", tools=[...], middleware=[TodoListMiddleware()])
result = agent.invoke({"messages": [{"role": "user", "content": "帮我规划..."}]})
print(result["todos"])

参数对应关系:

能力JSPython
摘要触发/保留trigger / keep(对象)trigger / keep(元组或字典)
审批配置interruptOninterrupt_on
审批决定字段editedActionedited_action
PII 作用范围applyToInput / applyToOutputapply_to_input / apply_to_output
恢复方式new Command({ resume })Command(resume=...)

六、小结

中间件一句话作用是否需 checkpointer
summarizationMiddleware对话超长时自动摘要,保住上下文不炸需要(跨轮累积)
humanInTheLoopMiddleware敏感工具执行前暂停,等人审批需要(中断处存状态)
piiMiddleware检测并脱敏邮箱 / 卡号等隐私信息不需要
todoListMiddleware给 Agent 一个任务清单工具,做多步规划不需要

通用要点:

  • 用法统一:import 进来,放进 createAgent({ middleware: [...] });
  • 多个中间件可以叠加,按注册顺序生效(before 正序、after 逆序、wrap 嵌套);
  • 依赖历史或中断的中间件(摘要、HITL)需要 checkpointer;
  • 默认值通常就很合理,按需覆盖即可。

把这几类中间件用起来,你的 Agent 就从「能跑」进化到了「可控、可观测、可合规」。

评论
0/100