如何用 Mixin 为 Agent 设计可插拔的能力组件?
Mixin 这个设计模式,在 Agent 系统里之所以好用,是因为它把“能力”变成了可组装、可测试、可替换的模块。我会从设计原则开始,聊到如何避免组合爆炸,再给一个生产可用的代码结构。
🧭 Mixin 设计的四个铁律¶
遵循这四条,Mixin 就能像 Unix 管道一样串联。
🧩 一个完整的可插拔 Agent 骨架¶
先定义“基类”和“能力协议”,而不是直接写死所有功能。
from typing import Protocol, runtime_checkable
@runtime_checkable
class AgentLike(Protocol):
"""宿主 Agent 必须提供的接口"""
context: dict
async def think(self, message: str) -> dict: ...
async def act(self, action: str, **kwargs) -> str: ...
接着,每个 Mixin 围绕一个关注点,用 super() 协作:
import time, asyncio
class TimingMixin:
"""为 act 添加耗时统计"""
async def act(self, action: str, **kwargs):
start = time.monotonic()
result = await super().act(action, **kwargs)
elapsed = time.monotonic() - start
self.context.setdefault("metrics", []).append(
{"action": action, "latency": elapsed}
)
return result
class RetryMixin:
"""自动重试可恢复错误(如限流)"""
async def act(self, action: str, **kwargs):
for attempt in range(3):
try:
return await super().act(action, **kwargs)
except TemporaryError:
if attempt == 2: raise
await asyncio.sleep(2 ** attempt)
class CacheMixin:
"""对幂等 action 添加结果缓存"""
async def act(self, action: str, **kwargs):
cache_key = f"{action}:{kwargs}"
if cache_key in self.context.get("cache", {}):
return self.context["cache"][cache_key]
result = await super().act(action, **kwargs)
self.context.setdefault("cache", {})[cache_key] = result
return result
注意每个 Mixin 都不直接知晓其他 Mixin 的存在,它们通过 super() 在运行时按 MRO 串联。
🔀 按场景自由组装¶
class MyAgent(TimingMixin, RetryMixin, AgentBase):
def __init__(self):
self.context = {} # 满足 AgentLike
async def act(self, action, **kwargs):
# 最终的实际操作
return await some_tool(action, **kwargs)
MRO 为 MyAgent -> TimingMixin -> RetryMixin -> AgentBase。
调用 act("search", q="...") 时,执行顺序:
-
TimingMixin.act记录开始时间 →super() -
RetryMixin.act包裹重试逻辑 →super() -
AgentBase.act真正执行
如果哪天不需要缓存,就不加 CacheMixin;想要缓存且先于重试执行,就把 CacheMixin 放在 RetryMixin 左边。
🧪 让插拔能力可测试¶
Mixin 模式的最大好处是每个能力可以独立测试。我们构造一个最小化的桩基类:
class FakeAgent(AgentBase):
def __init__(self):
self.context = {}
async def act(self, action, **kwargs):
return f"done-{action}"
# 只测试重试逻辑
class TestAgent(RetryMixin, FakeAgent):
pass
不需要启动完整的 Agent 系统,就能模拟失败并验证重试次数和延迟。
⚠️ 容易忽略的工程细节¶
-
文档化依赖:用
Protocol或 docstring 明确 Mixin 期望宿主有哪些属性和方法。否则三个月后没人敢动。 -
避免状态冲突:每个 Mixin 在
self.context里使用专属命名空间,比如metrics、cache,防止键名碰撞。 -
动态组合:如果需要运行时根据配置拼装,用
type()动态创建类(但保持 MRO 可控)。 -
性能开销:
super()调用链每层有微小开销,但相比 IO 或 LLM 调用可忽略。如果真的有性能瓶颈,可以合并相邻 Mixin。
💡 和装饰器、策略模式的关系¶
-
装饰器:适合增强单个方法,但难以共享状态。
-
策略模式:适合替换算法,但不同维度增强容易类爆炸。
-
Mixin:多维度、可叠加、状态共享,最适合 Agent 这种“同一核心流程需要不同横切关注点”的场景。
🧠 从“搭积木”到“做菜”的思维转变¶
Mixin 让我想到烹饪中的香料包——你不需要每次都重新调制五香粉,而是把味道封装成独立小包,根据菜谱(场景)选配。Agent 的能力也应该这样:核心流程是白米,Mixin 是配菜,最后炒出扬州炒饭还是蛋炒饭,看你怎么搭。
用这种思路设计的 Agent 底座,半年后需求变更时你会发现,新能力只是多写一个 Mixin 并调整继承顺序,而不是改得满盘皆碎。