news 2026/10/31 13:49:36

DeepAgents中间件实战:从胶水代码到可维护的Agent工程化架构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DeepAgents中间件实战:从胶水代码到可维护的Agent工程化架构

1. 从“胶水代码”到“中间件”:DeepAgents 到底在解决什么问题

如果你最近半年在折腾 AI Agent,大概率经历过这样一个阶段:一开始用 LangChain 的AgentExecutor跑通一个能调用工具的对话机器人,感觉挺爽;然后想加个“对话历史压缩”“工具调用重试”“敏感词过滤”“Token 用量统计”,于是开始往主流程里塞各种if-else和回调函数。再往后,业务方要求“同一个 Agent 在不同渠道要有不同的行为”“工具调用前要过审批”“长任务要能断点续跑”,你发现自己的agent.py已经膨胀到八百行,改一处崩三处。

这就是典型的“胶水代码地狱”。而DeepAgents 中间件这个项目,本质上就是冲着这个痛点来的——它把 Agent 执行过程中那些横切的、可复用的、与具体业务逻辑无关的能力,抽成一层可插拔的中间件管道。你可以把它理解成 Web 框架里的 middleware:请求进来,先过日志、再过鉴权、再过限流,最后才到业务 handler;Agent 也一样,用户输入进来,先过上下文管理、再过工具权限校验、再过 Token 预算控制,最后才到模型推理。

我最早接触这个概念是在 LangChain 生态里看到createMiddleware这个 API 设计,当时第一反应是“这不就是 Koa 的洋葱模型吗”。后来实际用 DeepAgents 跑了一个多月的生产任务,才真正体会到中间件化对 Agent 工程化的价值。它解决的不是“能不能跑通”的问题,而是“能不能维护、能不能观测、能不能安全上线”的问题。

这篇文章适合三类人看:一是已经用 LangChain 或类似框架搭过 Agent、但被维护成本折磨的开发者;二是正在做 Agent 平台化、需要给多个业务方提供统一能力的团队;三是对 Agent 架构演进感兴趣、想了解“中间件”这层抽象到底怎么落地的人。我会从设计思路、核心机制、实操搭建、踩坑排查四个维度展开,尽量把每个“为什么这么设计”讲透,而不是只丢一段能跑的代码。

提示:本文涉及的代码示例基于 LangChain 生态的通用模式,DeepAgents 的具体 API 名称可能随版本变化,重点是理解中间件的分层思想和实现套路,迁移到其他框架(如 Dify、CrewAI 的插件机制)也是相通的。

2. 中间件分层设计:为什么不是“写个装饰器就完事”

2.1 从洋葱模型理解 Agent 中间件的执行顺序

很多人第一次听到“Agent 中间件”,会下意识觉得“不就是给工具函数加个装饰器吗”。这个理解只对了一半。装饰器解决的是单个函数的增强,而中间件解决的是整条执行链路的编排。区别在哪?我举个例子。

假设你的 Agent 一次完整执行是这样的:接收用户消息 → 加载历史 → 拼接 Prompt → 调用 LLM → 解析工具调用 → 执行工具 → 把结果回填 → 再次调用 LLM → 返回最终答案。如果只用装饰器,你只能分别增强“调用 LLM”和“执行工具”这两个点,但像“Token 预算在整条链路累计消耗”“上下文在多次 LLM 调用之间动态压缩”这类跨阶段逻辑,装饰器就无能为力了。

DeepAgents 的中间件采用的是洋葱模型,也就是每个中间件都有“进入”和“退出”两个切面。请求从最外层中间件进入,逐层向内,到达核心执行器后再逐层向外返回。这个设计的好处是:你可以在“进入”时做前置校验(比如检查用户配额),在“退出”时做后置处理(比如统计本次消耗、写入审计日志),而且外层中间件能拿到内层所有阶段的汇总结果。

# 中间件洋葱模型的伪代码示意 class Middleware: def before(self, context): # 进入时执行,从外到内 pass def after(self, context, result): # 退出时执行,从内到外 pass # 执行顺序:A.before -> B.before -> core -> B.after -> A.after pipeline = [MiddlewareA(), MiddlewareB()]

