
1. 从能跑到能管DeepAgents 中间件到底在解决什么如果你已经用 LangChain 搭过几个 Agent大概率经历过这个阶段Demo 跑得飞起一旦接入真实业务就开始失控。工具调用乱序、上下文越滚越长、某个工具报错直接把整条链打断、想加个日志得改一堆地方。这些问题不是模型能力不够而是Agent 的执行流程缺少可插拔的管控层。DeepAgents 的中间件机制就是冲着这件事来的。它借鉴了 Web 框架里中间件的经典思路——在 Agent 执行的关键节点上开放钩子让你能在不改动核心逻辑的前提下插入自己的处理逻辑。关键词里的createMiddleware就是这套机制的入口函数配合 LangChain 的 Agent 体系使用。这篇文章适合两类人一是已经跑通 LangChain Agent、想进一步做工程化改造的开发者二是正在选型 Agent 框架、想搞清楚中间件这个抽象到底能带来什么实际价值的技术负责人。我会从中间件的工作原理讲起拆解createMiddleware的用法再结合工具调用拦截、上下文压缩、错误恢复这几个真实场景给出可复现的代码最后聊聊我在实际项目里踩过的坑。需要先说明一点DeepAgents 是构建在 LangChain 之上的 Agent 框架它的中间件不是孤立存在的而是深度嵌入了 Agent 的思考—行动—观察循环。理解这一点后面的所有设计就都顺了。2. 中间件在 Agent 循环里究竟挂在哪几个钩子上2.1 先搞清楚 Agent 的一次完整执行长什么样要理解中间件得先理解 Agent 的主循环。一个典型的 ReAct 风格 Agent单轮执行大致是这样的把系统提示、历史消息、可用工具列表打包成一次模型调用模型返回内容可能是普通回复也可能是工具调用请求如果是工具调用执行对应工具拿到结果把工具结果追加回消息历史回到第 1 步直到模型不再请求工具或达到轮次上限这个循环里每一步之间都是一个天然的切面。中间件要做的就是在这些切面上挂载自定义逻辑。你可以把它想象成一条流水线上的质检工位——原料进来、加工、出厂每个环节你都能插一手。2.2 四类核心钩子及其触发时机DeepAgents 的中间件体系围绕几个关键钩子展开我按执行顺序梳理一下钩子类型触发时机典型用途模型调用前消息打包完成、发起模型请求之前上下文裁剪、提示注入、敏感词过滤模型调用后拿到模型响应、尚未执行工具之前响应校验、工具调用改写、限流判断工具执行前确定要调用某工具、实际执行之前参数校验、权限检查、缓存命中判断工具执行后工具返回结果之后结果脱敏、格式归一、错误兜底这四个钩子覆盖了 Agent 单轮循环里所有可干预的位置。关键在于中间件是链式执行的——你可以注册多个中间件它们按顺序依次处理前一个的输出是后一个的输入。这跟 Express 或 Koa 的中间件模型几乎一模一样。2.3 为什么是中间件而不是回调有人会问LangChain 本身就有 callback 机制为什么还要中间件我的理解是两者的定位不同。Callback 是观察者主要用来做日志、监控、追踪它不该改变执行流程。而中间件是参与者它可以修改数据、短路流程、甚至决定要不要继续往下走。举个具体例子你想在工具调用前做参数校验校验不通过就返回一个错误提示给模型让它重试。用 callback 你做不到这件事因为 callback 拿不到阻止执行的能力。但中间件可以——它在工具执行前钩子里直接返回一个替代结果流程就绕过了真实工具调用。这个区别在工程上非常关键。3. createMiddleware 的实战用法从零写一个可用的中间件3.1 最小可用示例给每次工具调用打日志先上一个能跑起来的最小例子建立手感。假设你已经有一个配置好的 Agent现在想给所有工具调用加上结构化日志from deepagents import createMiddleware def logging_middleware(): def before_tool(state, tool_call): print(f[TOOL-CALL] name{tool_call[name]} args{tool_call[args]}) return tool_call def after_tool(state, tool_call, result): print(f[TOOL-RESULT] name{tool_call[name]} ok{result is not None}) return result return createMiddleware( before_toolbefore_tool, after_toolafter_tool, )把它注册到 Agent 上agent create_deep_agent( modelllm, tools[search_tool, calc_tool], middleware[logging_middleware()], )跑起来之后每次工具调用你都能在控制台看到清晰的进出记录。这个例子虽然简单但它揭示了中间件的核心契约钩子函数接收当前状态和上下文返回处理后的数据。返回什么下游就拿什么。3.2 钩子函数的参数约定与返回值语义这里有几个容易踩的细节我逐个说清楚。关于state参数它携带的是当前 Agent 的完整运行状态包括消息历史、已执行轮次、自定义的共享数据等。你可以在中间件里读取它也可以往里面写东西——比如把某个工具的调用次数记到 state 里后续中间件就能读到。关于返回值before_tool返回的对象会被当作最终要执行的工具调用传给下游。如果你返回None通常意味着跳过这次工具执行。after_tool返回的对象则会替换真实的工具结果写回消息历史。这个替换能力是很多高级玩法的基石。关于异常钩子里抛出的异常默认会中断整个 Agent 循环。如果你希望某个中间件出错时优雅降级需要在钩子内部自己 try/except 兜住。我个人的习惯是除了真正致命的错误其他一律在中间件内部消化掉避免一个日志中间件的 bug 把整个 Agent 搞挂。3.3 多个中间件的执行顺序与短路规则当你注册多个中间件时执行顺序遵循洋葱模型before_*类钩子按注册顺序正序执行after_*类钩子按注册顺序逆序执行这个设计的好处是最外层注册的中间件能包住所有内层逻辑非常适合做统一的计时、异常捕获。比如你把一个计时中间件注册在最前面它就能准确测量从进入 Agent 到退出的完整耗时。短路规则也值得强调任何一个before_tool钩子返回None后续中间件的before_tool和真实工具执行都会被跳过直接进入after_tool阶段。这个特性用来做缓存特别顺手——命中缓存就直接返回不用真的调工具。4. 三个真实场景中间件怎么把 Agent 从玩具变成产品4.1 场景一工具调用参数校验与自动纠错模型调用工具时传错参数是家常便饭。比如一个查询天气的工具要求city是字符串模型可能传个列表或者日期格式要求YYYY-MM-DD模型给了2024/1/5。与其让工具自己报错、再让模型重新理解错误信息不如在中间件里直接拦截修正。import re def param_guard_middleware(): def before_tool(state, tool_call): if tool_call[name] get_weather: city tool_call[args].get(city) if isinstance(city, list): tool_call[args][city] city[0] if not isinstance(tool_call[args].get(city), str): return None # 参数无法修复跳过执行 if tool_call[name] query_orders: date tool_call[args].get(date, ) fixed re.sub(r(\d{4})/(\d{1,2})/(\d{1,2}), r\1-\2-\3, date) tool_call[args][date] fixed return tool_call return createMiddleware(before_toolbefore_tool)这段代码做了两件事把列表类型的city取首元素转成字符串把斜杠日期统一成短横线格式。实测下来光是这两个小修正就能把工具调用的失败率降一大截。注意参数修复要保守只做那些语义明确、不会引入歧义的转换拿不准的宁可跳过让模型重试。4.2 场景二上下文窗口的主动压缩Agent 跑久了消息历史会膨胀到超出模型上下文窗口。常见的做法是等超了再截断但那样会丢失关键信息。更好的策略是在中间件里做主动压缩当消息数超过阈值时把早期的工具调用结果做摘要只保留结论。def context_compressor(max_messages30, keep_recent10): def before_model(state, messages): if len(messages) max_messages: return messages old messages[:-keep_recent] recent messages[-keep_recent:] summary summarize_tool_results(old) # 你自己的摘要逻辑 return [summary] recent return createMiddleware(before_modelbefore_model)这里的summarize_tool_results可以调一个小模型来做也可以用规则提取关键字段。我一般会把工具结果里的状态码、关键数值、错误信息抽出来拼成一段紧凑文本。踩过的坑摘要不能太激进否则模型会忘记自己刚才查过什么导致重复调用工具。我的经验是保留最近 10 条原始消息只压缩更早的部分。4.3 场景三工具失败的兜底与重试工具调用失败时默认行为是把错误信息塞回消息历史让模型自己决定怎么办。但模型经常头铁对着同一个错误反复重试。中间件可以在after_tool里识别失败模式做有限次数的自动重试或者直接返回一个更友好的提示。def retry_middleware(max_retry2): def after_tool(state, tool_call, result): if is_transient_error(result): attempts state.get(retry_count, {}).get(tool_call[id], 0) if attempts max_retry: state.setdefault(retry_count, {})[tool_call[id]] attempts 1 return re_execute_tool(tool_call) # 重新执行 return {error: 服务暂时不可用请稍后再试或换一种方式} return result return createMiddleware(after_toolafter_tool)is_transient_error用来判断是不是临时性错误——比如超时、限流、连接抖动这类值得重试而参数错误、权限不足这类重试也没用直接返回友好提示。这个区分很重要无脑重试只会浪费 token 和时间。5. 中间件与 LangChain 生态的配合别重复造轮子5.1 和 LangChain callback 的分工前面提过 callback 和中间件的区别这里展开说下怎么配合。我的实践是callback 负责记录中间件负责干预。比如用 LangSmith 做全链路追踪那是 callback 的活而当 token 消耗超过预算时中断执行那是中间件的活。两者可以共存。你完全可以在中间件里触发自定义的 callback 事件把干预行为也记录下来这样排查问题时能看到哪个中间件在什么时候改了什么。5.2 和工具装饰器的边界LangChain 的工具本身支持tool装饰器你可以在工具函数内部做校验。那为什么还要中间件因为中间件是横切的装饰器是纵切的。装饰器只能管自己那个工具而中间件能统一管所有工具。当你有一二十个工具、想统一加限流或审计时中间件是唯一不重复的选择。5.3 在 Dify、CrewAI 等框架里的对应概念如果你用过 Dify 或 CrewAI会发现它们也有类似机制。Dify 的工作流节点某种程度上就是中间件思想的可视化版本CrewAI 的step_callback更接近 callback 而非中间件。选型时我的建议是如果你的核心诉求是精细控制执行流程LangChain DeepAgents 中间件的组合灵活度最高如果只是想快速搭个能用的流程可视化编排可能更省事。6. 我在生产环境踩过的几个坑6.1 中间件里的状态污染最开始我把重试计数直接存在模块级变量里结果多个并发请求互相干扰A 请求的重试次数算到了 B 头上。正确做法是把状态挂在state对象上它是每次 Agent 运行独立的。这个坑很隐蔽单线程测试根本发现不了一上并发就出问题。6.2 钩子里做耗时操作拖垮响应我在before_model里加了个调外部接口做敏感词检测的逻辑结果每次模型调用前都要等几百毫秒整体延迟翻倍。后来改成本地缓存 异步预取把检测结果提前算好钩子里只做查表。中间件是执行路径上的关键节点任何耗时操作都要慎重能异步就异步能缓存就缓存。6.3 异常吞掉导致问题难排查前面说钩子里异常要自己兜住但兜住不等于静默。我一度把所有异常都except: pass结果中间件失效了都不知道。后来改成记录日志 上报监控 返回原始数据既不影响主流程又能及时发现问题。这个平衡点需要根据业务重要性来定。6.4 中间件顺序引发的诡异 bug有一次我把参数校验中间件注册在了缓存中间件后面导致缓存命中的请求跳过了参数校验脏参数直接进了缓存。顺序即逻辑注册中间件时一定要想清楚谁先谁后。我的习惯是在代码里用注释标明每个中间件的职责和依赖关系。7. 关于 Agent 中间件的一些延伸思考中间件这个抽象之所以有价值是因为它把Agent 的通用能力和业务的特化需求解耦了。框架提供稳定的执行骨架中间件负责填充千变万化的业务逻辑。这套思路在 Web 开发里已经被验证了二十年搬到 Agent 领域同样成立。如果你正在做 Agent 的工程化我的建议是先把主流程跑通再逐步把横切关注点抽成中间件。不要一上来就设计一套复杂的中间件体系那样容易过度设计。等你在真实场景里遇到第三个每个工具都要做一遍的需求时就是引入中间件的最佳时机。另外中间件的能力边界值得持续关注。目前它主要覆盖执行流程的干预对于模型选型切换多 Agent 协作调度这类更上层的问题还需要配合其他机制。但可以预见随着 Agent 框架的成熟中间件会像 Web 中间件一样形成一个丰富的生态——限流、熔断、可观测性、A/B 测试这些在服务端沉淀多年的模式都会在 Agent 领域重新长一遍。谁能先把这套工程化能力做扎实谁就能在 Agent 落地这件事上少走弯路。