LangChain:中间件
通过摘要、人工审核、调用限制、重试和自定义钩子管理 Agent 的运行过程。
正文目录
中间件在 Agent 的运行过程中加入可复用的处理策略。本章介绍摘要、人工审核、调用限制与重试,并说明自定义钩子的状态更新和执行顺序。
适用版本:LangChain 1.2.12。示例中的
model为已初始化的聊天模型,创建方式见第 2 章。
1. 中间件解决什么问题
中间件是在 Agent 执行关键位置挂载的处理逻辑。模型负责决策,工具负责执行,系统提示词描述规则;中间件则在运行过程中实施日志记录、请求调整、次数限制、重试和人工审核等策略。
如果把这些公共需求写进每个工具或 Agent 主流程,代码会重复且难维护。通过 create_agent(..., middleware=[...]) 统一挂载,可以复用策略,也便于观察策略在什么时机生效。
| 类别 | 代表能力 | 解决的问题 |
|---|---|---|
| 成本与资源控制 | 调用限制、摘要、上下文编辑 | 循环过长、费用过高、上下文过大 |
| 容错 | 模型回退、模型重试、工具重试 | 临时失败后如何恢复 |
| 审核与信息处理 | 人工审核、PII 检测 | 外部动作的确认、特定敏感信息处理 |
| 规划与工具选择 | 待办列表、工具筛选 | 长任务遗漏步骤、工具过多难以选择 |
| 执行能力扩展 | 文件搜索、Shell、文件系统、子 Agent | 提供环境操作与任务拆分能力 |
| 开发测试 | 工具模拟 | 在真实工具未就绪时验证流程 |
按实现来源,中间件可分为内置、自定义和提供商专属类型。Filesystem、Subagent 属于 Deep Agents 相关能力,使用时需确认对应安装包和导入路径;可用中间件也随版本变化。
2. 历史摘要 SummarizationMiddleware
达到阈值后,用摘要模型压缩较早历史,再保留最近一段原始消息。摘要会影响后续使用的消息状态,并不是仅把日志打印得更短。
| 参数 | 含义 |
|---|---|
model |
用于生成摘要的模型,可与主模型不同 |
trigger |
一个触发条件,或多个条件组成的列表;列表中任一条件满足即可触发 |
keep |
保留近期原始内容的预算,接收一种条件 |
token_counter |
Token 计数函数;近似计数与实际计费可能不同 |
summary_prompt |
自定义摘要要求,包含 {messages} 占位符 |
trim_tokens_to_summarize |
摘要调用自身处理历史的 Token 预算,注意与触发阈值配合 |
trigger 和 keep 的条件可按 ("tokens", 数量)、("messages", 数量)、("fraction", 比例) 表达。比例依赖模型 profile 中可靠的 max_input_tokens,不能把错误的上下文上限填进去只为消除报错。
from langchain.agents import create_agentfrom langchain.agents.middleware import SummarizationMiddlewarefrom langgraph.checkpoint.memory import InMemorySaver
agent = create_agent( model=model, middleware=[ SummarizationMiddleware( model=model, trigger=("messages", 20), keep=("messages", 6), summary_prompt=( "总结以下对话,保留已确认事实、用户要求和未完成事项。" "不确定内容仍标注为不确定。\n{messages}" ), ) ], checkpointer=InMemorySaver(),)config = {"configurable": {"thread_id": "summary-demo"}}20 条触发、保留 6 条只是演示参数。实际保留边界还需要维护工具调用与结果的完整关系。摘要也可能遗漏或扭曲信息,重要事实可另外结构化保存;是否节省费用,要同时考虑新增的摘要调用与后续输入减少量。
3. 人工审核 HumanInTheLoopMiddleware
它在指定工具执行前暂停图,返回待审核动作;用户作出决定后,从原检查点恢复。
| 决策 | 行为 |
|---|---|
approve |
按原参数执行 |
edit |
修改工具名称或参数后执行 |
reject |
拒绝该动作,并向 Agent 提供反馈 |
interrupt_on 按工具名设置策略:True 启用审核,False 不拦截,也可用字典指定 allowed_decisions 和 description。description_prefix 设置通用提示文字。
from langchain.agents import create_agentfrom langchain.agents.middleware import HumanInTheLoopMiddlewarefrom langchain.tools import toolfrom langgraph.checkpoint.memory import InMemorySaverfrom langgraph.types import Command
@tooldef preview_booking(city: str) -> str: """生成教学用的模拟预订结果,不进行真实预订。""" return f"模拟结果:目的地为{city}"
review_agent = create_agent( model=model, tools=[preview_booking], middleware=[HumanInTheLoopMiddleware( interrupt_on={ "preview_booking": { "allowed_decisions": ["approve", "edit", "reject"] } } )], checkpointer=InMemorySaver(),)review_config = {"configurable": {"thread_id": "review-demo"}}result = review_agent.invoke( {"messages": [{"role": "user", "content": "调用工具生成北京的模拟预订结果"}]}, config=review_config,)interrupts = result.get("__interrupt__", [])if interrupts: print(interrupts[0].value)下面是审核界面已收到用户同意之后的恢复示例,仅适用于当前中断恰好含一个待审核动作:
if interrupts: pending = interrupts[0].value["action_requests"] if len(pending) != 1: raise ValueError("请按待审核动作逐项收集决定") final_state = review_agent.invoke( Command(resume={"decisions": [{"type": "approve"}]}), config=review_config, )多动作的 decisions 必须与 action_requests 顺序对应;允许的选项来自对应审核配置。恢复需要同一 thread_id 和可用的 checkpointer,不能重新发送原问题来代替恢复。恢复后仍可能发生新的中断,调用方应继续检查。
4. PII 信息处理
PIIMiddleware 检测个人身份相关信息,可按内置类型(如 email、credit_card、ip、mac_address、url)或自定义检测器处理。
| 策略 | 处理效果 |
|---|---|
redact |
用类型标签替换命中内容 |
mask |
遮蔽部分内容 |
hash |
替换为散列表示,便于关联统计;不等于绝对匿名 |
block |
命中时抛出异常,阻断当前处理 |
from langchain.agents.middleware import PIIMiddleware
email_filter = PIIMiddleware( pii_type="email", strategy="redact", apply_to_input=True, apply_to_output=True, apply_to_tool_results=True,)三个 apply_to_* 决定检查输入、模型输出、工具结果中的哪些位置;仅处理最终输出不能阻止之前已发送给模型的数据。检测能力取决于类型、正则或自定义函数,不能把它理解为所有敏感信息都能自动识别。
5. 待办列表 TodoListMiddleware
该中间件提供 write_todos 工具,帮助 Agent 把大任务拆成步骤,并在状态中保存 todos。每项包含 content 和 status,常用状态为 pending、in_progress、completed。
from langchain.agents.middleware import TodoListMiddleware
planning_middleware = TodoListMiddleware()可以通过 system_prompt 调整规划规则,通过 tool_description 调整待办工具描述。待办工具的名称为 write_todos。
以修复程序错误为例,任务可拆分为:列出工作区文件 → 读取加法实现与测试 → 执行测试定位问题 → 修改实现 → 再运行测试 → 汇报结果。待办记录有助于跟踪进度,但“标记完成”仍是模型的状态更新,不能代替测试通过等真实证据;中间件也不会自动提供文件读写与测试工具。
6. 调用限制、回退与重试
6.1 限制模型和工具调用
| 中间件 | 关键参数 | 超限处理 |
|---|---|---|
ModelCallLimitMiddleware |
run_limit、thread_limit、exit_behavior |
结束当前运行或抛异常 |
ToolCallLimitMiddleware |
同上,可通过 tool_name 限制指定工具 |
continue、end、error |
run_limit 针对一次 Agent 运行;thread_limit 针对同一线程累计,需要保留相应状态。线程限制不是全站、全用户或进程级的统一配额。
from langchain.agents.middleware import ( ModelCallLimitMiddleware, ToolCallLimitMiddleware,)
limits = [ ModelCallLimitMiddleware(run_limit=8, exit_behavior="error"), ToolCallLimitMiddleware(run_limit=5, exit_behavior="continue"),]工具超限后 continue 的意思是把限制反馈给模型,让它决定后续步骤,超限工具不会因此照常执行;这也不保证模型立即停止,所以常配合模型次数或图步数上限。end 结束本次运行,不表示删除整段会话。
6.2 三种容错策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
ModelFallbackMiddleware |
主模型失败后尝试备用模型 | 提供商不可用或特定模型失败 |
ModelRetryMiddleware |
重试失败的模型请求 | 可恢复的超时、限流等 |
ToolRetryMiddleware |
重试失败的工具执行 | 工具依赖的临时故障 |
重试常关注 max_retries、initial_delay、backoff_factor、max_delay、jitter、retry_on 和失败后的行为。指数退避逐步拉长间隔,抖动让并发请求错开。实际等待时间还受重试序号、延迟上限和抖动设置影响,应结合参数配置与运行日志确认。
模型客户端本身可能已有重试,再叠中间件会增加总尝试次数。调用限制、重试与回退的嵌套顺序也会影响计数和实际 API 请求数,应通过 Trace 验证。对有副作用的工具,先明确幂等与去重方式,再决定是否重试。
6.3 工具筛选与模拟
LLMToolSelectorMiddleware 用一个模型从工具集合中挑选与问题相关的工具。model 指筛选模型,max_tools 控制模型筛选出的工具数量,always_include 指定始终包含的工具,这些工具不计入 max_tools。筛选数量不等于工具执行次数限制,也不能替代执行时的权限校验;筛选本身还会增加一次模型开销。
LLMToolEmulator 用模型模拟工具结果,适合验证流程或原型。它给出的天气、查询结果或执行结果都不是真实业务证据,不能用模拟结果验证真实工具是否正确。
7. 上下文编辑与执行扩展
ContextEditingMiddleware 可通过类似 ClearToolUsesEdit 的策略移除模型请求中较旧、较大的工具结果内容,同时保留必要的调用关系。重点区分:模型实际看到的消息可以变少,而持久状态中仍可保留原记录;检查请求或输入 Token 才能确认效果。
文件搜索类中间件利用 Glob 匹配路径、Grep 检索内容。Shell 类能力执行命令;Filesystem 提供文件操作;Subagent 用于拆分任务。接入这些能力时,需要根据对应版本配置安装包、执行环境、可访问目录与工具集合。
8. 自定义中间件的六个钩子
| 类型 | 钩子 | 时机与用途 |
|---|---|---|
| Node-style | before_agent |
一次运行开始时,初始化或检查 |
| Node-style | before_model |
每次模型调用前,验证、更新状态 |
| Node-style | after_model |
每次模型调用后,检查输出、更新状态 |
| Node-style | after_agent |
一次运行正常完成时,最终处理 |
| Wrap-style | wrap_model_call |
包裹模型请求,可改请求、重试、回退 |
| Wrap-style | wrap_tool_call |
包裹工具执行,可校验、捕获异常、调整执行 |
before_model / after_model 可能在一次 Agent 运行中执行多次。after_agent 不能当作异常时必然执行的 finally 清理钩子。
8.1 节点钩子:读状态,返回更新
节点钩子接收 state 与 runtime。返回 None 表示不提交状态更新;返回字典由图按照各字段的 reducer 合并。不要仅原地修改消息对象后返回 None 来表达持久更新。
from langchain.agents.middleware import before_modelfrom langchain.messages import AIMessage
@before_model(can_jump_to=["end"])def stop_on_explicit_command(state, runtime): last = state["messages"][-1] if last.type == "human" and last.content == "/stop": return { "messages": [AIMessage(content="已结束本次运行。")], "jump_to": "end", } return None跳转需用 can_jump_to 声明允许目标,再返回 jump_to。LangChain 1.2.12 中间件支持的目标为 "end"、"tools"、"model";这些公开值与底层图节点名称属于不同层次。跳到工具节点前,还需准备有效的工具调用消息。
8.2 包装钩子:接收请求与 handler
from langchain.agents.middleware import wrap_model_call
@wrap_model_calldef count_visible_messages(request, handler): print("本次模型请求的消息数:", len(request.messages)) response = handler(request) return responsehandler(request) 执行内层处理。调用一次是正常转发;多次调用可能产生多次真实请求;不调用则需要返回合法的替代结果,不能随意返回 None。
通过 request.override(...) 生成修改后的模型请求,可动态调整模型、消息和可见工具等;这类临时请求修改不等于提交 Agent 状态更新。工具包装器处理的是工具请求,其结果通常为 ToolMessage,也可能涉及更新状态的 Command,不能照搬模型响应的类型。
9. 装饰器、类与执行顺序
单个钩子、简单逻辑通常用装饰器;需要多个钩子共享配置、复用或提供同步/异步实现时,继承 AgentMiddleware 更清楚。二者底层都形成中间件对象,能力并非两套互不相通的机制。
from langchain.agents.middleware import AgentMiddleware
class MessageCountLogger(AgentMiddleware): def before_model(self, state, runtime): print("调用前:", len(state["messages"])) return None
def after_model(self, state, runtime): print("调用后:", len(state["messages"])) return None顺序取决于 middleware=[A, B, C] 的排列:
before_*:A → B → Cwrap_*:A 进入 → B 进入 → C 进入 → 实际调用 → C 返回 → B 返回 → A 返回after_*:C → B → A不同类别的钩子仍分别运行在各自生命周期位置,不能把整个中间件列表简单理解为“执行完 A 的所有方法才执行 B”。例如,放在摘要之前的输入处理会影响摘要模型能看到什么;重试包装器放在回退包装器外部或内部,也会改变每次失败后的行为。
10. 本章要点
- 中间件负责执行策略,工具负责具体能力;名称相近不代表职责相同。
- 节点钩子提交状态更新,包装钩子控制一次模型或工具请求。
- 摘要、裁剪、调用限制、重试分别控制不同问题,参数不能互相替代。
思考题
- 一次 Agent 调用中
before_model会运行几次? - 工具筛选能否限制调用次数?
- 为什么恢复人工审核要保留
thread_id?