跳转至

如何用 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="...") 时,执行顺序:

  1. TimingMixin.act 记录开始时间 → super()

  2. RetryMixin.act 包裹重试逻辑 → super()

  3. 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 系统,就能模拟失败并验证重试次数和延迟。


⚠️ 容易忽略的工程细节

  1. 文档化依赖:用 Protocol 或 docstring 明确 Mixin 期望宿主有哪些属性和方法。否则三个月后没人敢动。

  2. 避免状态冲突:每个 Mixin 在 self.context 里使用专属命名空间,比如 metricscache,防止键名碰撞。

  3. 动态组合:如果需要运行时根据配置拼装,用 type() 动态创建类(但保持 MRO 可控)。

  4. 性能开销:super() 调用链每层有微小开销,但相比 IO 或 LLM 调用可忽略。如果真的有性能瓶颈,可以合并相邻 Mixin。


💡 和装饰器、策略模式的关系

  • 装饰器:适合增强单个方法,但难以共享状态。

  • 策略模式:适合替换算法,但不同维度增强容易类爆炸。

  • Mixin:多维度、可叠加、状态共享,最适合 Agent 这种“同一核心流程需要不同横切关注点”的场景。


🧠 从“搭积木”到“做菜”的思维转变

Mixin 让我想到烹饪中的香料包——你不需要每次都重新调制五香粉,而是把味道封装成独立小包,根据菜谱(场景)选配。Agent 的能力也应该这样:核心流程是白米,Mixin 是配菜,最后炒出扬州炒饭还是蛋炒饭,看你怎么搭。

用这种思路设计的 Agent 底座,半年后需求变更时你会发现,新能力只是多写一个 Mixin 并调整继承顺序,而不是改得满盘皆碎。