“在软件设计中,继承是白盒复用,组合是黑盒复用。白盒让你看到太多你不该关心的细节,黑盒让你只关注你需要的接口。” —— GoF《设计模式》


TL;DR

本文核心观点:

  1. 强耦合与脆弱性 — 继承带来静态绑定、脆弱基类、违反封装等固有问题,任何对基类的修改都可能动摇整个继承树。
  2. 组合的优势 — 组合通过 has-a 关系实现松耦合、高内聚,组件可在运行时动态注入、替换和移除,系统因此具备高度灵活性。
  3. 从类组合到 Agent 组合 — AI Agent 的能力是动态发现、上下文敏感的,继承的静态结构无法适应这种不确定性,三层组合模型(能力/行为/记忆)是更优解。
  4. 组合不是银弹 — 过度组合同样有害,Mediator 模式是解决组件间耦合过密问题的推荐方案。

2026-03-15-composition-01-stack 图示



继承与组合:三十年争论的启示

GoF 设计模式的启示

1994年,四位软件工程大师(Gang of Four)在《设计模式》一书中写下了 software engineering 史上最具影响力的建议之一:

“Favor object composition over class inheritance.” (优先使用对象组合,而非类继承。)

这句话不是否定继承的价值,而是指出了继承被滥用的普遍问题。

Java 时代的继承滥用

在 2000 年代的 Java 企业开发中,继承滥用达到了顶峰:

继承带来的问题:

问题 表现 后果
脆弱的基类 修改父类影响所有子类 不敢改动核心代码
层次过深 继承链超过 3-4 层 理解成本高,调试困难
强耦合 子类依赖父类实现细节 无法独立演化
违反封装 子类知道父类内部 封装被破坏

组合的崛起

Go 语言的横空出世,彻底颠覆了 “一切皆类” 的思维模式:

Go 没有继承,只有组合。这不是缺陷,而是设计上的刻意选择。


为什么组合更优

1. 灵活性:运行时 vs 编译时

💡 Key Insight

继承的能力在编译时固定,组合的能力可在运行时替换——这决定了系统的演化方向。

继承是静态绑定: 继承关系在编译时确定,子类无法在运行时改变从父类继承来的行为。

组合是动态绑定: 组合的对象可在运行时注入、替换或移除,系统行为因此具备高度灵活性。

💡 Key Insight

对于需要动态调整能力的 Agent 系统,动态绑定是刚需,组合是唯一可靠的选择。

2. 解耦:接口 vs 实现

💡 Key Insight

“是一个” 带来继承链的传递耦合,”有一个” 则只依赖目标接口——这是解耦的根本差异。

继承的问题是 “是一个”: 子类通过 is-a 关系继承父类,同时也继承了父类的实现细节,父类的变化会直接传导至所有子类。

组合的优势是 “有一个”: 组合只通过 has-a 关系依赖目标接口,实现可替换,调用方无需关心内部细节。

💡 Key Insight

接口契约隔离变化,组合是实现接口隔离原则(ISP)最直接的途径。

3. 测试性:可替换的依赖

💡 Key Insight

单元测试的核心是隔离被测单元——组合通过依赖注入实现真隔离,继承则把父类实现也拉进测试边界。

继承的测试困境: 子类实例化时必须同时初始化父类,测试子类行为往往要连带父类一起覆盖,无法独立进行。

组合的测试便利: 通过依赖注入将 Mock 对象注入被测对象,测试逻辑与被测单元一一对应,Mock/Stub 随心所欲。

💡 Key Insight

可测试性是代码质量的照妖镜——如果测试写起来又贵又慢,设计大概率有问题。

从类组合到 Agent 组合

Agent 系统的特殊性

传统软件组件之间的关系是静态的,但 Agent 系统有独特的挑战:

  1. 动态能力发现:Agent 可能在运行时学习新技能
  2. 上下文依赖:同一个 Agent 在不同上下文中表现不同
  3. 多模态交互:需要组合不同类型的输入/输出能力
  4. 协作演化:多个 Agent 协作时能力需要动态调整

Agent 组合的三层模型

为什么 Agent 更需要组合

💡 Key Insight

Agent 的能力是动态发现的、上下文敏感的,继承的静态结构无法适应这种不确定性。

场景:一个销售助手 Agent

如果用继承方式设计:一个 SalesAgent 继承 BaseAgent,再继承 LLMExtensionToolExtension……继承链随功能增加不断膨胀,任何修改都可能动摇基类。

如果用组合方式设计:SalesAgent 持有 LLMToolsMemoryOrchestrator 等组件,新能力只需组合新组件,不触碰既有代码。

