在构建 AI Agent 框架时,用多重继承组合 MemoryMixin、ToolMixin、LoggingMixin,MRO 和 C3 线性化算法是如何决定方法查找顺序的?
好的,我以一个实际 Agent 框架的基础组件为例,把 MRO 和 C3 线性化如何决定方法查找顺序这个问题说透。我会从钻石继承的动机、C3 算法的直觉、到具体 Mixin 链的运行时行为逐一展开,中间用图示。
🧱 多重继承组合 Mixin:为什么需要明确的顺序¶
设计 Agent 时,我们可能这样写:
这三个 Mixin 各自都实现了 execute() 方法,并且内部都用 super().execute() 把调用往后传递。如果 MRO 不清晰,要么某些 Mixin 被跳过,要么调用链死循环,要么不同机器上表现不一。Python 用 C3 线性化 算法把这种复杂的继承图拍平成一条唯一的、无歧义的线性序列,super() 就顺着这条序列一步一步走。
📐 C3 线性化的核心逻辑(直觉版)¶
C3 算法的输入是所有基类的 MRO 序列,输出是一个满足以下约束的列表:
-
子类永远在父类前面(单调性)。
-
基类的书写顺序决定优先级:对于
class D(B, C),B 排在 C 前面。 -
每个祖先只出现一次。
对钻石继承:
如果我们写 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。
用图直观感受这种线性化:
🧪 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)。