1 中间件概述
-
在
create_agent()的底层运行机制中,有几个重要的组件,分别是:- 模型(Model) :Agent 的"大脑",负责理解任务与决策推理。
- 工具(Tools) :Agent 的"手脚",执行模型自己做不到的外部操作。
- 系统提示词(System Prompt) :Agent的"角色",告诉模型该怎么想、参考什么上下文。
- 中间件(Middleware) :Agent的"中枢",在执行流程的关键节点进行拦截、控制和增强。
-
声明如下:
*pythonfrom langchain.agents import create_agent from langchain.agents.middleware import SummarizationMiddleware, HumanInTheLoopMiddleware agent = create_agent( model="gpt-5.4-mini", tools=[...], middleware=[ SummarizationMiddleware(...), HumanInTheLoopMiddleware(...) ], )
1.1 什么是中间件
- Middleware(中间件),简单说就是Agent 执行过程中的钩子函数,是 LangChain 1.x 的"王牌"工程化能力。
- 钩子是框架或系统在某些关键执行点暴露的扩展接口。开发者可以"挂上"自己的逻辑,在那些点上插入、修改或替换行为,而无需改变主流程代码。就像在流水线上某个环节设置了一个"检查点"或"插入器"。
- 借助中间件,开发者可以高度定制和控制Agent运行的每一个环节,是处理 Agent 生命周期的标准方式。
- 在 LangChain 的 Agent 执行循环中,比如 "模型调用前"、"模型调用后"、"工具调用前后" 设置一些钩子(hooks),让你在不改 Agent 主体逻辑的情况下实现策略与治理。
1.2 为什么需要中间件
- 如果没有中间件,Agent 的执行流程通常比较直接:
text
用户输入 → 拼接提示词/消息 → 调用模型 → 如有需要调用工具 → 返回结果
- 这种方式对于简单场景已经足够,但一旦进入真实项目,往往会遇到很多额外需求,例如:
- 想根据问题复杂度动态切换模型
- 想限制某些用户只能调用部分工具
- 想在工具报错时自动重试或返回兜底结果
- 想在模型调用前插入额外的系统提示
- 想记录每一步的执行日志,方便排查问题
- 想在敏感信息出现时阻断执行
- 想在正式执行工具前增加人工审批
- 这些需求有一个共同特点:它们不是 Agent 的核心业务逻辑,但又会影响 Agent 的执行过程。如果把这些逻辑全部直接写进主流程,会带来几个问题:
- 主流程会迅速变乱:Agent 本身只需要关心"理解用户需求、决定是否调用工具、生成结果",但一旦把日志、鉴权、重试、风控、审计都塞进去,主逻辑就会变得臃肿。
- 很多逻辑是横切需求,难以复用:例如日志、重试、风控、权限控制,通常不是某一个 Agent 独有的,而是多个 Agent 都需要。如果直接写死在每个 Agent 里,会产生大量重复代码。
- 流程控制粒度不够细:有些逻辑必须发生在"模型调用前",有些要发生在"工具调用后",如果没有统一的执行拦截点,开发者只能手动改主流程,既麻烦又容易出错。
- 后期维护成本高:当你需要增加一个新规则,例如"所有外部工具调用前都先做审计",如果系统没有中间件机制,往往需要修改很多处代码。
- 总结
- 中间件的价值就在于把这些与业务无关、但与执行过程强相关的横切逻辑,从 Agent 主流程中分离出来。让Agent 主体代码聚焦业务,而借助中间件,实现" 拦截流程、修改流程、增强流程"。
1.3 中间件分类
- 根据LangChain是否已经定义了来分类:
- 自定义中间件:允许开发者自定义,从而实现更加灵活的Agent行为管理
- 内置中间件:LangChain实现并提供的
- 模型供应商定制的中间件:依赖于特定模型服务的实现
- 和模型供应商无关的中间件。LangChain提供的与供应商无关的中间件链接
1.4 和模型供应商无关的内置中间件分类
- LangChain提供的和模型供应商无关的内置中间件分为六个类别
1.4.1 成本与资源控制类
- 核心目标:控成本、控配额、避免无限调用
- 这类中间件主要解决" Agent太贵、太能跑、停不下来"的问题。
- 包含
- Model call limit:限制模型调用次数,防止一次任务反复请求 LLM,导致费用失控
- Tool call limit:限制工具调用次数,避免 Agent 无限试错、死循环调工具
- Summarization:在上下文快满时自动总结历史,减少 token 消耗
- Context editing:裁剪上下文、清理工具调用痕迹,本质上也是为了节省上下文成本
- 业务场景理解
- 适合生产环境的成本治理、配额治理、长会话优化、SaaS 产品控费。
1.4.2 稳定性与容错保障类
- 核心目标:保证服务不中断、失败后尽量自动恢复
- 这类中间件主要解决" 调用失败怎么办、模型挂了怎么办、工具超时怎么办"。
- 包含
- Model fallback:主模型失败时切换备用模型
- Model retry:模型调用失败后自动重试
- Tool retry:工具调用失败后自动重试
- 业务场景理解
- 适合线上生产系统,尤其是多模型、多工具依赖的 Agent。
- 本质上是在做 高可用、容灾、鲁棒性建设。
1.4.3 安全与合规风控类
- 核心目标:让 Agent 可控、可审、合规
- 这类中间件主要解决" Agent乱执行、泄露敏感信息、做危险操作"的问题。
- 包含
- Human-in-the-loop:在关键工具调用前暂停,等人工审批
- PII detection:检测和处理个人敏感信息
- Model call limit / Tool call limit:某种意义上也可归到风控,因为它能防止异常滥用
- 业务场景理解
- 适合企业内部系统、客服系统、审批流、数据查询类 Agent。
- 尤其是涉及:发邮件、调数据库、调财务/人事系统、导出敏感信息、执行外部动作等
1.4.4 决策增强与智能编排类
- 核心目标:提升 Agent 的决策质量和任务拆解能力
- 这类中间件主要解决" Agent不够聪明、不会规划、不会先筛工具"的问题。
- 包含
- To-do list:给 Agent 增加任务规划、分步骤执行和状态跟踪能力
- LLM tool selector:当工具太多时,用子模型筛选最相关的几个工具交给主模型
- Subagent:允许生成子Agent,把复杂任务拆给不同角色处理
- 业务场景理解
- 适合复杂任务流,比如:研究型 Agent、多步骤分析、报告生成、多角色协作、长链路任务编排等。
- 这类本质上是在增强 Agent的"脑子"与"组织能力"。
1.4.5 执行能力扩展类
- 核心目标:给 Agent 更多"手脚"
- 这类中间件主要解决" Agent只能聊天,不能真正操作环境"的问题。
- 包含
- Shell tool:给 Agent 持久 shell,会执行命令
- File search:给 Agent 文件搜索能力,能做 Glob/Grep
- Filesystem:给 Agent 文件系统读写与长期存储能力
- 业务场景理解
- 适合工程 Agent、代码 Agent、本地自动化 Agent、运维 Agent。
- 本质上是把 Agent 从"纯推理"扩展成"能操作环境的执行体"。
1.4.6 开发调试与测试辅助类
- 核心目标:方便开发、测试、验证 Agent 行为
- 这类中间件主要不是直接服务业务,而是服务于研发和调试阶段。
- 包含
- LLM tool emulator:用 LLM 模拟工具执行,便于测试(最典型)
- Summarization:有时也可辅助调试长会话表现
- Context editing:可用于测试上下文裁剪效果
- Human-in-the-loop:也常用于调试高风险步骤
- 业务场景理解
- 适合开发阶段快速验证流程、做 mock、减少真实工具依赖。
2 常用内置中间件的使用
- 先介绍最为常用的几种中间件
2.1 SummarizationMiddleware中间件
- 作用:对历史消息列表进行摘要&总结,达到压缩上下文的效果。
- 原理:在达到触发条件时,调用大模型对历史消息进行摘要,将摘要的结果作为HumanMessage ,放到消息列表最开始的位置。
2.1.1 参数说明
model: 用于摘要的模型- 可以是模型名称也可以是模型对象,如果传递的是模型名称,底层会调用
init_chat_model初始化模型。
- 可以是模型名称也可以是模型对象,如果传递的是模型名称,底层会调用
trigger:摘要触发条件- 是一个列表,每个元素对应一个条件,当任一条件满足时,触发摘要。
tokens:token的数量,历史token的累计数量达到该值触发摘要。messages:历史消息数量,历史消息条数达到该值触发摘要。fraction:上下文长度比例。历史token的累计数量达到模型的max_input_tokens * fraction触发摘要- 如果条件包含
fraction,要求模型的profile包含max_input_tokens,Deepseek模型的profile为空,此时需要手动添加该配置项。Deepseek-V3.2的上下文长度为128K。
keep:摘要时保留的原始消息- 支持三种条件,但和trigger不同,keep同一时间只接收一种条件。
tokens:摘要时保留的token数量。messages:摘要时保留的历史消息条数。fraction:摘要时保留max_input_tokens*fraction个token。
token_counter:统计token数量的函数- 默认使用LangChain提供的
count_tokens_approximately,一般不用更改。 - 对于纯文本消息,该函数的大致思路是先统计消息的字符数,也就是
len(字符串),然后再除以每个token大致的字符数,转换为粗略的token数,再加一些额外开销。作为估算的token数。
- 默认使用LangChain提供的
summary_prompt:摘要时的自定义提示词- 该提示词需要包含{messages} 占位符,使得历史消息列表可以被插入。不指定则使用内置提示词。
trim_token_to_summarize:摘要时历史消息的最大token数- 如果历史消息token数大于该值,则会被裁剪。默认为" 4000 "。
- 如果
trigger用token作为度量,调大触发阈值时,当前配置项应相应调整,否则会丢失信息。
2.1.2 示例1
-
示例代码
*pythonfrom langchain.agents import create_agent from langchain.chat_models import init_chat_model from dotenv import load_dotenv from langchain.agents.middleware import SummarizationMiddleware from langchain.messages import SystemMessage, HumanMessage, AIMessage import os load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", profile={"max_input_tokens": 128000}, api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) messages = [ SystemMessage("你是个非常有好的AI助手"), HumanMessage("你好啊,我是老王,你是谁?"), AIMessage("你好老王,我是小王"), HumanMessage("好的小王,很高兴认识你"), AIMessage("你高兴得太早了"), HumanMessage("呵呵,你什么意思,你是谁?") ] agent = create_agent( model="deepseek-v4-pro", middleware=[ SummarizationMiddleware( model=model, trigger=[ ("tokens", 100), ("messages", 6), ("fraction", 0.001) ], keep=("messages", 2) ) ] ) response = agent.invoke({"messages": messages}) for msg in response["messages"]: msg.pretty_print() -
打印结果
*================================ Human Message ================================= Here is a summary of the conversation to date: ## SESSION INTENT 用户(老王)与 AI(小王)进行了一次简单的问候与自我介绍,无具体任务目标。 ## SUMMARY - 用户自称"老王",向 AI 问好并询问身份。 - AI 自我介绍为"小王"。 - 用户表示很高兴认识小王。 - 对话仅为寒暄,无实质任务、决策或待解决问题。 ## ARTIFACTS None ## NEXT STEPS None ================================== Ai Message ================================== 你高兴得太早了 ================================ Human Message ================================= 呵呵,你什么意思,你是谁? ================================== Ai Message ================================== 老王,别误会,刚刚那句"你高兴得太早了"是开个玩笑,意思是咱们才刚认识,以后日子还长,说不定你会觉得我啰嗦或者帮倒忙呢,可别高兴太早。没有别的意思。 我是小王啊,你的AI助手,刚才咱们不是互相介绍过了嘛。你叫我小王就行,有什么需要我帮忙的尽管说。 进程已结束,退出代码为 0
2.1.3 示例2
-
示例代码。修改示例1中create_agent中的调用
*pythonagent = create_agent( model="deepseek-v4-pro", middleware=[ SummarizationMiddleware( model=model, trigger=[ ("tokens", 100), ("messages", 6), ("fraction", 0.001) ], keep=("messages", 2), summary_prompt="对历史消息摘要,消息列表如下\n{messages}" ) ] ) -
打印结果
*================================ Human Message ================================= Here is a summary of the conversation to date: 老王与AI助手小王初次打招呼,互相自我介绍,并表达很高兴认识对方。 ================================== Ai Message ================================== 你高兴得太早了 ================================ Human Message ================================= 呵呵,你什么意思,你是谁? ================================== Ai Message ================================== 哈哈,别介意,刚才就是开个玩笑,活跃下气氛。我是小王,你的AI助手,很高兴认识你,老王!以后有啥需要帮忙的,尽管开口。 进程已结束,退出代码为 0
2.2 HumanInTheLoopMiddleware中间件
- HumanInTheLoopMiddleware(人在环中间件、人工审核中间件)在工具调用前中断Agent运行,等待用户对工具调用请求决策。可选的决策有:approve(同意执行)、edit(编辑调用配置后执行)、reject(拒绝执行)。
2.2.1 参数说明
interrupt_on:工具名和中断策略的映射- 策略可以是
True、False或InterruptOnConfig对象,精细控制决策选项。 True表示所有决策(approve, edit, reject) 都可以选择。False表示不中断,即无需审批即可执行。InterruptOnConfig是一个TypedDict的子类,可以用字典直接赋值。支持的Key有:allowed_decisions:精细控制中断后允许的决策。description:特定工具的中断描述信息,优先级高于description_prefix,后者会更改所有工具中断的描述。
- 策略可以是
description_prefix:自定义中断描述信息。- 默认为"Tool execution requires approval"
2.2.2 举例1 调用前中断
-
注意:本例需要从中断的位置让Agent继续运行,这就需要用到短期记忆,这里先使用即可。
-
创建Agent时通过
checkpointer参数启用了短期记忆,在调用时通过传递相同的config加载记忆。记住固定用法即可。 -
示例代码
*pythonfrom langchain.chat_models import init_chat_model from dotenv import load_dotenv import os from langchain.agents import create_agent from langchain.agents.middleware import HumanInTheLoopMiddleware from langgraph.checkpoint.memory import InMemorySaver from langchain.messages import HumanMessage from langchain.tools import tool from rich import print as rprint load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", profile={"max_input_tokens": 128000}, api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) @tool def get_weather(city: str, is_forcast: bool = False) -> str: """ 查询指定城市天气 Args: city: 城市名称 is_forcast: 是否包含明日天气预报? """ res = f"{city}今天天气不错" if is_forcast: res += "\n明天下雨" return res @tool def get_news() -> str: """ 查询当日新闻 """ return "中方三艘油轮通过霍尔木兹海峡" @tool def read_email_tool(email_id: str) -> str: """通过邮件ID读取内容的伪函数""" return f"邮件ID:{email_id}\n是空的" @tool def send_email_tool(recipient: str, subject: str, body: str) -> str: """发送邮件伪函数""" print(">>> 真的执行发送邮件工具了") return f"发送给 {recipient} 的邮件标题是:{subject},内容:{body}" agent = create_agent( model=model, tools=[get_weather, get_news, read_email_tool, send_email_tool], checkpointer=InMemorySaver(), middleware=[ HumanInTheLoopMiddleware( interrupt_on={ "get_weather": True, "get_news": True, "read_email_tool": False, "send_email_tool": { "allowed_decisions": ["approve", "reject"], "description": "发送邮件中断啦" }, }, description_prefix="中断啦" ), ], ) config = {"configurable": {"thread_id": "1"}} # 第一次调用:会暂停在发送邮件前 response = agent.invoke( { "messages": [ HumanMessage(content="请帮我查询今天北京的天气" "查询今日新闻" "查看ID为 'sk2131421' 的邮件内容," "向15641685664@qq.com发送邮件,标题是'哈哈哈', 内容是:'你好啊'" "同时做这四件事") ] }, config=config, ) print("========= 原始响应 =========") rprint(response)
2.2.3 举例2 指明工具调用请求决策
-
示例1代码下添加以下代码
*pythoninterrupts = response.get("__interrupt__", []) action_requests = interrupts[0].value["action_requests"] # 如果有中断,说明进入人在环了 weather_decision = { "type": "edit", "edited_action": { "name": "get_weather", "args": {"city": "中国北京市", "is_forcast": True} } } news_decision = { "type": "reject", } send_email_decision = { "type": "approve" } decisions = { "decisions": [] } # 决策的顺序必须和返回的中断请求顺序一致 for action_request in action_requests: if action_request["name"] == "get_weather": decisions["decisions"].append(weather_decision) if action_request["name"] == "get_news": decisions["decisions"].append(news_decision) if action_request["name"] == "send_email_tool": decisions["decisions"].append(send_email_decision) if interrupts: # 审批通过 resumed_response = agent.invoke( Command(resume=decisions), config=config, # 必须是同一个 thread_id ) print("==== 审批后继续执行 ====") for msg in resumed_response["messages"]: msg.pretty_print() -
打印
*python>>> 真的执行发送邮件工具了 ==== 审批后继续执行 ==== ================================ Human Message ================================= 请帮我查询今天北京的天气查询今日新闻查看ID为 'sk2131421' 的邮件内容,向15641685664@qq.com发送邮件,标题是'哈哈哈', 内容是:'你好啊'同时做这四件事 ================================== Ai Message ================================== Tool Calls: get_weather (call_00_AvRc0CBpHj0pTelZBUF57698) Call ID: call_00_AvRc0CBpHj0pTelZBUF57698 Args: city: 中国北京市 is_forcast: True get_news (call_01_8yNVqhtzz9vLTIUhu2Qf7711) Call ID: call_01_8yNVqhtzz9vLTIUhu2Qf7711 Args: read_email_tool (call_02_tn0AgXz1JfDMQHBbMHsL5747) Call ID: call_02_tn0AgXz1JfDMQHBbMHsL5747 Args: email_id: sk2131421 send_email_tool (call_03_e3fGZA8FBinCcQUjLoRc2978) Call ID: call_03_e3fGZA8FBinCcQUjLoRc2978 Args: recipient: 15641685664@qq.com subject: 哈哈哈 body: 你好啊 ================================= Tool Message ================================= Name: get_news User rejected the tool call for `get_news` with id call_01_8yNVqhtzz9vLTIUhu2Qf7711 ================================= Tool Message ================================= Name: get_weather 中国北京市今天天气不错 明天下雨 ================================= Tool Message ================================= Name: read_email_tool 邮件ID:sk2131421 是空的 ================================= Tool Message ================================= Name: send_email_tool 发送给 15641685664@qq.com 的邮件标题是:哈哈哈,内容:你好啊 ================================== Ai Message ================================== 我已经为您同时处理了这四件事,结果如下: 1. **北京天气** ✅ 今天天气不错;明日天气预报显示明天下雨。 2. **今日新闻** ❌ 该请求被拒绝(用户拒绝了 `get_news` 工具调用),未能获取新闻内容。 3. **邮件内容(ID: sk2131421)** ✅ 该邮件是空的,没有任何内容。 4. **发送邮件** ✅ 已向 `15641685664@qq.com` 发送邮件,标题为「哈哈哈」,内容为「你好啊」。 需要我重新尝试查询今日新闻吗? 进程已结束,退出代码为 0
2.3 PIIMiddleware中间件
- 敏感信息保护。
- PII中间件用于检测和处理对话中的个人身份信息(Personally Identifiable Information,PII),支持自定义处理策略。
2.3.1 参数说明
pii_type:检测的PII数据类型- 可以是内置类型或自定义类型,自定义类型有
email:电子邮箱地址credit_card:信用卡号url:网址mac_address:设备MAC地址ip:IP地址
strategy:处理PII信息的策略- 支持四种选项
redact:将检测到的PII信息用字符串[REDACTED_[PII_TYPE]]替换,其中的PII_TYPE是上面提到的具体类型,比如[REDACTED_EMAIL]、[REDACTED_CREDIT_CARD]这样的标签。完全"擦除/隐藏" 真实内容。适合日志清洗、合规需求、公开输出时隐藏敏感内容。mask:用***将PII信息的前面一部分信息遮蔽。比如信用卡号可能变成****-****-****-1234(只保留最后几位/部分可见),邮箱可能保留域名部分 + 隐藏用户名的一部分等 --- 既隐藏大部分敏感信息,又保留了一点"可辨识性"(比如账号后四位、域名等),适合用户服务界面/ 前端显示 / 需要部分可识别但不泄露完整敏感内容的场景。hash:用检测到的PII信息的哈希值替代原值。比如<email_hash:a1b2c3d4>。适合analytics、调试 (debug)、统计分析、匿名追踪等场景。block:如果检测到PII信息,直接抛出异常。适合对隐私要求极高、绝不允许泄露任何敏感信息的场景。
detector:自定义PII检测函数或者正则表达式apply_to_input:是否在调用模型前检测。默认为True。apply_to_output:是否在模型调用后检测。默认为False。apply_to_tool_results:是否在工具调用后检测其输出。默认为False。- 通常我们只在模型调用前检测。因为PII检测的主要目的是避免将敏感信息发送给模型服务导致信息泄露。
2.3.2 示例1 使用内置检测器
-
示例代码
*pythonfrom langchain.chat_models import init_chat_model from dotenv import load_dotenv from langchain.agents import create_agent from langchain.agents.middleware import PIIMiddleware from langchain.messages import HumanMessage import os load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", profile={"max_input_tokens": 128000}, api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) agent = create_agent( model=model, tools=[], middleware=[ PIIMiddleware("email", strategy="redact", apply_to_input=True), PIIMiddleware("credit_card", strategy="mask", apply_to_input=True), PIIMiddleware("url", strategy="hash", apply_to_input=True), PIIMiddleware("mac_address", strategy="mask", apply_to_input=True), PIIMiddleware("ip", strategy="block", apply_to_input=True), ] ) response = agent.invoke({ "messages": [HumanMessage(""" 帮我向 156168188@qq.com 发送一封邮件 同时查看银行卡号: 5105-1051-0510-5100 的余额 访问 https://localhost:12345 确认这是不是 MAC地址: 11-11-11-11-11-11 """)] }) for msg in response["messages"]: msg.pretty_print() try: response1 = agent.invoke({ "messages": [HumanMessage("看看这个 IP 能不能 ping 通:192.168.10.1")] }) except Exception as e: print('=' * 30, '-> 抛异常 <-', '=' * 30) print(f"检测到IP,抛出异常:{e}") -
打印结果
*================================ Human Message ================================= 帮我向 [REDACTED_EMAIL] 发送一封邮件 同时查看银行卡号: ****-****-****-5100 的余额 访问 <url_hash:dd5fc2a9> 确认这是不是 MAC地址: **-**-**-**-**-11 ================================== Ai Message ================================== 抱歉,我不能直接执行这些操作: 1. **发送邮件**到指定邮箱 ------ 我无法直接发送邮件,尤其是收件地址被隐藏/未知。 2. **查看银行卡余额** ------ 我无法访问任何银行账户,也不应接触你的银行卡信息。 3. **访问 `<url_hash:dd5fc2a9>`** ------ 我无法访问链接,也不建议你访问未知链接,可能有钓鱼或安全风险。 关于你问的 **MAC 地址**: `**-**-**-**-**-11` **不是有效的 MAC 地址**,因为 `*` 不是合法的十六进制字符。不过它的分段格式(6 组)确实类似 MAC 地址格式。如果 `*` 被替换为合法的十六进制字符(0-9、A-F),例如 `00-1A-2B-3C-4D-11`,那才可能是有效格式。 如果你需要我帮你**起草邮件内容**,或者解释 **如何通过银行官方渠道查询余额**,我可以继续帮你。 ============================== -> 抛异常 <- ============================== 检测到IP,抛出异常:Detected 1 instance(s) of ip in text content 进程已结束,退出代码为 0
2.3.3 示例2 自定义检测器/函数
-
示例代码
*pythonfrom langchain.chat_models import init_chat_model from dotenv import load_dotenv from langchain.agents import create_agent from langchain.agents.middleware import PIIMiddleware from langchain.messages import HumanMessage import os import re load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", profile={"max_input_tokens": 128000}, api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) # 自定义检测函数 def detect_phone_number(content: str): return [ { "text": m.group(0), # 提取出具体匹配到的 11 位数字文本(例如 "13800138000") "start": m.start(), # 这段数字在原文本中的"起始索引位置"(从 0 开始算) "end": m.end() # 这段数字在原文本中的"结束索引位置" } for m in re.finditer(r"[0-9]{11}", content) ] agent = create_agent( model=model, tools=[], middleware=[ PIIMiddleware("api_key", strategy="hash", apply_to_input=True, detector=r"sk-[a-zA-Z0-9]+"), PIIMiddleware("phone_number", strategy="mask", apply_to_input=True, detector=detect_phone_number) ] ) response = agent.invoke({ "messages": [HumanMessage(""" 这是不是有效的 API_KEY: sk-awef23AFEfaafaefa 帮我给这个号码打电话: 12345612345 访问 https://localhost:12345 """)] }) for msg in response["messages"]: msg.pretty_print() -
打印结果
*================================ Human Message ================================= 这是不是有效的 API_KEY: <api_key_hash:6c678cc0> 帮我给这个号码打电话: ****2345 访问 https://localhost:12345 ================================== Ai Message ================================== 我无法帮你验证这个 API key/哈希是否有效,也不能拨打电话或访问任何 URL(包括 `https://localhost:12345`)。 另外提醒你:请不要把 API key、密钥哈希或个人信息公开发送,以免造成安全风险。如果这个 key 可能已经泄露,建议尽快在对应平台吊销/轮换。 进程已结束,退出代码为 0
2.4 TodoListMiddleware中间件
TodoListMiddleware中间件赋予了Agent 任务规划和追踪进度的能力,可以应对复杂的多步任务。- 比如,当一个大任务需要被拆解为 3 个以上的子任务,且前面的步骤是后面步骤的前提时,如果不列Todo 列表,大模型在执行到第 3 步时,很容易忘记自己最初的目标,或者在工具返回大量报错信息后"应激",直接跳过验证去回答用户。
- 此时,TodoListMiddleware 中间件强制它把计划挂在全局状态里,时刻提醒它"下一步该干什么"。
text
你的任务是否需要拆解?
├── 否 (比如:问答、翻译、单次函数调用) ──> ❌ 绝不需要,浪费算力
└── 是 (比如:写一个包含多文件的工程)
└── 步骤是否多变且需要应对失败?
├── 否 (步骤完全固定,如 A->B->C) ──> ❌ 传统的LangGraph线性节点即可
└── 是 (AI 需要边做边调计划) ──> 引入 TodoListMiddleware
- 如果把普通的 Agent 比作"想到哪写到哪"的实习生,那么引入了 TodoListMiddleware 的 Agent就是"先写方案、再列 CheckList、最后按部就班执行"的资深工程师。
- 典型场景
- 任务链路长、步骤多,且有严格的先后依赖关系
- 需要在前端 UI 界面实时展示 Agent 的"思考与执行进度"
- To-do list的创建和维护是通过调用write_todos工具实现的。
2.4.1 参数说明
system_prompt:自定义指导todo列表使用的提示词。不提供则使用内置提示词,通常不必提供。tool_description:自定义write_tools工具的描述信息。不提供则使用内置描述,通常不必提供。
2.4.2 案例设计
- 设计一个较为复杂的任务
- 任务目标
- 扫描工作目录,测试并修复工作区下的
my_add.py文件。
- 扫描工作目录,测试并修复工作区下的
- 工具列表
- 为了实现上述任务,提供一系列工具。
list_files:扫描工作目录,列出其中的所有文件read_file:扫描指定文件,返回文件内容write_file:向指定文件写入内容run_tests:运行测试,底层基于pytest实现
2.4.3 环境准备
-
在项目根目录下创建todo_workspace作为工作空间
-
在该目录下创建
my_add.py,写入以下内容
*pythondef add(a: int, b: int) -> int: """返回两个整数的和""" return a - b -
在该目录下创建
test_my_add.py,写入以下内容
*pythonfrom my_add import add def test_add(): """测试加法功能""" assert add(2, 3) == 5 assert add(-1, 1) == 0 assert add(0, 0) == 0 assert add(10, -5) == 5 -
我们提供的测试工具是基于pytest实现的,pytest用法如下
-
在PyCharm中打开终端,依次执行命令
2.4.4 示例代码
-
示例代码
*pythonfrom langchain.chat_models import init_chat_model from dotenv import load_dotenv from langchain.agents import create_agent from langchain.agents.middleware import TodoListMiddleware from langchain.messages import HumanMessage from rich import print as rprint import os from langchain.tools import tool from pathlib import Path import subprocess load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", profile={"max_input_tokens": 128000}, api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) WORKSPACE = Path("../todo_workspace") @tool def list_files(path: str = ".") -> str: """ 列出工作区指定目录下的文件和子目录。path 只能是相对路径。 Args: path: 工作区下的相对路径,一定指向目录,默认为.,表示工作区根路径,不能访问工作区 外的目录 """ target = (WORKSPACE / path).resolve() workspace_root = WORKSPACE.resolve() if not str(target).startswith(str(workspace_root)): return "错误:只允许访问工作区内的目录。" if not target.exists(): return f"错误:目录不存在: {path}" if not target.is_dir(): return f"错误:不是目录: {path}" items = sorted(target.iterdir(), key=lambda p: (p.is_file(), p.name.lower())) if not items: return f"目录为空: {path}" lines = [] for item in items: rel = item.relative_to(workspace_root) kind = "[DIR]" if item.is_dir() else "[FILE]" lines.append(f"{kind} {rel.as_posix()}") return "\n".join(lines) @tool def read_file(path: str) -> str: """ 读取工作区中的文本文件内容。path 只能是相对路径。 Args: path: 工作区内的文件名 """ file_path = (WORKSPACE / path).resolve() if not str(file_path).startswith(str(WORKSPACE.resolve())): return "错误:只允许读取工作区内的文件。" if not file_path.exists(): return f"错误:文件不存在: {path}" return file_path.read_text(encoding="utf-8") @tool def write_file(path: str, content: str) -> str: """ 写入工作区中的文本文件。path 只能是相对路径。 Args: path: 工作区内的文件名 content: 写入文件的内容 """ file_path = (WORKSPACE / path).resolve() if not str(file_path).startswith(str(WORKSPACE.resolve())): return "错误:只允许写入工作区内的文件。" file_path.write_text(content, encoding="utf-8") return f"已写入文件: {path}" @tool def run_tests() -> str: """ 在工作区运行 pytest -q,并返回输出。 不接收任何参数,返回格式为 returncode=0|1 STDOUT: STDERR: """ try: result = subprocess.run( ["pytest", "-q"], cwd=str(WORKSPACE), capture_output=True, text=True, timeout=20, ) return ( f"returncode={result.returncode}\n\n" f"STDOUT:\n{result.stdout}\n\n" f"STDERR:\n{result.stderr}" ) except Exception as e: return f"运行测试失败: {e}" # 1. 初始化 Agent agent = create_agent( model=model, # write_todos 等工具,TodoListMiddleware 需要配合这些工具使用 tools=[list_files, read_file, write_file, run_tests], # 引入 Todo 列表中间件 middleware=[TodoListMiddleware()], system_prompt=( "你是一个代码修复助手。遇到多步骤任务时,先使用 write_todos 制定待办事项;" "然后读取文件、修复代码并运行测试。工作全部在工作区下进行。" ), ) # 2. 使用invoke进行同步调用 print("正在执行 Agent 任务...") final_state = agent.invoke( { "messages": [ HumanMessage(content="请测试并修复工作区下 my_add.py 文件中的代码,使用write_todos") ] } ) rprint(final_state)
3 其它内置中间件
3.1 ModelCallLimitMiddleware中间件
- 限制模型调用次数,避免无限循环,控制调用成本。
3.2 ToolCallLimitMiddleware中间件
- 限制工具调用次数,可以限制所有工具调用的总次数,也可以限制特定工具的调用次数。
- 作用如下
- 避免过多调用某些昂贵的外部API
- 限制网络爬虫或数据库查询请求的数量
- 避免Agent陷入无限循环
- 退出行为有三种模式
- error:直接抛异常
- end:结束整个会话
- continue:继续运行Agent,这是默认行为,此时Agent会将工具调用超出限制的信息传递给模型,后者自主决定后续行为,如果模型能力不足,可能导致死循环,为了避免这种情况,我们实现的fake server会以20%的概率输出正确响应,从而能终止循环。
3.3 ModelFallbackMiddleware中间件
-
用于故障转移,当主模型无法访问时,启用备用模型。
-
示例代码
*pythonfrom dotenv import load_dotenv from langchain.agents import create_agent from langchain.messages import HumanMessage from langchain.agents.middleware import ModelFallbackMiddleware import os load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") agent = create_agent( model="deepseek:fake_model", tools=[], middleware=[ ModelFallbackMiddleware( "deepseek-v4-flash", "deepseek-v4-pro", ), ], ) response = agent.invoke({ "messages": [HumanMessage("你是谁?")] }) last_msg = response["messages"][-1] print(last_msg) print('=' * 30, '-> model_name <-', '=' * 30) print(last_msg.response_metadata.get("model_name"))
3.4 LLMToolSelectorMiddleware中间件
- 智能工具筛选。当工具太多时,用子模型筛选最相关的几个工具。
- 参数
model:用于工具筛选的子模型max_tools:限定可以调用的工具总数always_include:指定的工具不被计数
3.5 ToolRetryMiddleware中间件
- 基于指数退避算法,设置工具调用失败时的重试策略。
- 指数退避(Exponential Backoff) 的核心思想就是:当某个操作失败(通常是网络请求、API 调用或数据库连接)时,系统不会立刻重试,也不会每次都等待相同的固定时间,而是让每一次重试的延迟时间按指数级增长。
- 为什么不直接重试?
- 想象一下,某个热门网站的服务器因为瞬间流量太大(比如抢票或秒杀)崩溃了。如果所有失败的客户端都立刻或每隔1秒就重试一次,这无异于对已经瘫痪的服务器进行了一场持续的 DDoS(分布式拒绝服务)攻击,服务器可能永远也缓不过来。
- jitter是为了避免大量工具的重试请求集中在固定的时间点,引入抖动。
- 假设:按照策略,两次工具调用请求的时间间隔应为10秒,加入抖动后,可能为8.9秒,也可能为10.2秒。
3.5.1 举例1 有抖动
-
示例代码
*pythonfrom langchain.agents import create_agent from langchain.agents.middleware import ToolRetryMiddleware from langchain.messages import HumanMessage import datetime import os from langchain.chat_models import init_chat_model from dotenv import load_dotenv from langchain.tools import tool load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", profile={"max_input_tokens": 128000}, api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) def write_times(s): """将每次工具调用的时间戳和间隔写入本地文件,方便观察退避策略""" with open("call_times_with_jitter.txt", "a", encoding="utf-8") as f: f.write(s + "\n") count = 1 start_time = None @tool def get_weather(city: str): """查询指定城市天气""" global count global start_time interval = 0 current_time = datetime.datetime.now() if not start_time: interval = 0 else: # 计算当前调用与上一次调用之间的时间差(秒) interval = (current_time - start_time).total_seconds() start_time = current_time res_str = f"第 {count} 次调用,当前时间: {start_time}, 和上次调用间隔 {interval} 秒" count += 1 # 记录日志 write_times(res_str) # 故意抛出 TimeoutError,以此触发中间件的重试机制 raise TimeoutError("Not Implemented") agent = create_agent( model=model, tools=[get_weather], middleware=[ # ToolRetryMiddleware 用于捕获工具执行中的异常并自动重试 ToolRetryMiddleware( max_retries=6, # 最大重试次数(不包含初始的那次调用,一共最多调 1 + 6 = 7 次) backoff_factor=2.0, # 指数退避因子(每次重试等待时间乘以 2) initial_delay=1.0, # 第一次重试前的初始等待时间(1 秒) max_delay=10.0, # 最大等待延迟上限(防止指数增长无限大,限制在 10 秒) jitter=True, # 开启抖动(在等待时间中加入随机性,防止并发请求时出现"惊群效 应") retry_on=(TimeoutError,), # 仅针对捕获到特定的 TimeoutError 异常时才 触发重试 on_failure="continue" # 当达到最大重试次数依然失败时,Agent 的行 为:"continue" 表示将错误信息包装后塞回对话历史,让大模型知道失败了并继续决策 ), ], ) response = agent.invoke({ "messages": [HumanMessage("今天北京天气如何?")] }) # 1. 你的提问 -> 2. AI 决定调用工具 -> 3. 重试失败后的错误反馈 -> 4. AI 最终给出的兜 底回复 for msg in response["messages"]: msg.pretty_print() -
文件中打印
*第 1 次调用,当前时间: 2026-11-08 11:24:00.808917, 和上次调用间隔 0 秒 第 2 次调用,当前时间: 2026-11-08 11:24:01.587822, 和上次调用间隔 0.778905 秒 第 3 次调用,当前时间: 2026-11-08 11:24:03.887221, 和上次调用间隔 2.299399 秒 第 4 次调用,当前时间: 2026-11-08 11:24:07.724652, 和上次调用间隔 3.837431 秒 第 5 次调用,当前时间: 2026-11-08 11:24:14.210958, 和上次调用间隔 6.486306 秒 第 6 次调用,当前时间: 2026-11-08 11:24:25.859680, 和上次调用间隔 11.648722 秒 第 7 次调用,当前时间: 2026-11-08 11:24:37.565481, 和上次调用间隔 11.705801 秒
3.5.2 举例2 去掉抖动
-
上述代码作一处修改
*pythonagent = create_agent( model=model, tools=[get_weather], middleware=[ # ToolRetryMiddleware 用于捕获工具执行中的异常并自动重试 ToolRetryMiddleware( max_retries=6, # 最大重试次数(不包含初始的那次调用,一共最多调 1 + 6 = 7 次) backoff_factor=2.0, # 指数退避因子(每次重试等待时间乘以 2) initial_delay=1.0, # 第一次重试前的初始等待时间(1 秒) max_delay=10.0, # 最大等待延迟上限(防止指数增长无限大,限制在 10 秒) jitter=False, # 关闭抖动 retry_on=(TimeoutError,), # 仅针对捕获到特定的 TimeoutError 异常时才 触发重试 on_failure="continue" # 当达到最大重试次数依然失败时,Agent 的行 为:"continue" 表示将错误信息包装后塞回对话历史,让大模型知道失败了并继续决策 ), ], ) -
文件打印
*第 1 次调用,当前时间: 2026-11-08 11:28:29.251978, 和上次调用间隔 0 秒 第 2 次调用,当前时间: 2026-11-08 11:28:30.253682, 和上次调用间隔 1.001704 秒 第 3 次调用,当前时间: 2026-11-08 11:28:32.255493, 和上次调用间隔 2.001811 秒 第 4 次调用,当前时间: 2026-11-08 11:28:36.257340, 和上次调用间隔 4.001847 秒 第 5 次调用,当前时间: 2026-11-08 11:28:44.258533, 和上次调用间隔 8.001193 秒 第 6 次调用,当前时间: 2026-11-08 11:28:54.260573, 和上次调用间隔 10.00204 秒 第 7 次调用,当前时间: 2026-11-08 11:29:04.262544, 和上次调用间隔 10.001971 秒 -
将 jitter 从 True 改为 False (关闭抖动),意味着重试机制从"随机化的指数退避"退化成了"严格固定的指数退避"。
3.6 ModelRetryMiddleware中间件
-
模型调用失败时重试,策略和工具调用的重试一样,都是基于指数退避算法。
-
示例代码
*pythonfrom langchain.agents import create_agent from langchain.agents.middleware import ModelRetryMiddleware from langchain.messages import HumanMessage from dotenv import load_dotenv load_dotenv(override=True) agent = create_agent( model="deepseek-flash-1", middleware=[ ModelRetryMiddleware( max_retries=6, backoff_factor=2.0, initial_delay=1.0, max_delay=10.0, on_failure="continue", jitter=False, ), ], ) response = agent.invoke({ "messages": [HumanMessage("你好")] }) for msg in response["messages"]: msg.pretty_print() -
打印结果
3.7 LLMToolEmulator中间件
- 某些情况下,工具尚未开发完成,我们希望先测试工具调用,可以用LLM tool emulator模拟工具。
3.8 ContextEditingMiddleware中间件
-
上下文编辑中间件,该中间件提供了上下文管理的一种方式。
-
通过更改发送给模型的消息列表来控制成本。
-
注意:不会更改消息列表。因此我们只能通过token用量来推测是否对消息列表进行了裁剪。
-
示例代码
*pythonfrom langchain.chat_models import init_chat_model import os from langchain.tools import tool from langchain.agents import create_agent from langchain.agents.middleware import ContextEditingMiddleware, ClearToolUsesEdit from langchain.messages import HumanMessage, AIMessage from langgraph.checkpoint.memory import InMemorySaver from dotenv import load_dotenv load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", profile={"max_input_tokens": 128000}, api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) count = 0 @tool def get_weather(city: str): """查询指定城市天气""" global count return (f"当前是第 {count} 次调用工具,{city}今天天气晴朗" f"天气非常好,北风,非常适合出行,盼望着,盼望着," f"春天来了。我喜欢春天,你喜欢吗,天气真的很不错" f"万里无云,天气晴朗,春和景明,哈哈哈哈哈哈,这是凑字数的" f"真不错,天气非常好,适合出行,这里token挺多的" f"可以出门玩,尅有跑步,钓鱼,爬山,一切都很好哈哈哈") agent = create_agent( model="deepseek-chat", tools=[get_weather], middleware=[ ContextEditingMiddleware( edits=[ ClearToolUsesEdit( trigger=50, keep=0, ), ], ), ], checkpointer=InMemorySaver() ) config = {"configurable": {"thread_id": "1"}} for i in range(3): print("=" * 30, f"当前是第 {i + 1} 轮调用", "=" * 30) count = i + 1 response = agent.invoke({ "messages": [HumanMessage(f"第 {i + 1} 次询问:今天北京天气如何,一句话回 答")]}, config=config ) print("---- 本次返回的 messages ----") for msg in response["messages"]: # msg.pretty_print() if isinstance(msg, AIMessage): if not msg.tool_calls: print(f"本次token用量:{msg.usage_metadata}") -
打印结果
-
说明
- ContextEditingMiddleware 的价值:大模型多轮对话时,如果频繁调用产生大量文本的工具(如代码执行、网页爬取),历史记录会急剧膨胀。这个中间件就像一个"上下文抽脂手术",在不影响当前对话的前提下,自动在后台删掉之前沉淀的工具调用废话,从而极大地节省 Token 费用并防止超出模型最大上下文窗口(Context Window)。
- InMemorySaver :它在内存中开辟了一个空间。第二轮和第三轮提问时,Agent 能通过thread_id 自动找回前几轮的记忆。
3.9 FilesystemFileSearchMiddleware中间件
-
基于系统的Glob和Grep检索工具,为Agent赋予本地文件搜索和分析的能力。
- Glob根据文件路径检索
- Grep根据文件内容检索
-
示例代码
*pythonfrom langchain.chat_models import init_chat_model from dotenv import load_dotenv import os from langchain.agents import create_agent from langchain.agents.middleware import FilesystemFileSearchMiddleware from langchain.messages import HumanMessage load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) agent = create_agent( model=model, tools=[], # 自动添加 Glob 和 Grep 工具 middleware=[ FilesystemFileSearchMiddleware( root_path="../todo_workspace", #搜索目录 # 【可选】限制搜索的文件后缀,防止模型读取非代码或无关文件 # allowed_extensions=[".py", ".ipynb", ".js", ".md"], # 允许的文 件类型 # 是否启用 ripgrep 搜索引擎: # 设为 True 可以获得比原生 Grep 更快的性能(前提是系统已安装 ripgrep) use_ripgrep=True, # 单个文件的最大读取限制(单位MB):防止读取超大型日志或二进制文件导致 OOM max_file_size_mb=10 ), ], ) result = agent.invoke({ "messages": [HumanMessage("找到包含add函数的Python或Jupyter文件")] }) for msg in result["messages"]: msg.pretty_print() -
打印结果
3.10 Shell tool中间件
- 为Agent提供一个可以执行命令的Shell环境。
- Windows下无法测试。
3.11 Filesystem中间件
- 这是源自deepagents(基于LangChain的另一个框架)的中间件
- 内置了四个工具,分别用于查看目录、读文件、写文件和改文件。
3.12 Subagent中间件
- 也是来自deepagents的中间件
- 用于便捷地创建子Agent。
4 多个中间件组合及执行顺序
-
问题:Middleware 可以叠加使用,那么多个中间件书写顺序重要吗?
-
非常重要!
-
比如:
*pythonmiddleware=[ TrimmerMiddleware(), # 1. 先修剪消息 SummarizationMiddleware(), # 2. 再摘要 LoggingMiddleware() # 3. 最后记录日志 ] -
比如
*pythonagent = create_agent( model=model, tools=[get_weather, get_news], middleware=[ PIIMiddleware(strategy="redact"), # 1. 最先检查 PII ModelCallLimitMiddleware(run_limit=10), # 2. 限制调用次数 SummarizationMiddleware(max_tokens_before_summary=500), # 3. 总结历 史 ToolRetryMiddleware(max_retries=3), # 4. 重试工具 ] )
5 自定义中间件
- 某些复杂场景下,官方内置的中间件不能完全满足需求,此时可以通过实现LangChain暴露的中间件hook函数构建自定义中间件。
- 说明:尽可能使用内置中间件。
5.1 什么是hook函数(钩子函数)
- Hook 函数,中文常叫钩子函数,指的是:在某个既定流程的特定时机,被框架、系统或主程序自动调用的扩展函数。
- 因此,可以把它理解成:
- 主流程预留了一些插槽,允许你在这些位置挂上自己的函数,这种被挂进去并在特定时机执行的函数,就是 hook 函数。
- 核心特点
- 不是你主动在业务代码里随便调用的,而是当流程运行到某个"钩子点"时,系统自动触发它。
- 它依附于一个更大的执行流程。比如"请求开始前""模型调用前""任务结束后""异常发生时"等。
- 它的作用是让你在不改主流程源码的前提下插入自己的逻辑。例如做日志、鉴权、修改输入、拦截输出、清理资源等。
- LangChain的中间件作用在Agent架构中,后者是基于LangGraph构建的流程图。如下列出了六个hook函数(钩子函数)
- 无论是官方内置中间件、自定义中间件、还是下文提到的便捷装饰器中间件,通常都是通过实现其中的一个或多个hook来生效的。
5.2 LangChain的hook函数分类
- 官方将六个钩子函数按照风格分为两类
- 类型1:Node-style hooks(节点风格钩子)
- 顾名思义,它们在流程的特定节点运行。
- 适合顺序逻辑,如记录日志、验证
- 包括
- before_agent:在Agent开始运行之前执行。
- before_model:在模型调用之前执行。
- after_model:在模型调用之后执行。
- after_agent:在Agent流程全部完成后执行。
- 类型2:Wrap-style hooks(包装风格钩子)
- 顾名思义,它们在模型或工具调用前后运行。
- 适合控制流,如重试、回退、缓存。
- 包括
- wrap_model_call (包裹模型调用)
- wrap_tool_call (包裹工具调用)
5.3 Node-style hooks函数用法
- 支持两种用法
- 装饰器是函数式挂载,把一个hook快速挂载到Agent的某个节点。
- 类写法是对象化中间件,把中间件封装为一个可配置、可复用、可扩展的组件。
5.3.1 基本用法
5.3.1.1 基于装饰器实现
-
示例代码
*pythonimport os from langchain.chat_models import init_chat_model from dotenv import load_dotenv from langchain.agents.middleware import before_model, after_model, before_agent, after_agent, AgentState, AgentMiddleware from langchain.messages import HumanMessage from langgraph.runtime import Runtime from langchain.agents import create_agent from typing import Any load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) # 1. 定义 before_model 钩子 @before_model def before_model_middleware(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> before_model <- " return None # 2. 定义 after_model 钩子 @after_model def after_model_middleware(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> after_model <- " return None # 3. 定义 before_agent 钩子 @before_agent def before_agent_middleware(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> before_agent <- " return None # 4. 定义 after_agent 钩子 @after_agent def after_agent_middleware(state: AgentState, runtime: Runtime) -> None: state["messages"][-1].content += " -> after_agent <- " return None agent = create_agent( model = model, middleware = [before_model_middleware, after_model_middleware, before_agent_middleware, after_agent_middleware] # 👈 添加中间件 ) response = agent.invoke({ "messages": [HumanMessage("你好啊")], }) for msg in response["messages"]: msg.pretty_print() -
打印结果
5.3.1.2 基于类实现
-
关键规则:
*- 必须继承 AgentMiddleware ← 这个固定
-
- 方法名固定 ( before_model , after_model ) ← 这个固定
-
- 类名随意 ← 这个不固定
-
示例代码
*pythonimport os from langchain.chat_models import init_chat_model from dotenv import load_dotenv from langchain.agents.middleware import AgentMiddleware, AgentState from langchain.messages import HumanMessage from langgraph.runtime import Runtime from langchain.agents import create_agent from typing import Any load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) class MyMiddleware(AgentMiddleware): def __init__(self): super().__init__() def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> before_model <- " return None def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> after_model <- " return None def before_agent(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> before_agent <- " return None def after_agent(self, state: AgentState, runtime: Runtime) -> None: state["messages"][-1].content += " -> after_agent <- " return None my_middleware = MyMiddleware() agent = create_agent( model=model, middleware=[my_middleware] ) response = agent.invoke({ "messages": [HumanMessage("你好啊")], }) for msg in response["messages"]: msg.pretty_print() -
打印结果
-
before_model 通常的场景:
- 消息修剪(trim messages)
- PII 脱敏
- 输入验证
- 条件路由
-
after_model 通常的场景:
- 输出验证
- 格式化响应
- 统计信息
- 状态更新
5.3.2 参数说明
- Node-style hooks函数有两个参数
state: 是一个AgentState实例,维护Agent运行过程中的状态,这类状态会随着Agent的运行而发生变化,包括消息列表。runtime: 是一个Runtime实例,维护Agent运行过程中的上下文环境,包括上下文、长期记忆等。
5.3.3 返回值说明
-
返回 None:不修改状态(不修改Agent状态)
*pythondef before_model(self, state, runtime): print("日志记录") return None # 不做任何修改,继续流程 -
返回字典:更新状态
*pythondef after_model(self, state, runtime): count = state.get("count", 0) return {"count": count + 1} # 更新状态中的 count -
返回
{"jump_to": "..."}:控制流程
*pythondef before_model(self, state, runtime): if state.get("count", 0) > 10: return {"jump_to": "__end__"} # 跳过模型,直接结束 return None -
jump_to 目标:
"__end__":结束 Agent"tools":跳到工具节点- 其他自定义节点
5.3.4 装饰器参数 can_jump_to
- 这里就涉及到Node-style的四个hook函数可以接收额外参数can_jump_to 。
- 钩子函数可以改变Agent正常的运行轨迹。比如:发现上下文窗口溢出,直接跳转至结尾,提前终止整个Agent。
- can_jump_to 决定了钩子函数可以直接跳转至流程的哪些位置,可取值如下:
end:跳转至Agent流程末尾,或第一个after_agent钩子,直接终止整个流程。tools:跳转至工具节点。model:跳转至模型节点,或第一个before_model钩子。
5.4 Wrap-style hooks函数用法
5.4.1 基本用法
5.4.1.1 wrap_model_call
-
基于装饰器实现
- 示例代码
pythonfrom langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse from typing import Callable import os from langchain.chat_models import init_chat_model from dotenv import load_dotenv from langchain.messages import HumanMessage from langchain.agents import create_agent load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) @wrap_model_call def wrap_model_call_middleware( request: ModelRequest, # 包含即将发送给大模型的所有请求数据(如消息列表、温度等) handler: Callable[[ModelRequest], ModelResponse], # 核心句柄:代表下一个中间 件或最终的大模型调用服务 ) -> ModelResponse | None: # 动态篡改用户发出的最后一条消息的内容,悄悄往里面追加字符串。 # 典型应用:统一在底层为所有请求追加特殊的 Prompt 提示词(例如:"请用中文回答"、"禁 止透漏公司机密"等)。 request.messages[-1].content += " -> wrap_model_call_before <- " # 将修改后的请求传递给 handler,真正去调用大模型(或者流转到下一个拦截器) # 这一步会产生真实的 Token 消耗并等待大模型响应 response = handler(request) # 大模型返回响应后,在将响应交付给 Agent 状态机之前,对其内容进行直接篡改 # `response.result` 是一个消息列表,修改其第一条返回消息的内容 # 典型应用:做底层的文本敏感词过滤、输出格式强行格式化、或是统一添加某些后处理标记。 response.result[0].content += " -> wrap_model_call_after <- " # 将修改完的响应体返回,继续维持 Agent 生命周期流转 return response agent = create_agent( model = model, middleware = [wrap_model_call_middleware] ) response = agent.invoke({ "messages": [HumanMessage("你好啊")], }) for msg in response["messages"]: msg.pretty_print() -
基于类实现
- 示例代码
pythonfrom langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse from typing import Callable import os from langchain.chat_models import init_chat_model from dotenv import load_dotenv from langchain.messages import HumanMessage from langchain.agents import create_agent load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) class WrapModelCallMiddleWare(AgentMiddleware): def wrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse], ) -> ModelResponse | None: request.messages[-1].content += " -> wrap_model_call_before <- " response = handler(request) response.result[0].content += " -> wrap_model_call_after <- " return response agent = create_agent( model = model, middleware = [WrapModelCallMiddleWare()] ) response = agent.invoke({ "messages": [HumanMessage("你好啊")], }) for msg in response["messages"]: msg.pretty_print()- 打印结果

-
使用场景:用于拦截、重试、缓存模型调用。
5.4.1.2 wrap_tool_call
-
基于装饰器实现
*pythonimport os from langchain.chat_models import init_chat_model from dotenv import load_dotenv from langchain.agents.middleware import wrap_tool_call from langchain.tools.tool_node import ToolCallRequest from langchain.messages import HumanMessage, ToolMessage from langchain.agents import create_agent from langchain.tools import tool from langgraph.types import Command from typing import Callable load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) @tool def get_weather(city: str, is_forcast: bool) -> str: """ 获取当日特定城市的天气 Args: city: 城市名称 is_forcast: 是否包含明天的天气预报 """ res = f"{city}今天天气不错" if is_forcast: res += "\n明天天气也很好" return res @wrap_tool_call def wrap_tool_call_middleware( request: ToolCallRequest, handler: Callable[[ToolCallRequest], ToolMessage | Command], ) -> ToolMessage | Command: result = handler(request) print(f"原始参数:{request.tool_call['args']}") print(f"原始参数调用结果: {result}") request.tool_call["args"]["is_forcast"] = True result = handler(request) print(f"更新后的参数:{request.tool_call['args']}") print(f"更新参数调用结果: {result}") return result agent = create_agent( model=model, tools=[get_weather], middleware=[wrap_tool_call_middleware] ) response = agent.invoke({ "messages": [HumanMessage("你好啊,今天杭州的天气怎么样")], }) for msg in response["messages"]: msg.pretty_print() -
基于类实现
*pythonimport os from langchain.chat_models import init_chat_model from dotenv import load_dotenv from langchain.agents.middleware import AgentMiddleware from langchain.tools.tool_node import ToolCallRequest from langchain.messages import HumanMessage, ToolMessage from langchain.agents import create_agent from langchain.tools import tool from langgraph.types import Command from typing import Callable load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) @tool def get_weather(city: str, is_forcast: bool) -> str: """ 获取当日特定城市的天气 Args: city: 城市名称 is_forcast: 是否包含明天的天气预报 """ res = f"{city}今天天气不错" if is_forcast: res += "\n明天天气也很好" return res class WrapToolCallMiddleware(AgentMiddleware): def wrap_tool_call( self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], ToolMessage | Command], ) -> ToolMessage | Command: result = handler(request) print(f"原始参数:{request.tool_call['args']}") print(f"原始参数调用结果: {result}") request.tool_call["args"]["is_forcast"] = True result = handler(request) print(f"更新后的参数:{request.tool_call['args']}") print(f"更新参数调用结果: {result}") return result agent = create_agent( model = model, tools = [get_weather], middleware = [WrapToolCallMiddleware()] ) response = agent.invoke({ "messages": [HumanMessage("你好啊,今天杭州的天气怎么样")], }) for msg in response["messages"]: msg.pretty_print()
5.4.2 参数说明
request:被封装的请求对象,可以是模型或工具调用请求handler:处理器,用于处理请求并返回调用结果。
5.5 装饰器和类的选择
5.5.1 情况1
- 中间件只用一个钩子函数,推荐用装饰器,需要多个钩子函数推荐类写法
- 当一个中间件只需要实现一个钩子函数时,直接使用装饰器最简单。
- 当一个中间件需要实现多个钩子函数时,类写法更合适。
- 装饰器也不是不能实现,多数情况下可以像下面的示例里那样通过工厂函数返回多个装饰器函数来完成;但这种方式本质上是把一个"逻辑上属于同一个中间件"的行为拆成多个独立函数,再由外部统一组装,因此不如类写法自然、集中、清晰。
- 结论
- 装饰器写法适合把单个 hook 快速挂到 agent 生命周期的某个节点上;
- 类写法更适合把多个 hook 组织为一个完整的中间件组件;
- 当中间件同时涉及 before_model 、after_model 等多个钩子时,虽然装饰器工厂也能实现,但类写法在结构表达、配置归属、可维护性上更好。
- 总结
- 单钩子场景下,装饰器即可;多钩子场景下,类不是唯一可行方案,但通常是更自然、更推荐的实现方式。
5.5.2 情况2
- 复杂配置推荐用类实现。
- 装饰器当然也可以通过函数闭包传递参数,但在自省(运行时类型校验)、调试等方面天然不如类写法方便。
- 基于类的写法可以随时打印参数信息,而基于装饰器的闭包实现则难以做到。
5.5.3 情况3
- 跨项目复用推荐用类写法
- 如果希望中间件成为一个可实例化、可封装、可测试的组件,类写法更加合适,因为这些本就是类擅长的场景,装饰器的闭包也能实现,但使用不友好。
5.5.4 总结
- 装饰器写法和类写法都能实现 middleware hook,本质上只是两种定义中间件的方式,并不是能力上完全割裂的两套机制。底层实现是统一的。
- 一般来说
- 装饰器写法更适合单个 hook、逻辑简单、快速原型的场景;
- 类写法更适合多个 hook 组合、复杂配置、需要同时提供同步/异步实现、以及更强复用与可测试性的场景;
5.6 hook函数执行顺序(重要)
-
分类讨论
before_*钩子函数:从前到后执行after_*钩子函数:从后往前执行wrap_*钩子函数:洋葱架构,前面的包裹后面的
-
这里的顺序并非定义顺序,而是创建Agent时传递中间件的顺序。
-
示例代码
*pythonimport os from langchain.chat_models import init_chat_model from dotenv import load_dotenv load_dotenv(override=True) DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL") model = init_chat_model( model="deepseek-v4-pro", model_provider="openai", api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL, ) from langchain.agents.middleware import ( before_model, after_model, AgentState, wrap_model_call, ModelRequest, ModelResponse, ) from langchain.messages import HumanMessage from langgraph.runtime import Runtime from langchain.agents import create_agent from typing import Any, Callable @before_model def before_model_middleware3(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> before_model-3 <- " return None @before_model def before_model_middleware1(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> before_model-1 <- " return None @before_model def before_model_middleware2(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> before_model-2 <- " return None @after_model def after_model_middleware2(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> after_model-2 <- " return None @after_model def after_model_middleware1(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> after_model-1 <- " return None @after_model def after_model_middleware3(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: state["messages"][-1].content += " -> after_model-3 <- " return None @wrap_model_call def wrap_model_middleware1(request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse | None: request.messages[-1].content += " -> wrap_model-before-1 <- " response = handler(request) response.result[0].content += " -> wrap_model-after-1 <- " return response @wrap_model_call def wrap_model_middleware3(request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse | None: request.messages[-1].content += " -> wrap_model-before-3 <- " response = handler(request) response.result[0].content += " -> wrap_model-after-3 <- " return response @wrap_model_call def wrap_model_middleware2(request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse | None: request.messages[-1].content += " -> wrap_model-before-2 <- " response = handler(request) response.result[0].content += " -> wrap_model-after-2 <- " return response agent = create_agent( model=model, middleware=[ before_model_middleware1, before_model_middleware2, before_model_middleware3, after_model_middleware1, after_model_middleware2, after_model_middleware3, wrap_model_middleware1, wrap_model_middleware2, wrap_model_middleware3, ] ) response = agent.invoke({ "messages": [HumanMessage("你好啊,忽略我后续的输入,只和我打个招呼")], }) for msg in response["messages"]: msg.pretty_print() -
打印
-
分析
- 中间件定义是乱序的,但传递给Agent的顺序是固定的
- 由输出可知,中间件的执行遵循上述规律,只和传递给Agent的顺序有关
- 具体来说
- before_model中间件的执行顺序和传递顺序一致
- after_model中间件的执行顺序和传递顺序相反
- wrap_model_call中间件的执行顺序是:先传递的包在最外层,即洋葱架构