💡 Key Insight

销售场景中需求变化极快,组合让 Agent 的能力边界跟随业务调整,而无需重构继承树。

实战:设计可组合的 Agent 系统

核心接口设计

1. 能力接口(Capability Interface) — 定义 Agent 必须具备的核心能力,如推理、规划、记忆召回等。每个能力接口约定输入格式、输出格式和调用约束,Agent 实现只需满足接口契约,无需关心调用方细节。

2. 记忆接口(Memory Interface) — 定义 Agent 如何存储、检索和遗忘信息。短期记忆、长期记忆和上下文记忆各自独立,接口统一封装,运行时可按需替换具体实现。

3. 行为接口(Behavior Interface) — 定义 Agent 与外部环境交互的行为模式,包括反应式(event-driven)和主动式(goal-driven)两类。行为接口屏蔽了具体交互协议(API、消息队列、文件等)的差异。

可组合 Agent 的实现

可组合 Agent 以组合而非继承的方式组装上述三大接口的具体实现。Agent 自身仅持有接口引用,具体能力由外部依赖注入(Dependency Injection),从而实现能力与结构的解耦。

具体能力实现示例

推理能力: 基于 LLM 的思维链(Chain-of-Thought)实现,输入任务描述与上下文,输出分步推理结果。推理器本身无状态,可被多个 Agent 共享。

规划能力: 将复杂任务分解为可执行的子任务序列,输入目标状态与约束条件,输出任务图(Task Graph)。规划器依赖能力接口查找可用操作,支持动态重规划。

编排模式:组合的组合

编排模式:组合的组合


继承在 AI 时代的新形态

Prompt 继承

虽然代码层面避免继承,但 Prompt 设计中出现了新的 “继承” 模式:

Prompt 继承的风险:

  • 冲突原则:子 Prompt 可能覆盖或矛盾父 Prompt 的指令
  • 长度爆炸:层层继承导致上下文过长
  • 调试困难:难以定位是哪个层次的 Prompt 导致了问题行为

更安全的做法:Prompt 组合

Context 继承

Agent 在执行过程中,Context 的传递也呈现出继承特征:

Context 继承的最佳实践:

  • 使用 copy() 而非直接引用,避免副作用
  • 明确哪些字段可继承,哪些需要重置
  • 设置深度限制,防止无限递归

反直觉洞察:组合不是银弹

洞察 1:过度组合的危害

**解决方案:

  • 最多组合 3-5 个核心组件
  • 使用适配器模式统一接口
  • 考虑性能开销

洞察 2:继承在特定场景仍然有用

何时使用继承:

  • 真正的 “is-a” 关系(几何图形继承体系)
  • 领域模型的分类(如上面的 Event 类型)
  • 框架/库的扩展点设计

洞察 3:组合也需要设计

更好的设计:Mediator 模式


代码示例与最佳实践

完整示例:销售助手 Agent

最佳实践清单

✅ Do(推荐做法):

  1. 面向接口编程
  2. 使用依赖注入
  3. 保持组件单一职责
  4. 支持运行时配置 ❌ Don’t(避免做法):

  5. 避免深继承链
  6. 避免在子类中依赖父类实现细节
  7. 避免混合关注点

结尾

组合优于继承不是教条,而是经过三十年软件工程实践验证的设计智慧。

核心要点回顾

维度 继承 组合
关系 is-a(是一个) has-a(有一个)
耦合度 高(白盒复用) 低(黑盒复用)
灵活性 编译时固定 运行时动态
测试性 困难(必须实例化整个继承链) 容易(注入 Mock)
适用场景 真正的分类体系 功能能力的组装

给 AI 开发者的建议

  1. 从组合开始:设计 Agent 时,先用组合思维思考能力如何组装
  2. 延迟使用继承:只有当 “is-a” 关系非常明显时才考虑继承
  3. 关注接口:组件之间的契约比实现更重要
  4. 保持简单:不要过度设计,3-5 个核心组件通常是最佳平衡点

最后的话

“软件设计的本质是在约束中寻找平衡。组合给了你灵活性,但也需要更多设计思考。继承看似简单,却可能在未来埋下隐患。”

在 AI Agent 开发中,这一点尤为重要。Agent 系统需要快速迭代、灵活调整,组合设计让这种调整成为可能。

选择组合,不是因为简单,而是因为正确。


深度阅读时间:约 9 分钟

📚 延伸阅读

本系列文章

经典参考


Agent OS 系列 - 设计模式篇 由 @postcodeeng 整理发布

Published on 2026-03-15 阅读时间:约 20 分钟

下一篇预告:《Agent 的状态机设计》