装饰器和洋葱模式的区分
执行逻辑、嵌套模型、before/after、短路能力 完全一样 。
真正差别就两点:
- 普通装饰器:直接把业务函数包死
- 洋葱中间件:层内部不去碰原始业务,只包装「next」这个代表下一层的回调
普通装饰器
python
def deco(func):
def wrapper(*args, **kwargs):
print("before")
res = func(*args, **kwargs) # 这里直接硬引用真正业务函数func
print("after")
return res
return wrapper
@deco
def business(): pass
func就是最终业务本体。- 定义阶段就把
deco(business)绑定死。
洋葱中间件层(每一层只认识next)
python
def mw(next_func):
def wrapper():
print("before")
res = next_func() # ⚠️我根本不知道next_func是谁,它可能是另一个中间件,也可能是业务
print("after")
return res
return wrapper
👉 mw 完全不知道、不关心最终的 business 存在。它只知道:我有一个叫 next_func 的东西,需要的时候调用它。
链条组装的时候才把它们串起来:
pipeline = mw1( mw2( mw3( business ) ) )
- mw1 的 next_func = mw2包装后的函数
- mw2 的 next_func = mw3包装后的函数
- mw3 的 next_func = business
每一层只和自己的直接邻居打交道,对整条链条一无所知。
用一句话区分
- 装饰器:层直接包裹业务函数;绑定发生在写代码/导入时,静态固化。
- 洋葱中间件:每一层只包裹"下一层",不一定是业务;链条是运行时动态拼出来的。
运行时动态拼,带来两个核心能力,装饰器很难低成本做到:
- 同一个业务本体,可以搭出多套不同链条
python
# 业务还是那个business
p1 = build_pipeline([mw1, mw2], business)
p2 = build_pipeline([mw3], business)
p3 = build_pipeline([], business)
p1/p2/p3 业务不变,但中间件链路完全不一样。
用装饰器要实现,你得手动写好多个被不同装饰器包装的副本。
- 中间件之间完全解耦,插拔式
你改中间件列表,不需要修改任何中间件源码,也不用碰业务代码。
但是执行流是100%同源
不管是多层@decorator,还是洋葱中间件:
-
before:外层 → 内层
-
after:内层 → 外层
-
不调用内层函数就短路
外层before
内层before
业务
内层after
外层after
所以你的感觉没错:运行行为几乎一样,只是组装时机和耦合度不同。
- 装饰器:编译期静态组装,强绑定目标函数。
- 洋葱中间件:运行时动态组装,层与层之间只通过next回调做间接连接。
每一层中间件,都不知道完整链路,只知道调用传给我的next往下走。框架负责做那个 build_pipeline 的反向循环组装。
洋葱模式一个很关键的约束:整条链条所有中间件,必须约定一套统一的入参/出参协议。
不是"只有第一层有入参",而是:请求对象从头传到尾,每一层接收同一个请求对象;next 返回的也必须是约定好的响应对象。
对比普通装饰器
普通装饰器:
python
def deco(func):
def wrapper(*args,**kwargs):
return func(*args,**kwargs)
return wrapper
用*args,**kwargs兼容任意函数签名,牺牲类型安全,什么参数都能透传。
洋葱中间件不玩可变参数,强制统一结构体:
- FastAPI Starlette中间件:
python
async def mw(request: Request, call_next):
# 所有中间件,第一个参数必须是 Request
resp = await call_next(request)
return resp
- 入参:
Request对象,整条链全部共用这一个对象实例 - 出参:必须返回
Response对象
你不能随便自定义参数,所有中间件都要遵守这套契约。
- DeepAgents
@wrap_tool_call
python
def mw(request: ToolCallRequest, handler):
res = handler(request)
return res
- 入参固定:
ToolCallRequest结构体 - 每一层都接收同一个 request 对象,传给下一层
handler(request) - 返回值也必须是工具调用的标准结果格式。
关键点:对象是同一个实例,可以原地修改
request 是可变对象,不是拷贝。
每一层拿到的是同一个对象引用,中间件可以原地修改这个结构体,下层中间件/业务直接看到修改后的内容。
示例 Starlette:
python
async def mw1(request: Request, call_next):
request.state.user_id = 1001 # 原地修改请求对象
resp = await call_next(request)
return resp
async def mw2(request: Request, call_next):
print(request.state.user_id) # 拿到mw1设置的值,同一个对象
return await call_next(request)
DeepAgents 同理:你可以在前面的中间件修改 request.tool_call["args"],后面中间件和真实工具直接拿到修改后的参数。
⚠️但不允许:随便新增、删减函数位置参数。不能mw接收2个参数,下一层接收3个参数,契约直接崩掉。
那参数是谁传进来的?
只有最外层外面的调用方 ,生成这个初始请求对象,传给链条第一层。
链条内部每一层,把同一个对象继续传给 next/call_next/handler。
调用方构建 request → 传给第一层mw1
mw1收到request,传给call_next(request) → mw2
mw2收到同一个request,传给call_next(request) → mw3
mw3收到同一个request,传给业务handler
业务处理完,产出response,原路逐层返回
整条链内部,不会重新构造新的请求参数,只会复用同一个结构体。
所以你的原话修正:
✅洋葱模式需要一套固定的结构体契约;初始入参由链条外部调用方生成,之后整条链路流转同一个对象实例。所有中间件必须遵守统一的入参出参类型。
带来的代价和收益
✅收益
- 类型清晰,框架好管控;
- 中间件之间传递数据靠对象属性(
request.state),不用搞丑陋的函数参数透传; - 每一个中间件可以独立单元测试,只要构造一个符合契约的request对象,直接调用中间件即可。
❌代价(就是你感受到的)
- 签名被锁死,不能随便自定义函数参数;
- 如果想换一套参数模型,整条中间件链全部要改;
- 跨层传数据,只能塞到这个统一对象里面(state字段)。
和装饰器再次对比
- 普通装饰器:
*args, **kwargs万能透传,不限制签名;但是类型差,隐式传参混乱。 - 洋葱中间件:强契约,固定结构体对象;牺牲签名自由度,换取可插拔、统一管理。
这也是为什么 FastAPI、DeepAgents 中间件都不用裸参数,全部封装成一个大对象(Request / ToolCallRequest)。
所有附加信息、上下文、元数据,全部塞到这个对象的属性里面往下传递,而不是增加函数参数。
一句话总结
洋葱模式不是只有第一层有入参;是整条链路全部共用同一个固定契约的对象实例,外部调用方生成初始对象,层层往下透传,中间件内部原地修改对象属性来传递上下文,不能随意增减函数参数。