近两年我一直在带团队做AI应用开发,经常遇到一种现象:新来的同事听说要学设计模式,第一反应是“这东西是不是过时了”“AI都直接写代码了,谁还研究模式”。但真到写多Agent编排、工具注册、上下文管理的时候,他们又会被同一个问题反复卡住——组件之间怎么组织才不乱、扩展点放在哪里、怎么让不同能力的模块互相替换而不牵一发动全身。这些问题的答案,恰恰就藏在那23个看似老掉牙的模式里。
“二十三种设计模式”这个经典话题,在任何时代都不是用来背的,而是用来“认场景”的。把23个模式拆开看,它们本质上是23种应对变化的设计策略。而进入AI应用开发时代,这些策略不仅没失效,反而在多Agent系统、工具调用链、流程编排这些新战场里被激活了。甚至热搜词里提到的“主从模式本质上是把subagent当作一种特殊的tool来调用”,这本身就是对传统模式在新架构下的一次重新解读。
这篇文章不打算按教科书顺序把23个模式逐个贴一遍UML图,那样你已经看过一百遍了。我想换个角度:以现代应用开发(尤其是AI原生应用)为背景,把23种模式分门别类放进真实项目里,讲清楚每个模式解决什么变化点、用什么思路识别它、用在哪一段代码里最合适。再单独拿出一节,聊一聊多Agent设计中那些“旧模式新用法”。最后,说几个我这些年踩过的和设计模式有关的坑。
1. 先别急着背23个名字:从AI开发者的视角看设计模式的价值
很多初学者拿到《设计模式》第一件事就是背名字:单例、工厂、观察者、策略……背完发现还是写不好代码,原因是把模式当成了“代码模板”,而不是“应对变化的设计决策”。在开始逐类拆解之前,我想把设计模式放在AI应用开发的坐标系里重新定位一下,这样后面每个模式的价值才会真正浮出来。
模式解决的核心问题只有一个:找到代码中可能变化的地方,然后把变化封装起来。 这是《设计模式》开篇就在讲的事情。所谓“面向接口编程”“优先组合而非继承”“开闭原则”,本质上都是为了让“变化的点”和“稳定的点”解耦。AI应用里变化最剧烈的是什么?是模型。今天用GPT,明天换Claude,后天可能上开源模型;今天一个Agent只调一个模型,明天可能需要根据任务难度动态路由到底层不同模型。如果不把“模型调用”这个变化点封装起来,将来每一次切换都是一场灾难。这种封装思路,映射到设计模式里就是策略模式、工厂模式、适配器模式的组合应用。
第二件重要的事是:设计模式不是“高级技巧”,而是“沟通词汇”。当我跟团队成员说“这个工具注册器用工厂方法实现”,我不用再解释一遍注册逻辑的扩展方式,对方立刻就知道新增一种工具类别时要改哪里、该实现什么接口。这种沟通成本的降低,在大规模AI应用工程里尤为宝贵——因为现在的Agent系统动辄几十个工具、十几个子Agent,没有统一的词汇,代码评审会变成逐行解释会。
第三,AI时代的设计模式正在出现“语义迁移”。比如说,Template Method(模板方法)过去用来定义算法骨架、把可变步骤留给子类实现;现在很多Agent框架的“主流程”就是模板方法——先收集用户意图、再调用工具、再组织回答,其中每一步都允许通过重写来定制。再比如Observer(观察者)过去是GUI事件监听那一套;现在Agent的事件总线、流式输出推送、工具执行状态通知,全是观察者模式的影子。因此,与其说设计模式过时了,不如说它在新的应用形态里找到了新的栖息地。
我会在后面的章节里把23个模式全部覆盖到,但不会平均用力。重点放在那些在AI原生应用、服务端架构、复杂业务编排里出场率最高的模式上,这部分我会结合代码和场景讲深一点;有些相对冷门的模式(比如解释器、备忘录),我会用最简练的方式讲清楚它的核心思想和常见应用点。你可以按自己的项目阶段挑着看:正在做系统架构的,重点看第2、3章;正在做业务流程编排的,重点看第4章;正在搞多Agent项目的,第5章不要错过。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 创建型模式:Agent工具注册与懒加载场景下的工厂、单例、建造者、原型
创建型模式一共5个:工厂方法、抽象工厂、单例、建造者、原型。这5个模式的核心思考起点是同一个:对象创建这件事本身,也可能成为变化点。 在AI应用开发里,对象创建的复杂度被放大了——工具对象动辄依赖模型客户端、向量数据库连接、外部API凭证,如果每个地方都手动new,改造成本极高。创建型模式解决的就是“如何让创建过程可控、可扩展、不散落各处”。
2.1 工厂模式:把Agent工具注册变成可扩展的装配线
工厂模式有三种形态:简单工厂、工厂方法、抽象工厂。很多人搞不清三者的区别,我用一个工具注册的场景来演示。
假设你正在开发一个Agent系统,里面有大量工具:天气查询、文档检索、数据库查询、邮件发送。最简单的写法是在注册中心里写一堆if-else:
python复制def create_tool(name: str, config: dict):
if name == "weather":
return WeatherTool(config)
elif name == "retriever":
return RetrieverTool(config)
elif name == "db_query":
return DatabaseTool(config)
...
这段代码的问题很直接:每新增一个工具,就要改动create_tool函数,违反了开闭原则。把它改造成工厂方法模式的做法是这样:
python复制class ToolFactory(ABC):
@abstractmethod
def create(self, config: dict) -> BaseTool:
pass
class WeatherToolFactory(ToolFactory):
def create(self, config: dict) -> BaseTool:
return WeatherTool(api_key=config["api_key"])
class RetrieverToolFactory(ToolFactory):
def create(self, config: dict) -> BaseTool:
vector_store = get_vector_store(config["collection_name"])
return RetrieverTool(vector_store=vector_store)
然后维护一个工厂注册表:
python复制TOOL_FACTORIES: dict[str, type[ToolFactory]] = {
"weather": WeatherToolFactory,
"retriever": RetrieverToolFactory,
}
新增工具时,只需要新增一个Factory类并注册,不需要改动已有的注册逻辑。这个模式最大的好处不是代码变短,而是把“怎么创建工具”这个变化点集中到了工厂类里,工具本身怎么初始化、依赖什么资源、参数怎么校验,都收口了。
抽象工厂模式则解决“一系列相关对象”的创建问题。举个例子,如果你需要支持OpenAI生态和Anthropic生态,两个生态分别有模型客户端、嵌入客户端、token计算器,这时候就不适合用单个工厂,而要用抽象工厂定义“模型生态工厂”接口,OpenAIFactory和AnthropicFactory分别实现整套产品的创建。在AI应用里,做模型网关时常会用到抽象工厂的思路,保证切换生态时整组依赖一起替换。
2.2 单例模式:模型客户端到底该不该全局唯一
单例模式大概是设计模式里被滥用最多、也最容易被骂的一个。网上一搜一大片“单例模式之我见”,观点两极分化。我的看法是:单例本身不是罪,罪在不加思考地把什么类都做成单例。判断依据很简单:这个对象在进程里是否真的只需要一个实例,且多个实例会带来资源浪费或状态不一致。
模型客户端就是一个适合单例的典型。比如OpenAI客户端,它内部维护连接池、token bucket限流器,如果你每次请求都new一个,连接池资源反复创建销毁,性能损耗巨大。更关键的是,很多模型客户端要求共享某些配置(比如API key、base_url、代理设置),多个实例反而容易配置漂移。所以,用单例来管理模型客户端是合理的。
但实体类、服务类、工具类绝不建议做成单例。比如一个UserService,如果你把它做成单例,那么它的成员变量就不能保存任何请求级状态,否则并发请求会互相污染。我在真实项目里见过把“当前用户ID”存在单例Service里的操作,结果就是A用户的请求偶尔看到B用户的数据,排查了整整两天。这种坑不是单例模式的错,是选错了使用场景。
如果你用Python写代码,单例模式不需要刻意用__new__去实现。直接用模块级全局变量,或者用依赖注入容器管理生命周期,更简单也更容易测试:
python复制# 模块级单例
_model_client = None
def get_model_client():
global _model_client
if _model_client is None:
_model_client = OpenAI(
api_key=settings.OPENAI_API_KEY,
base_url=settings.OPENAI_BASE_URL,
)
return _model_client
代码简单,行为清晰,测试时直接替换_model_client即可。
2.3 建造者模式和原型模式:搞定复杂配置对象与Prompt模板复用
建造者模式解决的是“对象有非常多参数,而且参数之间可能有依赖关系”的创建场景。AI应用里最典型的例子是构建一次模型调用的完整参数:model、messages、temperature、top_p、max_tokens、tools、response_format、metadata……在复杂场景里,这些参数不是平铺的,而是分层的:
- 模型层参数:model名称、temperature、max_tokens
- 消息层参数:system prompt、消息历史、当前输入
- 工具层参数:启用哪些工具、工具调用的严格程度(tool_choice)
- 输出层参数:输出格式、是否流式返回
直接传一堆具名参数会让调用方代码难以维护。用建造者模式写起来是这样:
python复制request = CompletionRequest.builder() \
.model("gpt-4o") \
.system_prompt(SYSTEM_PROMPT) \
.add_user_message(user_input) \
.add_tool(weather_tool) \
.add_tool(retriever_tool) \
.temperature(0.2) \
.enable_stream() \
.build()
Builder模式的好处在复杂参数场景体现得很充分:参数的组装逻辑内聚在Builder里,调用方只需要关注自己关心的参数,未设置的参数有合理默认值,还能在build()方法里做参数校验(比如tools和tool_choice不能冲突)。市面上的AI框架(比如LlamaIndex的Settings、LangChain的PromptTemplate)大量使用链式调用来实现类似Builder的效果。
原型模式则是“复制已有对象作为新对象起点”的创建方式。在AI应用里,Prompt模板是一个极其适合原型模式的对象。假设你有一套写SQL的Prompt模板,里面包含few-shot示例、格式要求、字段字典,每次执行查询前你想基于模板微调一下(比如增加用户当前的数据库schema),但不希望改动原始模板。这时可以通过prototype.clone()复制模板,再在副本上做修改。Python里可以用copy.deepcopy实现原型模式:
python复制class PromptTemplate:
def __init__(self, template: str, variables: dict, examples: list[str]):
self.template = template
self.variables = variables
self.examples = examples
def clone(self) -> "PromptTemplate":
return copy.deepcopy(self)
def update_variables(self, new_vars: dict) -> None:
self.variables.update(new_vars)
原型模式最大的价值是“多态复制”能力:当系统里有多种PromptTemplate子类时,调用方不需要知道具体子类类型,只需要调用clone()就能得到同类型副本。这在插件化系统里很常用。
3. 结构型模式:适配器、代理与组合在SDK集成里的真实位置
创建型模式解决了对象怎么来的问题,结构型模式解决的是“对象之间怎么组织关系”的问题。7个结构型模式——适配器、桥接、组合、装饰器、外观、享元、代理——在AI应用开发里的出场率非常高,尤其是适配器、代理、装饰器、组合这4个。我把它们放进具体的集成场景里看,比单独背定义有用得多。
3.1 适配器模式:把各家大模型API统一成一套接口
做AI应用的人一定深有体会:OpenAI的API和Anthropic的API长得完全不一样,更别提各种开源模型各自为政的调用方式。如果你在业务代码里直接用某个厂商的SDK,那将来换模型供应商的时候就等着哭吧。适配器模式在这里就是标准解法。
适配器模式的核心思想是:定义一个目标接口,让适配器把非标准接口转换成目标接口。 在模型接入层,我们可以定义统一的模型网关接口:
python复制class LLMClient(ABC):
@abstractmethod
def chat(self, messages: list[Message], **kwargs) -> ChatResponse:
pass
@abstractmethod
def embed(self, texts: list[str]) -> list[list[float]]:
pass
class OpenAIAdapter(LLMClient):
def __init__(self, api_key: str, base_url: str | None = None):
self._client = OpenAI(api_key=api_key, base_url=base_url)
def chat(self, messages, **kwargs):
# 把内部Message对象转换为OpenAI的格式
openai_messages = [m.to_openai_format() for m in messages]
...
同理解释器调用时,业务层只依赖LLMClient接口,真正切换模型供应商只是替换一个Adapter的实现而已。这种适配层在真实项目里通常还会配合工厂模式:根据配置文件里的model_provider字段决定实例化哪个Adapter。两个模式组合使用,切换成本被压得极低。
3.2 代理模式:懒加载、权限控制、缓存,代理不仅仅是“中间层”
代理模式和适配器模式长得有点像,容易混淆。它们的本质区别是:适配器改变接口,代理保持接口不变。代理模式是“提供一个替身来控制对原对象的访问”,接口和原对象一致,外包了一层控制逻辑。
在AI应用里,代理模式至少有三个高价值使用场景:
-
懒加载(虚拟代理):有些重量级对象(比如向量数据库客户端、大型模型)初始化成本很高,但可能整个会话里根本没被用到。用虚拟代理把初始化推迟到第一次真正调用时,能明显减少启动耗时。我在一个RAG服务里用过这个手法,服务启动时间从3秒降到800毫秒,用户无感知,但部署体验提升明显。
-
访问控制(保护代理):有些工具只对特定角色开放,比如“只能管理员执行的数据库写操作”。代理在转发调用前检查当前用户权限,比在每个工具内部写权限判断要干净得多,也更容易统一收口。
-
缓存(缓存代理):对重复的请求直接返回缓存结果。LLM调用成本高、时延高,在非流式场景下加一层缓存代理通常能降低不少开销。我做过一个实验:用嵌入向量做相似查询时,缓存代理能命中约30%的重复查询,成本和响应时间同步下降。
3.3 装饰器模式:给工具链加日志、加限流、加重试
每次说起装饰器,Python程序员都会心一笑:Python的@decorator语法天然就是为这个模式准备的。装饰器模式的核心特征有两点:一是包装者和被包装者实现相同接口,二是包装者在转发调用前后能插入额外行为。 这跟继承的差别在于:装饰器是水平的、可任意组合的;继承是垂直的、一条路走到黑的。
在Agent工具系统里,装饰器模式简直是给工具增加横切能力的标准答案。看这个例子:
python复制def with_retry(max_retries: int = 3, base_delay: float = 1.0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except TemporaryError as e:
if attempt == max_retries - 1:
raise
time.sleep(base_delay * (2 ** attempt))
return wrapper
return decorator
def with_timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
logger.info(f"{func.__name__} 耗时 {time.perf_counter() - start:.3f}s")
return result
return wrapper
装饰器模式在实现时有一个需要特别注意的点:保持包装层之间的透明性。 也就是每个装饰器都不能假定自己是“最外层”或“最内层”,它只能依赖接口约定的输入输出。如果你在装饰器里偷偷加了一个新参数,或者改了返回值的结构,那组合顺序一变,系统就会崩。好的装饰器只做一件事:进入前后拦截、记录、转换参数或结果,不改接口契约。
3.4 组合模式:Agent与工具树形结构的优雅管理
组合模式是“部分-整体”的结构模式,让客户端以一致的方式对待单个对象和对象组合。在AI应用里,最典型的组合模式场景就是Agent的层级结构。
一个组织良好的Agent系统往往不是扁平的“一堆工具+一个大脑”,而是树形的:顶层有一个协调Agent,下面按领域拆分成多个子Agent,每个子Agent管理自己的工具集;工具本身也可能由多个小工具组合而成(比如一个“数据分析工具”内部由图表生成、SQL查询、数据清洗三个小工具组成)。在这种结构里,如果调用方需要递归地执行某个操作(列出所有工具、统计总token消耗、打印整棵树的结构、统一关闭所有连接),组合模式就派上用场了:
python复制class Component(ABC):
@abstractmethod
def execute(self, context: dict) -> dict:
pass
@abstractmethod
def list_tools(self) -> list[str]:
pass
class ToolLeaf(Component):
def execute(self, context):
return self.tool.run(context["input"])
def list_tools(self):
return [self.tool.name]
class AgentComposite(Component):
def __init__(self, children: list[Component]):
self.children = children
def execute(self, context):
# 协调子节点执行,可以加入自己的调度逻辑
result = {}
for child in self.children:
result.update(child.execute(context))
return result
def list_tools(self):
return [tool for child in self.children for tool in child.list_tools()]
组合模式的关键收益是:调用方不需要关心当前节点是叶子还是组合节点,统一调用execute()即可。这种递归透明性在复杂工具链管理里非常实用。
3.5 桥接、外观、享元:三个容易被低估的模式
桥接模式被很多教科书讲得云里雾里,其实它的核心是“抽象和实现各自独立变化”。举个例子:你有一个输出模块,抽象层面分为“流式输出”和“一次性输出”,实现层面分为“控制台输出”和“WebSocket推送”。如果用继承,组合会变成4个子类;如果抽象和实现独立变化,本来就有两种变化维度。桥接模式把这两条维度分离,用组合接口的方式装配起来。在AI应用里,输出通道和输出格式就是两个典型的独立变化维度,桥接模式值得用。
外观模式是“给复杂子系统提供一个统一入口”。其实我们每天都在用:调用LangChain的AgentExecutor、调用FastAPI的APIRouter,本质上都是外观。外观模式的作用是降低使用方的认知负担,同时隔离子系统内部的变动。在Agent系统里,一个AgentService对上层提供chat()方法,内部封装了规划、工具调度、记忆管理、响应生成等复杂流程——这就是一个标准外观。
享元模式的核心是“通过共享减少对象数量”。在AI应用里最适合的场景是:多个Agent实例共享同一个向量检索实例、共享同一个Embedding模型实例、共享同一份只读的工具配置。因为这些对象无状态或只有只读状态,共享不会引发并发问题。我在实现多Agent系统时,会把模型客户端、嵌入模型、向量检索器都做成享元对象,放在一个共享容器里。这样100个子Agent不需要各自创建100份模型客户端,内存和连接池开销大幅下降。
4. 行为型模式:把策略、责任链、观察者铺进业务流程
行为型模式是11个,数量最多,也是实际项目里最能体现架构水平的部分。如果说创建型模式管“生”,结构型模式管“关系”,那么行为型模式管的就是“协作”——对象之间怎么传消息、怎么分配职责、怎么定义流程、怎么应对状态变化。在AI应用开发里,行为型模式的出场率非常高,尤其是策略、责任链、观察者、模板方法、状态这几个。
4.1 策略模式:把可替换的“算法”从业务流程里抽出来
策略模式的定义很简单:定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。它要解决的核心问题,是把“做什么”和“怎么做”分开。
在AI应用里,策略模式最常见的应用就是“模型路由”。同一个任务,可以根据输入特征选择不同的处理策略。比如:
- 简单问答直接走快而便宜的小模型
- 复杂推理任务走强推理大模型
- 检索增强任务要走带检索的RAG链路
- 工具调用任务要走Function Calling流程
把每条链路抽象成一个策略接口,路由层根据条件决定使用哪个策略:
python复制class ProcessingStrategy(ABC):
@abstractmethod
def can_handle(self, request: UserRequest) -> bool:
pass
@abstractmethod
def execute(self, request: UserRequest) -> Response:
pass
class SimpleQAStrategy(ProcessingStrategy):
def can_handle(self, request):
return request.complexity_score < 0.4
def execute(self, request):
# 调用小模型直接回答
return self.light_model.chat(request.messages)
每个策略独立维护,新增一种处理链路时不影响已有策略。策略模式的关键约束是:策略之间的接口必须一致,调用方只能依赖策略接口,不能依赖具体策略。否则就退化成if-else堆代码了。
4.2 责任链模式:把预处理流水线拆成一串“能处理就处理,不能处理就下传”的节点
责任链模式非常贴合“请求—处理”链路。它的核心结构是:多个处理器串成一条链,每个处理器决定是自己处理、还是传给下一个处理器。跟策略模式的区别在于:策略是“选一个执行”,责任链是“依次尝试,直到有人处理”。
AI应用里责任链最典型的使用场景是“LLM输入预处理”。想象一下,用户的原始输入进入模型之前,可能要经过好几道处理:
- 敏感信息过滤(脱敏)
- 内容安全检测
- 自定义指令注入
- 上下文压缩
用责任链模式写就是:
python复制class PreprocessHandler(ABC):
def __init__(self):
self._next: PreprocessHandler | None = None
def set_next(self, handler: "PreprocessHandler") -> "PreprocessHandler":
self._next = handler
return handler
def handle(self, message: Message) -> Message:
message = self.process(message)
if self._next:
return self._next.handle(message)
return message
@abstractmethod
def process(self, message: Message) -> Message:
pass
责任链的提升点在于:新增一个处理节点不需要改动已有节点,只需要在装配链时多挂一个。而且节点顺序可以配置化,同一套代码可以按需求排出不同的预处理链。有一个细节需要注意:责任链的每个节点要尽量保持“无状态”,不要在节点内部保存请求数据,否则并发请求会互相污染。
4.3 观察者模式:事件驱动是Agent系统消息传递的基本形态
观察者模式(又叫发布-订阅,在实现上略有差异)解决的是“一个对象状态变化后,需要通知一堆对象”的场景。它把观察者和被观察者解耦:被观察者不需要知道谁在监听自己,观察者也不需要知道状态变化来自哪里。
Agent系统的运行时,本质上是事件驱动的。工具执行完成是一个事件,Agent状态切换是一个事件,流式token产出是一个事件,用户取消是一个事件。如果这些事件全部用硬编码调用关系来串联,代码会变成一团乱麻。正确做法是引入事件总线:
python复制class EventBus:
def __init__(self):
self._subscribers: dict[str, list[Callable]] = defaultdict(list)
def subscribe(self, event_type: str, handler: Callable):
self._subscribers[event_type].append(handler)
def publish(self, event_type: str, data: dict):
for handler in self._subscribers[event_type]:
handler(data)
event_bus = EventBus()
event_bus.subscribe("tool.executed", on_tool_executed)
event_bus.subscribe("agent.started", on_agent_started)
用了观察者模式之后,模块之间的依赖方向被反转了:模块A不再直接调用模块B的方法,而是发布事件;模块B通过订阅事件来响应。这样A和B可以完全独立演进,新增一个模块只需要订阅自己关心的事件就行。在Agent运行监控、指标上报、日志搜集这些场景,观察者模式几乎是标配。
4.4 模板方法模式:Agent主流程的骨架与覆写点
模板方法模式是行为型模式里最容易被忽略、但应用范围最广的一个。它的核心思想是:在父类里定义算法骨架,把其中某些步骤延迟到子类中实现。跟策略模式的区别在于:策略模式是“整个算法可替换”,模板方法是“算法骨架固定、部分步骤可覆写”。
在Agent框架里,一次完整执行循环的骨架往往惊人地相似:
- 接收用户输入
- 构建上下文(历史消息、系统提示、检索结果)
- 调用模型生成决策
- 执行工具调用(如果需要)
- 汇总结果并生成最终响应
这个骨架适合固化在基类里,具体步骤留给不同的Agent子类覆盖。比如“调用模型生成决策”这一步,普通问答Agent直接透传模型输出;工具型Agent需要解析Function Calling的结果;反思型Agent需要先自我评估再决定是否补充一轮推理。
python复制class BaseAgent(ABC):
def run(self, user_input: str) -> str:
context = self.build_context(user_input)
decision = self.decide(context)
if self.need_tool_call(decision):
tool_result = self.execute_tool_call(decision)
context["tool_result"] = tool_result
return self.finalize(context)
return self.finalize(context)
def build_context(self, user_input: str) -> dict: ...
def need_tool_call(self, decision: dict) -> bool: ...
def execute_tool_call(self, decision: dict) -> str: ...
@abstractmethod
def decide(self, context: dict) -> dict: ...
@abstractmethod
def finalize(self, context: dict) -> str: ...
模板方法的美妙之处在于:流程骨架被固化,不允许子类随意改动顺序,防止“每个Agent一套混乱流程”;同时保留了足够的扩展点,业务逻辑的差异通过覆写具体步骤来实现。这套思路对团队协作极其友好——不同人维护不同Agent,但整体流程始终可控。
4.5 状态、命令、迭代器、访问者、中介者、备忘录、解释器:剩下的7个各就各位
前面6个行为型模式覆盖了最常见的业务编排场景。剩下这7个我快速带过,但每个都会指出它在现代表征系统里的典型应用位置,方便你遇到具体问题时对上号。
-
状态模式:当对象的行为随内部状态变化而变化时,把状态封装成独立类。AI场景里最典型的应用是Agent生命周期管理,比如一个会话Agent可能处于idle、working、waiting_tool、error等多个状态,每种状态下能够响应的事件和动作不同。用状态模式,状态转换逻辑被收敛到状态类里,杜绝了大楼级的if-else。
-
命令模式:把请求封装成对象,从而支持对请求进行参数化、排队、日志记录和撤销。在AI应用里,命令模式最好的应用是把“函数调用”本身序列化——多Agent系统里,subagent执行的工具调用可以先封装成Command对象,再写入事件流或日志,从而实现回放、审计和测试。
-
迭代器模式:提供一种方法顺序访问聚合对象里的元素,而不暴露底层表示。Python里的
__iter__/__next__就是标准实现。在流式输出场景,LLM返回的token序列本质上就是一个迭代器;Agent的异步事件流(AsyncIterator)也是迭代器模式的应用。 -
访问者模式:作用于某对象结构中的各元素的操作分离出来。这个模式在业务代码里不太常用,但在编译器、AST处理、配置解析等领域是王者。如果你在搞AI代码分析工具、规则引擎,访问者模式一定会遇到。
-
中介者模式:用一个中介对象封装一组对象的交互,让对象之间不再互相显式引用。在复杂多Agent系统里,协调者(Coordinator)就是一个中介者:各Agent不直接互相通信,而是通过协调者转发消息。这样能避免Agent之间形成网状依赖,让消息流转路径可控、可观测。
-
备忘录模式:在不破坏封装的前提下,捕获并外部化一个对象的内部状态,以便之后恢复。多轮对话系统的状态保存与恢复就是备忘录的典型应用。每个会话的上下文快照、Agent执行到一半的资源状态,都可以用备忘录模式保存下来,出现故障时从最近一个快照恢复。
-
解释器模式:定义一种语言的文法,并建立一个解释器来解释语言中的句子。在AI应用里,最有代表性的解释器模式应用是Prompt中的DSL(领域特定语言)——比如用特定的语法描述希望模型输出的格式化内容,然后解析这段语法并渲染成具体Prompt。业务规则引擎、表达式解析也是解释器模式的主场。
到这里,23个设计模式我已经全部过了一遍。简单做个对照表,方便你做索引:
| 分类 | 模式名称 | 核心用途 | 现代AI/业务场景 |
|---|---|---|---|
| 创建型 | 工厂方法/抽象工厂 | 对象的创建封装 | 模型客户端工厂、工具注册工厂 |
| 创建型 | 单例 | 全局唯一实例 | 模型客户端、配置管理器 |
| 创建型 | 建造者 | 复杂参数对象的构建 | 请求参数构建、Prompt构建 |
| 创建型 | 原型 | 对象复制 | Prompt模板复制、配置快照 |
| 结构型 | 适配器 | 接口转换 | 多模型API统一接入 |
| 结构型 | 桥接 | 抽象与实现分离 | 输出格式与输出通道解耦 |
| 结构型 | 组合 | 树形结构建模 | Agent/工具层级结构 |
| 结构型 | 装饰器 | 运行时扩展行为 | 日志、限流、缓存、重试 |
| 结构型 | 外观 | 统一入口 | AgentService封装复杂流程 |
| 结构型 | 享元 | 共享减少对象数量 | 共享模型实例、检索器 |
| 结构型 | 代理 | 控制对象访问 | 懒加载、权限控制、缓存 |
| 行为型 | 策略 | 算法可替换 | 模型路由、处理链路选择 |
| 行为型 | 责任链 | 请求依次尝试 | 输入预处理、安全检测链 |
| 行为型 | 观察者 | 事件广播订阅 | 事件总线、运行时监控 |
| 行为型 | 模板方法 | 算法骨架+子类覆写 | Agent执行主流程 |
| 行为型 | 状态 | 状态驱动的行为 | Agent生命周期管理 |
| 行为型 | 命令 | 请求对象化 | 工具调用序列化、可回放 |
| 行为型 | 迭代器 | 顺序访问聚合对象 | 流式token、异步事件流 |
| 行为型 | 访问者 | 结构上的操作分离 | AST分析、代码生成工具 |
| 行为型 | 中介者 | 封装对象间交互 | 多Agent协调者 |
| 行为型 | 备忘录 | 状态保存与恢复 | 会话快照、故障恢复 |
| 行为型 | 解释器 | 自定义语言解析 | Prompt DSL、规则引擎 |
5. 多Agent设计里的“旧模式新用法”:主从模式、黑板模式与工具化调用
这一节我单独拿出来聊,是因为最近多Agent架构成了热点,而很多讨论里都在“发明新名词”。但如果你有设计模式功底,你会发现大量新架构不过是在用新时代的术语重讲经典模式。热搜词里那句“最新的多agent设计里 主从模式,其实本质上将subagent视为另类的tool进行调用”,一语道破了天机。
5.1 主从模式(Master-Slave)到底是个什么模式
传统GoF设计模式里并没有“主从模式”这个词,但它本质上是模板方法模式、外观模式、命令模式、中介者模式在一个更高粒度上的组合应用。在Agent系统里,主从模式的结构是这样的:
- 一个主Agent(Master)负责接收用户任务、拆解子任务、编排执行顺序、汇总结果
- 多个子Agent(Slave/Subagent)负责执行具体的子任务,各有各的上下文和工具集
如果你观察Master调用Subagent的代码,你会发现调用方式跟调用一个工具几乎一样——传入参数、等待返回结果。因此,把subagent视为一种特殊的tool,在工程实现上是非常自然的选择。这也解释了为什么很多Agent框架的Tool接口和Agent接口长得那么像。
python复制class AgentTool(BaseTool):
"""把一个子Agent包成Tool,注册给主Agent使用"""
def __init__(self, subagent: BaseAgent, name: str, description: str):
self._subagent = subagent
self.name = name
self.description = description
def run(self, input: str) -> str:
# 主Agent调用子Agent,跟调用普通工具完全一致
return self._subagent.run(input)
# 注册时,子Agent和普通工具地位相同
master_agent.register_tool(AgentTool(sql_agent, "sql_agent", "专门负责生成和执行SQL查询"))
master_agent.register_tool(weather_tool)
这种设计带来的最大工程收益是:主Agent的调用逻辑不需要区分“这是一个工具还是一个子Agent”,调度、重试、错误处理逻辑可以完全复用。这正是模板方法模式“把骨架固定下来”的思路——无论是工具还是子Agent,都遵循“接收输入、执行、返回结果”的统一接口契约。
5.2 黑板模式:多Agent共享上下文的经典解法
另一个在多Agent系统里经常被重新提起的“老模式”是黑板模式,它跟中介者模式、观察者模式的关系很近。黑板模式的核心是:多个独立的“知识源”围绕一个共享的黑板(共享上下文/共享存储)工作,谁对当前问题有贡献,谁就向黑板写入信息;最终答案是从黑板上的信息汇总而来的。
在LLM多Agent系统里,黑板模式最常见的落地形态就是共享消息列表或共享工作区(Workspace)。比如做代码生成的多Agent系统里,一个Agent负责需求分析、一个负责编码、一个负责测试,它们不直接互相调用,而是共享同一个工作区:
python复制class SharedWorkspace:
def __init__(self):
self.messages: list[AgentMessage] = []
self.artifacts: dict[str, str] = {}
self.state = {"current_step": "analysis"}
def add_message(self, from_agent: str, content: str):
self.messages.append(AgentMessage(from_agent=from_agent, content=content))
def get_context_for(self, agent_role: str) -> str:
# 按角色筛选它关心的上下文
relevant = [m for m in self.messages if m.from_agent != agent_role]
return format_messages(relevant)
黑板模式的好处是Agent之间耦合度极低,非常适合异构Agent协作;坏处是流程不容易把控,调试复杂。所以实际项目里往往会混合使用:整体上借鉴黑板模式共享上下文,局部用主从模式保留编排控制力。
5.3 模式组合:真实多Agent系统的设计模式全景
如果你在从头搭建一个多Agent系统,不妨按我对这个系统的解构来对照一下,看看每个位置都用了什么模式:
- 启动时创建各个Agent及工具:工厂方法 + 享元
- Agent依赖共享模型客户端和向量检索器:单例 + 享元
- 主Agent调用子Agent(视为工具):代理/模板方法 + 适配器
- 子Agent之间的消息共享:黑板模式 + 观察者(事件推送)
- 全流程的生命周期管理:状态模式
- 工具调用链路:责任链(预处理)+ 装饰器(限流/重试/日志)+ 命令(序列化审计)
- 整体对外接口:外观模式
当你把这套组合拳打出来,展现在眼前的架构就是:每个对象职责单一、依赖方向清晰、扩展点明确、模块间可独立测试。这就是设计模式在AI时代最实际的价值——不是让你写出花哨的代码,而是让你面对复杂系统时不至于手忙脚乱。
6. 设计模式在真实项目里的三个常见误区与避坑建议
最后这节我想讲点真话。前面把23个模式的正面价值讲了很多,但设计模式在真实项目里也经常被“用坏”。这些年我见过太多因滥用设计模式而制造出来的复杂怪物,所以把最常见的三个坑单独拎出来说说,希望能帮你避开。
6.1 误区一:为了用模式而用模式,把代码“设计”成一坨迷宫
这是初学者最容易犯的错,甚至一些有经验的人也会犯。项目经理说“这次要展示架构能力”,于是把一段本来20行就能写完的逻辑,硬生生拆成5个类、4个接口、7个工厂。结果是代码可读性急剧下降,团队其他人维护时想骂人。
我见过一个最夸张的例子:有人写一个“读取配置文件并返回配置对象”的模块,用了抽象工厂+建造者+单例的组合,包了三层抽象。后来唯一的需求变化是从JSON换YAML,他们加了两个新类就满足了——但整个团队看懂那三层封装花了整整一周。这不是设计模式的胜利,是过度设计的灾难。
判断标准其实很简单:设计模式的价值体现在“变化出现时改动最小”。 如果你的项目根本没有变化需求,或者未来一年都不会加新工具、新模型、新流程,那老老实实写简单代码比什么都强。设计模式不是装饰品,它是应对变化的工具。没有变化,就没有使用模式的理由。
6.2 误区二:模式术语化,成了团队沟通的屏障
模式最初的价值之一是统一词汇,但有时候反而成了沟通障碍。有些开发者特别爱在评审时甩术语,“这里用一个策略模式”“那里搞一个工厂”,但说不出为什么要这样设计、解决了什么实际问题。结果就是,懂术语的人在云里雾里点头,不懂术语的人更不敢开口问。
我的建议是:在评审和设计文档里,先写清楚要解决的问题(变化点),再提用什么模式。 比如不要写“这个模块用抽象工厂”,而是写“这个模块需要支持OpenAI和Anthropic两套生态的整体切换,所以用抽象工厂封装不同生态的产品族创建逻辑”。把“为什么”说在前面,模式只是锦上添花的标签。这样即使是没学过设计模式的同事,也能跟上讨论思路。
6.3 误区三:把模式当银弹,忽视了简洁代码、单元测试和CR
设计模式不是万能的。它解决的是“对象间的协作与结构”问题,解决不了死锁、并发竞争、基础设施不稳定、需求不明确这些更本质的问题。一个架构如果有一堆漂亮的模式但根本没有测试保护,改动时一样心惊胆战;一个系统如果代码写得简洁直白,哪怕没有一个模式的名字,照样可以健康运行。
我的实践经验是:模式要搭配测试一起用。 策略模式也好,责任链模式也罢,它们把可替换的组件拆出来之后,天然就方便做单元测试——每个策略、每个处理节点、每个Agent状态,都能独立验证。这是设计模式最实在的收益之一。所以,学设计模式的时候,可以把“这个模式在哪里创造了测试边界”作为理解模式的一个视角。用测试驱动设计模式,比凭空套模式要靠谱得多。
7. 学习设计模式的几条实操路径与资料建议
说完了模式和坑,最后聊聊怎么学。市面上关于设计模式的资料多如牛毛,但质量参差不齐,盲目看容易越看越乱。我根据自己带团队的经验,整理了几条实操路径,按学习阶段区分,各取所需。
第一阶段:以“问题”为索引建立模式地图。 不要按名称顺序背模式,而是按“为了解决什么变化点”来组织知识。拿到一个新需求,第一反应不是“我要用XX模式”,而是“这里的变化点是什么”。建立“变化点→模式”的映射:对象创建方式变化→工厂;对象实例数量需要控制→单例;算法可以互替→策略;请求要经过一系列处理→责任链;一个对象状态变化要通知多个对象→观察者;算法骨架固定但步骤可变→模板方法;接口不兼容→适配器;需要控制对象访问→代理……当你用变化点来索引模式时,遇到真实需求就能快速对上号。
第二阶段:读源码,看真实项目怎么用。 理论学习到一定阶段之后,效率最高的学习方式是读优秀开源项目源码。我推荐三个方向:一是读LangChain、LlamaIndex这类AI框架的源码,看它们怎么组织Tool注册、Agent调度、上下文传递;二是读Spring Framework(Java)或FastAPI(Python)的源码,Spring里几乎把GoF 23个模式用了个遍,FastAPI的依赖注入容器也是理解工厂、单例、代理模式的好素材;三是读自己熟悉的核心业务代码,重点看那些“改一个需求要动很多文件”的地方,想想用哪个模式可以缩小改动面。
第三阶段:刻意练习,但只限真实需求。 不要为了练习模式而写一堆脱离业务的示例代码。更好的方式是,在现有项目里找一个真实的变化需求,用模式重构一遍。比如你的项目里有多个模型供应商的调用逻辑散落在业务代码里,就试着用适配器+工厂统一封装;你在开发多Agent编排功能,就试着用组合模式组织工具树,用状态模式管理Agent生命周期。重构之后对比改动量、可读性、测试难度,你会对模式的价值有切身体会。
资料推荐方面, 我列几本经典,按优先级拍:
- GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》:设计模式的原典,23个模式的源头。《Head First设计模式》是它的大众化读物,适合入门。
- 《设计模式之禅》:国产好书,语言通俗,实例贴近业务场景,Java程序员友好。
- 《Android源码设计模式解析与实战》:这个特别适合想从源码层面理解模式的人,结合Android框架源码讲每个模式,分析透顶。
- Eric Freeman的《Head First设计模式(第二版)》:入门首选,插图多、案例有趣,豆瓣评分一直很高。
- 学习电子版资料的话,直接在搜索引擎里搜“设计模式 Java实现”“设计模式 C++实现”+“豆瓣高分”就能找到不错的开源电子书。
但请记住,书只是起点。设计模式真正的学习材料是你手里的真实项目——你每解决一个“变化点”的架构问题,设计模式的知识才会真正长在你身上。
我个人在实际操作中的体会是:设计模式不是设计出来的,而是在重构中“长”出来的。与其一开始就堆砌一堆模式类,不如先写出能跑通的简单代码,然后盯着“哪里在变化”“哪里改起来最痛”发力,用模式把痛点一个个消解掉。当你经历过几轮“从慢性疼痛到顺畅重构”的循环之后,设计模式对你来说就不再是23个需要背诵的章节,而是一套自然而然的架构直觉。到那时候,你看新框架源码,一眼就能认出它在哪个位置用了哪个模式,也就能真正把它们按需组合到自己的系统里。