这个顺序非常关键。我踩过一个坑:把“Token 统计中间件”放在了最内层,结果发现它只能统计到单次 LLM 调用的消耗,拿不到工具调用产生的额外 Token(比如某些工具内部也会调模型)。后来把它移到最外层,才拿到了完整的链路消耗。所以中间件的位置本身就是一种语义,放错位置等于逻辑写错。

2.2 核心中间件类型拆解:哪些能力值得抽出来

不是所有逻辑都值得做成中间件。我的判断标准是:是否横切多个 Agent、是否与业务无关、是否需要独立配置。满足这三条的,才值得抽。基于这个标准,DeepAgents 这类项目通常会内置以下几类中间件。

中间件类型核心职责典型触发时机是否可省略
上下文管理历史压缩、摘要生成、窗口裁剪每次 LLM 调用前短对话可省
工具权限校验白名单、参数校验、审批流工具执行前生产环境必开
Token 预算控制累计消耗统计、超限熔断全链路按量付费必开
重试与降级失败重试、模型切换、兜底回复LLM/工具调用失败时建议开启
可观测性日志、Trace、指标上报全链路建议开启
内容安全输入输出过滤、敏感词拦截输入后、输出前合规必开

这张表是我自己在项目里总结的,不一定和官方文档完全一致,但覆盖了 90% 的实战场景。你会发现一个规律:越靠近“核心推理”的中间件越偏技术,越靠近“外层”的中间件越偏业务和合规。这个分层不是随便定的,它决定了你的中间件栈应该怎么排。

2.3 为什么用createMiddleware而不是继承基类

LangChain 生态里createMiddleware这种工厂函数的设计,一开始我觉得多此一举——直接写个类继承BaseMiddleware不香吗?用了一段时间才明白它的用意:降低心智负担,鼓励组合而非继承。

继承的问题在于,一旦你的中间件需要复用另一段逻辑,就得处理多继承或者把逻辑塞进基类,最后基类越来越胖。而工厂函数返回的是一个配置对象,你可以用函数组合的方式把多个小能力拼起来。比如“带重试的日志中间件”就是“重试中间件”和“日志中间件”的组合,而不是一个继承了两者的新类。

# 工厂函数风格:组合优于继承 def create_logging_middleware(logger): def handler(context, next_handler): logger.info(f"enter: {context.step}") result = next_handler(context) logger.info(f"exit: {context.step}") return result return handler def create_retry_middleware(max_retries): def handler(context, next_handler): for i in range(max_retries): try: return next_handler(context) except Exception as e: if i == max_retries - 1: raise return None return handler

这种写法还有个隐性好处:测试极其方便。每个工厂函数返回的都是纯函数,你可以单独 mocknext_handler来验证前置逻辑,不用启动整个 Agent。我在做 Token 预算中间件的单元测试时,就是靠这个特性把覆盖率做到了 90% 以上。

3. 核心机制深挖:中间件管道是怎么跑起来的

3.1 执行上下文(Context)的设计与传递

中间件之间靠什么通信?答案是执行上下文。这是整个中间件体系里最容易被低估、也最容易设计错的部分。上下文本质上是一个贯穿整条链路的数据袋,里面装着用户输入、历史消息、当前步骤、累计消耗、临时变量等等。

设计上下文时有两个极端要避免。一是上下文太瘦,只放一个messages列表,结果每个中间件都要自己去查数据库、调接口拿信息,重复劳动且不一致。二是上下文太胖,把所有东西都塞进去,导致序列化困难、内存暴涨、调试时打印出来几千行。

我的经验是遵循“按需注入、分层存放”原则。把上下文分成三层:不可变的请求元信息(用户 ID、会话 ID、渠道)、可变的执行状态(当前步骤、重试次数、累计 Token)、以及中间件私有的临时数据(用命名空间隔离,避免键冲突)。

