Skip to content

🐣 小a听说过的 23 种模式

小a听说过"23 种设计模式",背得出名字,可一到写代码就懵——不知道什么时候该用哪个

老z乐了:"模式不是背的,是长在代码里的。pi 工坊到处都是,我带你一个个认。每个模式,我给你看 pi 在哪用了、为什么用它。"


怎么读这份兵器谱

每个模式统一结构:

  1. 是什么(老z打比方)
  2. pi 在哪用了(源码证据)
  3. 为什么用它(解决的问题)
  4. 代价(没有银弹)

D.1 简单工厂 + 注册表(Provider 工厂)

🧙 比方:翻译局的"翻译员名册"——每个翻译员一份简历(配置),名册统一管理。

pi 哪用了:createProvider(第 17 章)+ 各 provider 注册

  • packages/ai/src/models.tscreateProvider 是工厂
  • 近 40 个 providers/*.ts 各是"一份配置"

为什么用:几十家厂商统一管理,且能热插拔(加新厂商不改核心)。

代价:工厂内部要处理"配置→对象"的复杂逻辑(第 17 章的闭包)。


D.2 策略模式(streamSimple / completeSimple)

🧙 比方:同一把锁,不同钥匙开——接口一样,实现不同。

pi 哪用了:ProviderStreams 接口(第 17 章)

  • stream(完整版,暴露细节)
  • streamSimple(简化版,抹平差异)

为什么用:同一调用入口,底层算法可替换。上层不关心用哪个,接口统一。

代价:多一层间接,调用栈深一点。


D.3 适配器模式(transform-messages)

🧙 比方:万能插头转换器——把不同制式转成统一的。

pi 哪用了:packages/ai/src/api/transform-messages.ts(第 17 章)

  • 把各家消息格式(OpenAI 的 messages / Google 的 contents)互转成统一格式

为什么用:统一抽象的核心难点——格式互转。

代价:适配器代码量巨大(每家方言一套规则)。


D.4 代理模式(proxy.ts)

🧙 比方:师爷代办——你不直接见知县,经师爷传话。

pi 哪用了:packages/agent/src/proxy.ts(第 18 章)

  • 远程流代理:客户端不直连厂商,经服务器转发(省 key、省带宽)

为什么用:控制访问(远程化、加缓存、加权限)。

代价:多一跳,有延迟。


D.5 观察者模式(subscribe)

🧙 比方:大喇叭广播——一处变化,多处收听。

pi 哪用了:packages/agent/src/agent.tssubscribe()(第 18 章)

  • Agent 维护 listeners 集合
  • 事件发生时通知所有监听器
  • UI 通过 subscribe 实时刷新

为什么用:解耦内核和界面。Agent 不关心谁在听,UI 不用轮询。

代价:事件多时调试难(链路长)。


D.6 模板方法(Provider 统一形状)

🧙 比方:同一条流水线——每家厂商按统一骨架填自己的实现。

pi 哪用了:每个 provider 都有相同的形状(第 17 章)

  • id / name / baseUrl / auth / models / api

为什么用:厂商实现各不同,但骨架一致,降低认知负担。

代价:函数式 TS 里不如 Java 经典,容易讲成"只是接口约束"。


D.7 责任链(auth resolve)

🧙 比方:跑得了和尚跑不了庙——一路往下试,任一环搞定就停。

pi 哪用了:packages/ai/src/auth/resolve.ts(第 17 章)

  • resolveProviderAuth:存档 key → OAuth → 环境变量 → 失败

为什么用:多级降级找凭据,逐级尝试。

代价:链太长时难追踪(走到哪了?)。


D.8 装饰器模式(tool-definition-wrapper)

🧙 比方:给工具穿马甲——不改原工具,加上校验/日志/截断。

pi 哪用了:packages/coding-agent/src/core/tools/tool-definition-wrapper.ts(第 20 章)

  • 不改原 tool,包装一层加 schema 校验、输出截断

为什么用:不改原代码就能增强功能(开闭原则)。

代价:层层包装可能让调用栈变深。


D.9 命令模式(轻量用法:keyed-operation-queue)

🧙 比方:点菜单——把"做什么"封装成对象,可排队。

pi 哪用了:packages/agent/src/harness/session/keyed-operation-queue.ts(第 18 章)

  • 把操作封装,按 key 排队(同 key 串行,不同 key 并发)

为什么用:并发控制——防同一个资源的并发冲突。

诚实说明:这是命令模式的轻量用法——它只占了"请求对象化 + 排队",没占典型的"撤销/重放"。pi 选它是为了并发控制,不是为了 undo。


D.10 状态机(agent-loop 的状态转换)

🧙 比方:红绿灯——根据当前状态和输入,转到下一个状态。

pi 哪用了:agent-loop.ts(第 18 章)

  • Agent 的状态(空闲/生成中/执行工具/结束)随循环推进而转换
  • StopReason 是状态转换的"输入信号"

为什么用:Agent 本质是有状态的,状态机能清晰描述"什么情况→干什么"。

代价:状态多时复杂,容易出不一致。

注:之前大纲提过"享元(模型目录单例)",但享元核心是"共享细粒度可变对象",模型目录更像"单例+注册表",语义不符。故替换为状态机——它更贴 agent-loop 的本质。


速查海报:遇到 X 问题,用哪个模式?

你要解决的问题该用的模式pi 的例子
统一管理一族相似对象工厂 + 注册表createProvider
同接口不同实现策略stream/streamSimple
不同接口要互通适配器transform-messages
控制访问/加中间层代理proxy.ts
一处变化多处通知观察者subscribe
统一骨架各自实现模板方法provider 形状
多级降级尝试责任链auth resolve
不改原代码加功能装饰器tool-wrapper
并发操作排队命令(轻量)keyed-operation-queue
状态驱动的行为状态机agent-loop

诚实提醒

🧙 老z的实话:

  1. 不是所有模式 pi 都用得很重。D.6(模板方法)和 D.9(命令)偏轻,我标了。
  2. 别硬套模式。先看代码"在解决什么问题",再判断它"恰好符合哪个模式"。模式是描述工具,不是设计目标
  3. 函数式语言里,模式长得不一样。pi 是 TypeScript,很多模式用函数/闭包实现,不像 Java 那么"classical"。理解思想,别纠结形式。

参考

  • GoF 经典:《设计模式》(Gamma 等)
  • pi 源码:各模式标注了文件路径
  • 对应章节:第 17/18/20 章详讲