跳转至

在构建 AI Agent 框架时,用多重继承组合 MemoryMixin、ToolMixin、LoggingMixin,MRO 和 C3 线性化算法是如何决定方法查找顺序的?

好的,我以一个实际 Agent 框架的基础组件为例,把 MRO 和 C3 线性化如何决定方法查找顺序这个问题说透。我会从钻石继承的动机、C3 算法的直觉、到具体 Mixin 链的运行时行为逐一展开,中间用图示。


🧱 多重继承组合 Mixin:为什么需要明确的顺序

设计 Agent 时,我们可能这样写:

class Agent(MemoryMixin, ToolMixin, LoggingMixin, BaseAgent):
    pass

这三个 Mixin 各自都实现了 execute() 方法,并且内部都用 super().execute() 把调用往后传递。如果 MRO 不清晰,要么某些 Mixin 被跳过,要么调用链死循环,要么不同机器上表现不一。Python 用 C3 线性化 算法把这种复杂的继承图拍平成一条唯一的、无歧义的线性序列,super() 就顺着这条序列一步一步走。


📐 C3 线性化的核心逻辑(直觉版)

C3 算法的输入是所有基类的 MRO 序列,输出是一个满足以下约束的列表:

  1. 子类永远在父类前面(单调性)。

  2. 基类的书写顺序决定优先级:对于 class D(B, C),B 排在 C 前面。

  3. 每个祖先只出现一次。

对钻石继承:

      A
     / \
    B   C
     \ /
      D

如果我们写 class D(B, C): ...,C3 会这样合并:

  • D 的线性化 = D + merge(B 的 MRO, C 的 MRO, [B, C])

  • B 的 MRO = [B, A, object]

  • C 的 MRO = [C, A, object]

  • merge 从左到右挑一个只在序列头部出现(不在其他序列尾部)的类。最终得到: D → B → C → A → object

如果把基类顺序反一下:class D(C, B),则 MRO 变成 D → C → B → A → object

用图直观感受这种线性化:

  继承图                C3 线性化后的调用链
    A                      D
   / \                     │
  B   C        ──▶        B
   \ /                    │
    D                     C
                          A
                        object

🧪 Agent 三 Mixin 实例:看 MRO 如何决定链

假设我们有三个 Mixin 和一个基础类,结构如下:

class BaseAgent:
    def execute(self, task):
        return f"BaseAgent: {task}"

class LoggingMixin:
    def execute(self, task):
        print(f"[LOG] Start {task}")
        res = super().execute(task)
        print(f"[LOG] End {task}")
        return res

class ToolMixin:
    def execute(self, task):
        print(f"[TOOL] Resolving tools for {task}")
        res = super().execute(task)
        print(f"[TOOL] Tools cleaned up")
        return res

class MemoryMixin:
    def execute(self, task):
        print(f"[MEM] Recall context for {task}")
        res = super().execute(task)
        print(f"[MEM] Saving context")
        return res

现在定义 Agent,注意基类顺序:

class Agent(MemoryMixin, ToolMixin, LoggingMixin, BaseAgent):
    pass

print(Agent.__mro__)
# (Agent, MemoryMixin, ToolMixin, LoggingMixin, BaseAgent, object)

用图表示就是:

定义顺序:
Agent(MemoryMixin, ToolMixin, LoggingMixin, BaseAgent)

对应 MRO 线性链:
Agent ──▶ MemoryMixin ──▶ ToolMixin ──▶ LoggingMixin ──▶ BaseAgent ──▶ object

调用 agent.execute("query") 的实际执行流程:
  1. MemoryMixin.execute("query")   # [MEM] Recall context...
  2.   → super() 指到 ToolMixin.execute("query")  # [TOOL] Resolving tools...
  3.     → super() 指到 LoggingMixin.execute("query") # [LOG] Start query
  4.       → super() 指到 BaseAgent.execute("query")  # BaseAgent: query
  5.     ← LoggingMixin 结束  # [LOG] End query
  6.   ← ToolMixin 结束      # [TOOL] Tools cleaned up
  7. ← MemoryMixin 结束      # [MEM] Saving context

这个顺序完全由定义类时基类的从左到右书写顺序决定。Memory 在最左,所以它的 execute 最先被调,并且最后返回,像一个“外层包装”。如果想要先记日志,再把工具解析包在里面,最后才加载记忆,可以把基类改成 (LoggingMixin, ToolMixin, MemoryMixin, BaseAgent)