Appearance
🐣 小a听说过的 23 种模式
小a听说过"23 种设计模式",背得出名字,可一到写代码就懵——不知道什么时候该用哪个。
老z乐了:"模式不是背的,是长在代码里的。pi 工坊到处都是,我带你一个个认。每个模式,我给你看 pi 在哪用了、为什么用它。"
怎么读这份兵器谱
每个模式统一结构:
- 是什么(老z打比方)
- pi 在哪用了(源码证据)
- 为什么用它(解决的问题)
- 代价(没有银弹)
D.1 简单工厂 + 注册表(Provider 工厂)
🧙 比方:翻译局的"翻译员名册"——每个翻译员一份简历(配置),名册统一管理。
pi 哪用了:createProvider(第 17 章)+ 各 provider 注册
packages/ai/src/models.ts的createProvider是工厂- 近 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.ts 的 subscribe()(第 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的实话:
- 不是所有模式 pi 都用得很重。D.6(模板方法)和 D.9(命令)偏轻,我标了。
- 别硬套模式。先看代码"在解决什么问题",再判断它"恰好符合哪个模式"。模式是描述工具,不是设计目标。
- 函数式语言里,模式长得不一样。pi 是 TypeScript,很多模式用函数/闭包实现,不像 Java 那么"classical"。理解思想,别纠结形式。
参考
- GoF 经典:《设计模式》(Gamma 等)
- pi 源码:各模式标注了文件路径
- 对应章节:第 17/18/20 章详讲