class ExecutionContext: def __init__(self, user_input, session_id): # 不可变层 self.request = {"input": user_input, "session_id": session_id} # 可变状态层 self.state = {"step": 0, "tokens_used": 0, "retries": 0} # 中间件私有层,按中间件名隔离 self.scratch = {} def get_scratch(self, namespace): return self.scratch.setdefault(namespace, {})

这个scratch命名空间的设计是我踩坑后加的。早期所有中间件共用一个字典,结果两个中间件都用了temp这个键,互相覆盖,排查了半天才发现。加上命名空间后,每个中间件只能访问自己的区域,冲突问题彻底消失。

3.2 工具调用拦截:权限、参数与审批流

工具调用是 Agent 最容易出安全事故的地方。模型可能被诱导调用删除数据的工具,也可能传入非法参数导致下游服务崩溃。中间件在这里的价值,就是在“模型决定调用”和“工具真正执行”之间插一道闸门。

DeepAgents 的工具拦截中间件通常做三件事。第一是白名单校验,检查工具名是否在当前 Agent 的允许列表里。第二是参数 Schema 校验,用 Pydantic 或 JSON Schema 验证参数类型和范围。第三是审批流,对于高危工具(比如发邮件、改数据库),先挂起等待人工确认。

def create_tool_guard_middleware(allowed_tools, high_risk_tools): def handler(context, next_handler): tool_call = context.state.get("pending_tool_call") if tool_call: if tool_call["name"] not in allowed_tools: raise PermissionError(f"工具 {tool_call['name']} 不在白名单") if tool_call["name"] in high_risk_tools: # 挂起等待审批,实际项目中会写入审批队列 context.state["awaiting_approval"] = True return {"status": "pending_approval"} return next_handler(context) return handler

这里有个实操细节:审批挂起后,Agent 的状态要能持久化。否则用户审批完,进程重启,上下文丢了,任务就断了。我在项目里是把上下文序列化后存 Redis,审批回调时再反序列化恢复。这也是为什么热词里会出现“redis 做中间件”——它不只是缓存,更是 Agent 长任务状态的中转站。

3.3 Token 预算与熔断:别让一次对话烧掉一个月预算

Token 消耗是 Agent 成本的大头,尤其是带工具循环的场景,一次任务可能调用十几次 LLM。没有预算控制的 Agent,就像没有限额的信用卡,迟早出事。Token 预算中间件的核心逻辑是:在每次 LLM 调用前预估消耗,调用后累计实际消耗,超过阈值就熔断。

预估消耗有个简单公式:预估 Token = 输入字符数 / 4 + 最大输出 Token。这个 4 是英文的平均字符 Token 比,中文大概是 1.5 到 2 个字符一个 Token,所以中文场景要调整系数。我一般会留 20% 的缓冲,避免预估偏低导致超支。

def create_token_budget_middleware(max_tokens, buffer_ratio=0.2): def handler(context, next_handler): used = context.state["tokens_used"] estimated = estimate_tokens(context) * (1 + buffer_ratio) if used + estimated > max_tokens: # 熔断,返回兜底回复而不是抛异常 return {"status": "budget_exceeded", "message": "本次任务已达预算上限"} result = next_handler(context) context.state["tokens_used"] += result.get("tokens", 0) return result return handler

注意:熔断时返回兜底回复而不是抛异常,是因为抛异常会中断整个 Agent 循环,用户看到的是报错;而兜底回复能让用户知道“任务被截断了”,体验更好。这个细节在官方文档里通常不会写,但生产环境很关键。

4. 从零搭建一条中间件管道:完整实操

4.1 环境准备与依赖选型

动手之前先把环境理清楚。DeepAgents 本身是 LangChain 生态的延伸,所以基础依赖是langchain和langchain-core。如果你要用 Redis 做状态存储,加redis和redis-py;要做可观测性,加langfuse或opentelemetry;参数校验用pydantic(LangChain 本身就依赖它)。

pip install langchain langchain-core redis pydantic # 可观测性可选 pip install langfuse opentelemetry-sdk

