工厂+策略+单例:拆解 OhMyGo 的 AI 模型层设计

发布时间:2026/7/2 5:59:48
工厂+策略+单例:拆解 OhMyGo 的 AI 模型层设计 GitHubhttps://github.com/FindMyWay2Ting/OhMyGo项目地址前置问题在动手写代码之前先想清楚这个系统要解决什么问题用户发起一次 AI 对话系统需要根据用户选择的模型类型普通对话 / RAG 文档问答 / MCP 工具调用 / 本地模型动态创建对应的 AI 模型实例并在整个会话周期内维护该实例的状态。这句话里藏着三个独立的问题怎么根据类型动态创建实例→ 工厂模式不同模型的调用方式不同怎么统一处理→ 策略模式全局只需要一个管理器不需要重复创建→ 单例模式三个问题对应三种设计模式这才是这个模块真正有价值的地方——不是为了用设计模式而用而是问题本身正好适合用这三种模式来解决。一、策略模式统一接口封装差异1.1 问题的本质OpenAI 对话、RAG 文档问答、MCP 工具调用听起来完全不同但它们有一个共同的操作集合给一组消息返回一个回复给一组消息流式返回一个回复只要把这个共同的操作抽象成一个接口上层调用方就不需要关心底层到底是哪个模型1.2 接口定义这就是策略模式的精髓接口定义行为契约具体实现负责差异1.3 四种模型的差异在哪里举一个最复杂的例子——AliRAG 的GenerateResponse可以看到RAG 模型的策略是检索 → 拼 prompt → 二次调用而不是 RAG 的模型策略是直接调用这就是策略模式的核心接口一样策略不同。1.4 上层怎么用调用方完全不需要知道底层是哪个模型二、工厂模式解耦创建和使用2.1 为什么不能直接 new假设没有工厂调用方要创建模型问题在哪调用方必须知道所有模型的构造函数签名参数各不相同每加一个新模型要改所有调用方无法在运行时扩展新模型编译时硬编码2.2 工厂实现先定义创建函数的类型工厂内部维护一个 mapkey 是模型类型字符串value 是创建函数注册模型启动时一次性完成分发创建运行时按需调用2.3 工厂模式解决了什么问题结果调用方不需要知道NewAliRAGModel的参数是什么新增模型只需在registerCreators()加一行注册支持运行时通过配置文件扩展新模型2.4 预留的扩展口项目里已经留好了运行时注册的方法这个方法现在没被调用但它的存在本身就是工厂模式可扩展性的体现。三、单例模式保证全局唯一3.1 为什么要单例工厂不需要单例——每次CreateAIModel返回一个新实例是正常的。但AIHelperManager必须单例如果 Manager 不是单例结果是用户切换会话时对话历史全丢了。3.2 sync.Once 实现单例sync.Once的语义无论多少个 goroutine 同时调用GetGlobalManager()once.Do()里的初始化函数只执行一次。这是 Go 里最标准、最简洁的单例写法。工厂同理3.3 Manager 的数据结构三层含义第一层 keyuserName不同用户的会话完全隔离第二层 keysessionID同一用户的不同会话互相独立每个 AIHelper 持有自己的消息历史支撑多轮对话四、三种模式的化学反应单一模式没什么稀奇把三种放在一起才是这个设计的精华。4.1 协作链路4.2 可扩展性的证明假设现在要接入 ClaudeAnthropic模型修改范围只在factory.go前端传modelType: 5后端自动路由到 Claude。这就是开闭原则的典型体现对扩展开放对修改封闭。4.3 隔离变化点策略模式和工厂模式配合实现了真正的关注点分离五、顺带的设计技巧除了三大模式这个模块里还有两个值得单独说的设计。5.1 回调注入灵活切换存储策略AIHelper的saveFunc不是硬编码的这样存储策略可以在运行时切换单元测试时可以注入一个 no-op 的 saveFunc5.2 读写锁保护并发安全sync.RWMutex的用法读多写少时读锁不互斥写锁才互斥。AI 对话场景下多个请求可能同时读消息历史但写操作添加消息是串行的——正好是 RWMutex 的最佳适用场景。六、架构图总览七、总结三种模式配合的核心价值新增一个 AI 模型只需在工厂注册一行代码其余所有代码零改动。