
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[一、模型 Provider 模块:基于策略模式统一多模型接入](#一、模型 Provider 模块:基于策略模式统一多模型接入)
[1.1 前置:策略模式原理讲解](#1.1 前置:策略模式原理讲解)
[1.1.1 传统 if-else 写法的弊端](#1.1.1 传统 if-else 写法的弊端)
[1.1.2 策略模式实现思路](#1.1.2 策略模式实现思路)
[1.2 LLMProvider 业务背景与需求](#1.2 LLMProvider 业务背景与需求)
[1.2.1 业务场景](#1.2.1 业务场景)
[1.2.2 原始调用流程](#1.2.2 原始调用流程)
[1.2.3 Provider 需要具备的通用能力](#1.2.3 Provider 需要具备的通用能力)
[1.3 使用 C++ 继承多态解决硬编码问题](#1.3 使用 C++ 继承多态解决硬编码问题)
[1.3.1 直接硬编码的缺陷](#1.3.1 直接硬编码的缺陷)
[1.3.2 解决方案:抽取顶层抽象基类 LLMProvider](#1.3.2 解决方案:抽取顶层抽象基类 LLMProvider)
[1.3.3 业务层多态调用](#1.3.3 业务层多态调用)
[1.3.4 LLMProvider和策略模式一一映射](#1.3.4 LLMProvider和策略模式一一映射)
[1.4 LLMProvider 抽象基类头文件实现](#1.4 LLMProvider 抽象基类头文件实现)
[1.5 LLMProvider 架构优势总结](#1.5 LLMProvider 架构优势总结)
[1.6 面试易错考点总结](#1.6 面试易错考点总结)
前言
在上一篇内容中,我们完成了项目基础日志模块的封装,搭建起 SDK 全局统一的日志输出能力,为后续开发提供了稳定的信息打印、故障排查底座。而随着项目继续推进,我们马上会面临一个新问题:项目后续可能需要对接多家不同厂商的大模型服务,不同平台的请求报文、鉴权方式、响应解析规则都存在差异。
如果直接写大量
if-else分支去区分每一个模型服务商,代码会迅速变得臃肿,新增、删除模型都需要改动核心业务代码,违背开闭原则。本章我们就引入策略模式,依靠 C++ 继承与多态的特性,抽离出通用的LLMProvider抽象层,把不同模型厂商的调用逻辑互相隔离。我们会先理清策略模式的核心思想,对比传统分支写法的缺陷,再结合大模型接入这个业务场景,一步步完成抽象基类的定义,搭建一套可扩展、易维护的多模型接入架构。这套架构也是整个 SDK 的核心业务骨架,后续所有模型厂商的实现,都将基于这套抽象规范来开发。
一、模型 Provider 模块:基于策略模式统一多模型接入
本章我们将使用C++ 策略模式 + 面向对象多态 ,设计一套可扩展的大模型接入层 ,也就是 LLMProvider,用来统一对接 DeepSeek、ChatGPT、Gemini、Ollama 本地模型等不同厂商大模型,屏蔽各个厂商 HTTPS 接口、报文格式的差异。
1.1 前置:策略模式原理讲解
策略模式是一种行为型设计模式,核心思想:把一系列可变的算法 / 业务行为单独封装,各个算法可以互相替换,调用方和具体实现解耦。
我们先用一个生活化的例子理解:从宿舍去往图书馆,存在多种出行方案,每一种方案就是一套独立策略:
- 步行:成本最低,速度慢
- 骑行:速度、开销均衡
- 校内公交:速度最快,花费更高
1.1.1 传统 if-else 写法的弊端
如果直接使用分支判断实现,代码大致逻辑如下:
cpp
if(没钱)
{
walk();
}
else if(想节省时间)
{
takeBus();
}
else
{
rideBike();
}
随着出行方式不断增加,if-else分支会持续膨胀,代码臃肿杂乱 ;新增方案必须修改这一段判断逻辑,违反开闭原则。
1.1.2 策略模式实现思路
- 定义顶层抽象策略接口,声明统一的行为函数
- 每一种方案,单独实现一个子类,继承这个抽象接口,重写行为方法
- 提供一个上下文管理类,持有策略对象,在运行时,你可以随时切换策略
示例代码:
cpp
// 抽象策略:出行策略
class TransportStrategy
{
public:
virtual void go() = 0;
};
// 具体策略:步行
class WalkStrategy : public TransportStrategy
{
public:
virtual void go() override { cout << "走路去机房"; }
};
// 具体策略:骑行
class BikeStrategy : public TransportStrategy
{
public:
virtual void go() override { cout << "骑车去机房"; }
};
// 具体策略:公交
class BusStrategy : public TransportStrategy
{
public:
virtual void go() override { cout << "坐公交去机房"; }
};
// 上下文类:持有策略,动态切换
class Student
{
private:
TransportStrategy* strategy;
public:
void setStrategy(TransportStrategy* s) { strategy = s; }
void goToLab() { strategy->go(); }
};
int main()
{
Student me;
me.setStrategy(new WalkStrategy());
me.goToLab();
me.setStrategy(new BusStrategy());
me.goToLab();
return 0;
}
策略模式 的优势非常明显:调用方只依赖顶层抽象接口,不需要关心底层具体实现;新增策略只需要新增子类,不需要改动原有调用逻辑,灵活、易维护。

1.2 LLMProvider 业务背景与需求
1.2.1 业务场景
我们的 SDK 需要同时对接多款大模型服务,包含云端 API 类型(DeepSeek、ChatGPT、Gemini)以及本地部署 Ollama 模型。
上层业务 希望使用一套统一接口调用模型,完全不用感知不同厂商 API 地址、请求 JSON 格式、返回解析逻辑的区别。
前面章节我们已经完成通用数据结构体、spdlog 日志封装,本章就完成模型接入层的架构设计。
1.2.2 原始调用流程
原生对接一个大模型 API,固定步骤:
- 获取厂商分配的 API-Key
- 阅读对应厂商 HTTP 接口文档
- 在代码中拼接 HTTP 请求报文,发起网络请求
- 解析返回的 JSON 数据,拿到模型应答
1.2.3 Provider 需要具备的通用能力
每一个模型接入实现类,都必须实现下面全部能力:
- **模型初始化:**读取配置,加载 api-key、接口地址 base_url
- **可用性检测:**校验配置合法性,标记当前模型是否可用
- 非流式消息发送:
stream=false,一次性返回完整回答 - 流式消息发送:
stream=true,分片增量返回,通过回调持续输出内容 - 获取模型名称
- 获取模型描述信息(用于前端展示模型简介、标签)
虽然不同厂商中 HTTP 报文中的字段类型个数或者报文结构都会有所差异,但是对于上面所需要的模型参数(如 api_key 和 base_url)以及相关函数所有大模型都是需要的,这就是我们需要抽象封装的地方。
1.3 使用 C++ 继承多态解决硬编码问题
1.3.1 直接硬编码的缺陷
如果不做抽象封装,业务代码里会写大量模型名称分支判断:
cpp
if(modelName == "deepseek")
{
DeepSeekProvider dp;
dp.sendMessage(...);
}
else if(modelName == "chatgpt")
{
ChatGPTProvider gp;
gp.sendMessage(...);
}
这种写法问题突出:每新增一款模型,就必须修改这一段业务判断代码,严重违反开闭原则,分支会越来越多,维护成本极高。
1.3.2 解决方案:抽取顶层抽象基类 LLMProvider
- 提取所有模型共有的接口 ,放到顶层基类
- 将接口声明为纯虚函数
virtual func() = 0,LLMProvider 成为抽象类 - 抽象类不允许实例化,它只用来定义接口契约,本身没有任何业务实现
- 每一个厂商的模型接入类,继承这个抽象基类,并且重写全部纯虚函数
继承关系:
- **基类:**LLMProvider(抽象接口,定义规范)
- **派生实现类:**DeepSeekProvider / ChatGPTProvider / GeminiProvider
1.3.3 业务层多态调用
C++ 多态特性:基类指针或者引用,可以指向任意一个派生类对象 。
后续我们实现的业务层代码就只需要依赖 LLMProvider 抽象基类即可,运行时传入不同子类对象,C++ 自动匹配调用子类重写的函数,直接消除大量 if-else 分支。
cpp
// 业务层统一发送消息函数,只依赖抽象基类
void SendMessage(LLMProvider& provider)
{
// 根据传入对象的实际类型,自动调用子类重写的sendMessage
provider.sendMessage(...);
}

1.3.4 LLMProvider和策略模式一一映射
我们这套 LLMProvider 架构,本质就是策略模式在大模型 SDK 中的落地:
- 抽象策略:
LLMProvider抽象基类,定义所有模型必须遵守的接口契约 - 具体策略:
DeepSeekProvider、ChatGPTProvider等模型实现子类 - 上下文: SDK 上层、
LLMManager管理器,只和抽象基类交互,运行时切换模型策略 - **开闭原则:**新增模型只需要新增派生类,原有上层业务代码无需改动
1.4 LLMProvider 抽象基类头文件实现
文件路径:SDK/include/LLMProvider.h
bash
SDK/
|-- include
| |-- LLMProvider.h
| |-- common.h
| `-- util
| `-- MyLog.h
`-- src
`-- util
|-- MyLog.cpp
`-- test_log.cc
cpp
#ifndef LLM_PROVIDER_H
#define LLM_PROVIDER_H
#include <functional>
#include <httplib.h>
#include <jsoncpp/json/json.h>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "common.h"
#include "util/MyLog.h"
using namespace ply_Log;
namespace ai_chat_sdk
{
// 云端模型提供者基类:定义了模型调用的通用接口
// 所有大模型接入都继承该类,实现全部纯虚函数,不允许实例化,定义统一的模型访问契约
class LLMProvider
{
public:
// 初始化模型:根据配置初始化模型
virtual bool initModel(const std::unordered_map<std::string, std::string> &modelConfig) = 0;
// 判断模型是否有效
virtual bool IsAvailable() = 0;
// 获取模型名称
virtual std::string getModelName() = 0;
// 获取模型描述
virtual std::string getModelDesc() = 0;
// 发送消息:全量调用,返回完整响应
virtual std::optional<std::string> sendMessage(const std::vector<Message> &messages,
const std::unordered_map<std::string, std::string> &requestParams) = 0;
// 发送消息:增量调用,实时返回响应
virtual std::optional<std::string> sendMessageStream(const std::vector<Message> &messages,
const std::unordered_map<std::string, std::string> &requestParams,
std::function<void(const std::string &response, bool isEnd)> callback) = 0;
// 回调函数:处理增量响应,isEnd表示是否为最后一段响应
// 析构函数:默认实现,确保子类能够正确析构
virtual ~LLMProvider() = default;
protected: // 保护成员:允许子类访问,确保模型状态的一致性
bool _isAvailable = false; // 标记模型是否有效
std::string _apikey; // API密钥,用于模型调用
std::string _baseUrl; // 模型基础URL
};
} // namespace ai_chat_sdk
#endif
代码分析:
- 依赖头文件:
<functional>:用于流式接口的回调std::function<optional>:函数返回值使用 optional,可以优雅表达 "调用失败无有效返回"- jsoncpp、vector、unordered_map:用来组装请求报文、保存对话消息
- 引入我们之前封装好的 common 公共结构体、MyLog 日志模块
- 纯虚函数: 所有接口都加上
=0,标识这是抽象类,子类必须全部实现,强制统一接口规范。initModel:接收配置键值对,加载 apikey、地址等参数IsAvailable:对外暴露可用性状态sendMessage:非流式对话接口sendMessageStream:流式接口,传入回调函数,每收到一段分片数据就回调通知上层,isEnd 标记是否是最后一包
- 虚析构函数
virtual ~LLMProvider() = default;:多态场景的关键,当用基类指针 delete 子类对象时,子类的析构函数能够正常执行,防止内存泄漏。 protected成员变量:- **访问权限:**子类可以直接访问,外部代码无法访问,适合存放所有模型通用的 apikey、地址、可用标记。
- 如果设置为 private,子类无法读取这些成员,就需要额外提供 get/set 接口,增加冗余代码。
1.5 LLMProvider 架构优势总结
- **消灭大量 if-else 分支:**依托 C++ 多态 + 策略模式,程序运行阶段动态切换模型策略。
- **符合开闭原则:**新增大模型时,只需要写一个继承 LLMProvider 的派生类,上层调用代码完全不用修改。
- **SDK 对外接口统一:**SDK 使用者只需要面向抽象接口开发,不用关心每家模型的 HTTP、JSON 报文细节。
- **接口强制约束:**纯虚函数强制子类必须实现全部接口能力,不会出现接口遗漏、功能缺失的问题。
- 兼容多种部署形态:同一套框架,既可以对接云端厂商 API,也可以接入本地 Ollama 部署模型,代码复用性强。
1.6 面试易错考点总结
- 包含纯虚函数的类属于抽象类,抽象类不能直接实例化对象。
- 多态继承场景,基类析构函数 必须声明为 virtual;如果不写虚析构,通过基类指针释放子类对象,子类析构不会调用,可能造成内存泄漏。
- 纯虚函数语法:
virtual 返回类型 函数名(参数列表) = 0; - **策略模式核心:**封装可变的业务行为,运行时替换策略,隔离调用方和具体实现。
protected权限的使用场景:基类成员希望子类能够直接访问,但是不对外暴露给外部调用,就使用 protected,不能用 private。
结束语
到这里,我们完成了
LLMProvider抽象基类的设计与策略模式架构的落地。我们从 if-else 硬编码的痛点出发,理解了策略模式的设计思路,借助 C++ 继承多态抽取出统一的抽象接口,将不同模型服务商的差异化调用逻辑解耦。这套抽象层是我们 SDK 的核心骨架,它遵守开闭原则:后续新增 DeepSeek、Chat-GPT 等其他模型接入实现时,只需要新增子类,不需要改动上层业务调用代码。