版本上有个坑要提醒:LangChain 的 API 在 0.1 到 0.3 之间变动很大,AgentExecutor在 0.3 之后逐渐被 LangGraph 取代。如果你看的教程和你的版本对不上,先pip show langchain确认版本,别硬套。我建议新手直接用较新的版本,因为中间件相关的抽象在新版本里更成熟。

4.2 定义第一个中间件:请求日志与耗时统计

第一个中间件我建议从最简单的日志开始,目的是验证管道能跑通,同时建立对执行顺序的直觉。

import time import logging logger = logging.getLogger("agent.middleware") def create_logging_middleware(): def handler(context, next_handler): step = context.state["step"] start = time.time() logger.info(f"[step {step}] 进入中间件,输入长度={len(context.request['input'])}") try: result = next_handler(context) elapsed = time.time() - start logger.info(f"[step {step}] 执行完成,耗时={elapsed:.2f}s") return result except Exception as e: elapsed = time.time() - start logger.error(f"[step {step}] 执行失败,耗时={elapsed:.2f}s,错误={e}") raise return handler

这段代码看起来平平无奇,但它建立了一个重要模式:中间件要同时处理成功和失败两条路径。很多新手只写成功路径,结果一出错日志就断了,排查时两眼一抹黑。把try-except加上,失败时也记录耗时和错误,这才是生产级日志。

4.3 组装管道:顺序决定行为

中间件的组装顺序直接决定行为,这是整个实操里最需要动脑的地方。我的一般原则是:可观测性在最外层,安全校验在次外层,业务逻辑相关的在内层,核心执行器在最内。

def build_pipeline(middlewares, core_handler): # 从右到左包裹,最左边的最先执行 handler = core_handler for mw in reversed(middlewares): handler = mw(handler) return handler pipeline = build_pipeline( middlewares=[ create_logging_middleware(), # 最外层:日志 create_token_budget_middleware(100000), # 次外层:预算 create_tool_guard_middleware(allowed, high_risk), # 内层:工具校验 create_context_middleware(), # 最内层:上下文管理 ], core_handler=agent_core )

为什么日志放最外层?因为它要记录整条链路的耗时,包括所有中间件的开销。为什么预算放次外层?因为它要在工具校验之前就拦住超支请求,避免白跑一遍校验。为什么上下文管理放最内层?因为它最贴近核心执行器,改动频率最高,放内层方便单独替换。

这个顺序不是绝对的,但每次调整都要问自己:这个中间件需要看到“原始请求”还是“处理后的请求”?需要拦截“所有下游”还是“部分下游”?想清楚这两个问题,顺序自然就定了。

4.4 接入 Redis 做状态持久化

长任务场景下,中间件的执行状态必须能跨进程恢复。我用 Redis 存两类数据:一是会话级的上下文快照,二是审批挂起时的待办队列。

import json import redis r = redis.Redis(host="localhost", port=6379, db=0) def save_context(session_id, context): # 只序列化可变状态和 scratch,请求元信息可重建 payload = { "state": context.state, "scratch": context.scratch, } r.setex(f"agent:ctx:{session_id}", 3600, json.dumps(payload)) def load_context(session_id, user_input): raw = r.get(f"agent:ctx:{session_id}") ctx = ExecutionContext(user_input, session_id) if raw: payload = json.loads(raw) ctx.state = payload["state"] ctx.scratch = payload["scratch"] return ctx

这里有个坑:scratch里如果存了不可序列化的对象(比如数据库连接、函数引用),json.dumps会直接报错。我的做法是在中间件里约定,scratch只存基础类型和字典列表,需要存复杂对象时用 ID 引用,恢复时再查回来。这个约定要写进团队规范,否则迟早有人往里塞奇怪的东西。

5. 常见问题与排查技巧实录

5.1 中间件不生效?先查这三个地方

中间件写了但没执行,是最常见的问题。我总结了三个排查方向,按顺序查基本能定位。

第一,检查组装顺序。build_pipeline是从右到左包裹的,如果你把中间件列表理解反了,可能你以为在最外层的实际在最内层,被其他中间件短路了。打印一下最终 handler 的包裹结构,或者在最外层中间件打个日志,看是否被调用。

第二,检查短路逻辑。某个中间件如果提前return了,后面的中间件和核心执行器都不会跑。比如预算中间件熔断后直接返回,工具校验中间件自然就不会执行。这是设计如此,但如果你忘了,就会误以为“中间件没生效”。

第三,检查异常吞没。有些中间件用try-except包住了next_handler但没重新抛出异常,导致错误被静默吞掉,表现为“中间件好像跑了但没效果”。记住:捕获异常后要么处理,要么重新抛出,绝不能默默吞掉。

现象可能原因排查方法
中间件日志完全不打印组装顺序错误或未加入管道打印 handler 包裹链
部分中间件不执行上游中间件短路返回检查各中间件的 return 分支
中间件执行了但结果不对上下文键冲突或数据被覆盖用 scratch 命名空间隔离
报序列化错误scratch 存了不可序列化对象只存基础类型,复杂对象存 ID

5.2 Token 统计对不上?可能是这三个原因

Token 统计不准是另一个高频问题。我遇到过三种情况。一是只统计了 LLM 调用,漏了工具内部的模型调用。有些工具(比如摘要工具)内部也会调 LLM,这部分消耗如果不算进去,总账就对不上。解决办法是把统计中间件放最外层,或者让工具也走同一套统计逻辑。

二是流式输出的 Token 统计时机不对。流式场景下,Token 是逐步产生的,如果你在流开始时就统计,拿到的是 0;在流结束后统计,又可能因为异常中断而漏统计。正确做法是监听流的on_llm_end回调,在回调里累加。

三是不同模型的 Token 计算方式不同。同样一段中文,不同厂商的 Tokenizer 切出来的 Token 数可能差 20% 以上。如果你的 Agent 支持多模型切换,统计逻辑要按模型分别处理,不能用一个系数套所有。

5.3 审批挂起后任务丢失?状态持久化的坑

审批流是中间件里最容易出问题的场景。用户提交任务,中间件挂起等待审批,结果服务重启,任务没了。或者审批通过后,恢复执行时上下文对不上,工具参数丢了。

我的解决方案是双写 + 幂等恢复。挂起时,把上下文快照和待审批的工具调用一起写入 Redis,同时给这个任务分配一个唯一 ID。审批回调时,用这个 ID 找回上下文,重新注入管道,从挂起点继续执行。关键是恢复时要跳过已经执行过的中间件,否则日志会重复、预算会重复扣。

def resume_from_approval(task_id, approved): payload = json.loads(r.get(f"agent:task:{task_id}")) if not approved: return {"status": "rejected"} ctx = rebuild_context(payload) ctx.state["resume_from"] = payload["checkpoint"] # 从检查点恢复,跳过已执行的中间件 return pipeline_resume(ctx)

这个checkpoint机制是我踩了两次坑之后加的。第一次没做检查点,恢复时从头跑,结果工具被调用了两次,差点造成数据重复。第二次做了检查点但没做幂等,网络重试导致恢复执行了两次。第三次才把幂等键加上,用任务 ID + 步骤号做唯一约束,彻底解决。

5.4 性能优化:中间件太多导致延迟上升

中间件不是越多越好。我做过一个测试,在一条链路上加到 8 个中间件后,单次请求的额外延迟到了 200ms 以上,其中大部分是序列化和日志 IO 的开销。优化方向有三个。

一是合并同类中间件。比如“日志”和“指标上报”可以合并成一个可观测性中间件,共享一次上下文遍历。二是异步化 IO 操作。日志写文件、指标上报这些不阻塞主流程的操作,丢到后台队列异步处理。三是按需启用。不是所有请求都需要全量中间件,比如内部调试请求可以跳过安全校验,短对话可以跳过上下文压缩。

# 按需启用:根据请求特征动态选择中间件 def select_middlewares(context): mws = [logging_mw, budget_mw] if context.request.get("channel") != "internal_debug": mws.append(tool_guard_mw) if len(context.request["input"]) > 2000: mws.append(context_compress_mw) return mws

这个动态选择的设计让我们的平均延迟降了 40% 左右。核心思路是:中间件是能力,不是负担,按需加载才是正解。

6. 中间件化之后,Agent 工程还能往哪走

把中间件这层抽象跑通之后,我发现很多之前觉得棘手的问题都有了新的解法。比如多租户场景,不同租户要不同的工具权限和预算,以前是给每个租户写一套 Agent,现在是同一套 Agent 配不同的中间件组合,配置化解决。再比如 A/B 测试,想对比两种上下文压缩策略的效果,以前要改代码重新部署,现在是两个中间件实现,按流量比例挂载,线上直接对比。

还有一个我觉得很有潜力的方向是中间件的可观测性反哺。当所有关键逻辑都走中间件后,你天然获得了一条完整的执行轨迹:每个中间件进入退出、每步耗时、每次 Token 消耗、每次工具调用。把这些数据沉淀下来,可以做成本归因、可以做异常检测、甚至可以做自动调优——比如发现某个工具调用失败率高,自动降级到备用工具。

我个人在实际操作中的体会是,中间件这套东西的价值不在于“少写代码”,而在于“把变化点集中管理”。Agent 这个领域变化太快了,模型在换、工具在加、合规要求在变,如果每次变化都要动核心逻辑,系统迟早失控。中间件让你把变化挡在外围,核心执行器保持稳定。这个思路,和当年 Web 框架从“面条式代码”演进到 MVC 再到中间件管道,本质上是一回事。

最后分享一个小技巧:如果你刚开始引入中间件,别一上来就搞七八个。先从日志和预算两个开始,跑通管道、建立直觉,再逐步往里加。每加一个中间件,都问自己“它解决的是哪类问题、放在哪一层、失败了怎么兜底”。想清楚这三个问题再加,比事后返工省事得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 9:55:21

Agent Harness实战:用OpenWorkBuddy把LLM概率输出拧成可验收办公产物

1. 从“能聊”到“能交活”:OpenWorkBuddy 到底在解决什么问题大模型接入办公场景这件事,过去两年我踩过的坑比写过的代码还多。最开始大家兴奋于“对话即入口”,觉得只要把 LLM 接进 IM 或者文档工具,办公自动化就水到渠成了。实…

作者头像 李华
网站建设 2026/10/31 9:35:37

AI Agent上下文工程实战:ReAct循环中的上下文压缩与记忆分层

1. 为什么上下文工程成了 AI Agent 的胜负手做 AI Agent 的人,十有八九都经历过这样的场景:模型本身能力不差,工具也接好了,ReAct 循环也跑通了,但一到真实任务上就开始犯迷糊——要么把之前确认过的信息忘得一干二净&…

作者头像 李华
网站建设 2026/10/24 10:17:29

AI Agent工程化实战:七要素拆解与七个关键决策点全复盘

这两年真正动手做过 AI Agent 的人,大概率都有过同一个体验:模型本身并不差,差的是怎么把模型组织成一个能稳定干活的系统。我最早接触 Agent 的时候,想法非常朴素——把大模型接上几个工具,让它自己调用就行了。结果一…

作者头像 李华
网站建设 2026/10/24 4:21:35

工业互联网安全态势感知:从架构设计到落地实操的完整指南

1. 工业互联网安全态势感知到底在解决什么问题1.1 从一个真实场景说起我在一家大型制造企业做安全架构咨询的时候,遇到过这样一件事:厂区的MES系统突然出现异常,产线上的PLC频繁重启,工程师排查了半天以为是设备老化,结…

作者头像 李华
网站建设 2026/10/28 6:28:50

AI Agent开发实战:从AgentCore到记忆系统与安全的全链路解析

1. 从一份调研报告说起:Agent 开发者到底在关心什么2026 年刚开年,Alibaba Cloud 发布了一份 AI Agent Handbook,同时附带了一份 Agent 开发者调研报告。这两份东西放在一起看,信息量其实很大。我前后翻了三遍,又结合自…

作者头像 李华