
大家好,我是Halcyon.平安
欢迎文末添加好友交流,共同进步!

-
- 一、文件概述
- 二、完整源码
- 三、头文件包含与命名空间
- 四、纯虚函数逐个解析
-
- [4.1 initModel --- 初始化模型](#4.1 initModel — 初始化模型)
- [4.2 isAvailable --- 检测模型是否可用](#4.2 isAvailable — 检测模型是否可用)
- [4.3 getModelName / getModelDesc --- 获取模型信息](#4.3 getModelName / getModelDesc — 获取模型信息)
- [4.4 sendMessage --- 全量发送消息](#4.4 sendMessage — 全量发送消息)
- [4.5 sendMessageStream --- 流式发送消息](#4.5 sendMessageStream — 流式发送消息)
- [五、protected 成员](#五、protected 成员)
-
- [5.1 为什么用 protected 而不是 private?](#5.1 为什么用 protected 而不是 private?)
- [5.2 成员默认值](#5.2 成员默认值)
- [5.3 _apiKey 和 _endpoint 的赋值时机](#5.3 _apiKey 和 _endpoint 的赋值时机)
- [六、为什么 LLMProvider 不定义析构函数?](#六、为什么 LLMProvider 不定义析构函数?)
- 七、策略模式
-
- [7.1 什么是策略模式?](#7.1 什么是策略模式?)
- [7.2 策略模式在本项目中的体现](#7.2 策略模式在本项目中的体现)
- [7.3 多态的原理](#7.3 多态的原理)
- [7.4 策略模式的好处](#7.4 策略模式的好处)
- 八、子类长什么样?
- 九、总结
一、文件概述
LLMProvider.h 定义了 SDK 中最核心的抽象基类 LLMProvider,它是所有大模型接入的统一接口。不管是 DeepSeek、ChatGPT、Gemini 还是 Ollama,都继承这个基类并实现它的纯虚函数。
plain
LLMProvider(抽象基类)
├── initModel() 纯虚 --- 初始化模型配置
├── isAvailable() 纯虚 --- 检测模型是否可用
├── getModelName() 纯虚 --- 获取模型名称
├── getModelDesc() 纯虚 --- 获取模型描述
├── sendMessage() 纯虚 --- 全量发送消息
├── sendMessageStream() 纯虚 --- 流式发送消息
│
└── protected 成员
├── _isAvailable 模型是否可用
├── _apiKey API 密钥
└── _endpoint API 地址
它没有
.cpp文件,因为纯虚函数没有默认实现,全部由子类提供。
二、完整源码
cpp
#pragma once
#include <functional>
#include <string>
#include <map>
#include <vector>
#include "common.h"
namespace ai_chat_sdk{
// LLMProvider 类
class LLMProvider{
public:
// 初始化模型
virtual bool initModel(const std::map<std::string, std::string>& modelConfig) = 0;
// 检测模型是否有效
virtual bool isAvailable() const = 0;
// 获取模型名称
virtual std::string getModelName() const = 0;
// 获取模型描述
virtual std::string getModelDesc() const = 0;
// 发送消息 - 全量返回
virtual std::string sendMessage(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam) = 0;
// 发送消息 - 增量返回 - 流式响应
virtual std::string sendMessageStream(const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam,
std::function<void(const std::string&, bool)> callback) = 0; // callback: 对模型返回的增量数据如何处理,第一个参数为增量数据,第二个参数为是否为最后一个增量数据
protected:
bool _isAvailable = false; // 标记模型是否有效
std::string _apiKey; // API密钥
std::string _endpoint; // 模型API endpoint base url
};
}
接下来我们逐块解析代码!
三、头文件包含与命名空间
cpp
#pragma once
#include <functional>
#include <string>
#include <map>
#include <vector>
#include "common.h"
| 头文件 | 引入的原因 |
|---|---|
<functional> |
std::function---sendMessageStream的回调参数需要它(专门用于流式返回!) |
<string> |
std::string--- 几乎每个接口都用字符串 |
<map> |
std::map--- modelConfig和 requestParam用键值对传参(名字+内容) |
<vector> |
std::vector --- messages消息列表用 vector |
"common.h" |
Message结构体定义在这里,sendMessage的参数依赖它 |
#pragma once是编译器级别的 include guard,保证头文件只被编译一次,效果等同于传统的#ifndef/#define/#endif写法,但更简洁且被主流编译器广泛支持。
四、纯虚函数逐个解析
4.1 initModel --- 初始化模型
cpp
virtual bool initModel(const std::map<std::string, std::string>& modelConfig) = 0;
virtual:声明为虚函数,允许子类重写(override)= 0:纯虚函数,没有默认实现,必须在子类中实现,否则子类也变成抽象类,无法实例化- 参数
modelConfig:用std::map<std::string, std::string>传键值对配置,不同模型的配置内容不同:
| 模型 | modelConfig 中典型的 key |
|---|---|
| DeepSeek | "apiKey", "endpoint", "modelName" |
| ChatGPT | "apiKey", "endpoint", "modelName" |
| Gemini | "apiKey", "endpoint", "modelName" |
| Ollama | "endpoint", "modelName", "modelDesc"(无 apiKey) |
- 返回
bool:初始化成功返回true,失败返回false
用
map<string, string>而不是专门的结构体,好处是灵活------不同模型需要的参数不同,不需要为每种模型定义不同的函数签名。调用方只需要把配置塞进 map,子类自己按 key 取值。
4.2 isAvailable --- 检测模型是否可用
cpp
virtual bool isAvailable() const = 0;
const:这是一个只读方法,不会修改对象状态。调用者可以放心地随时查询- 返回
protected成员_isAvailable的值 - 在模型未初始化 或初始化失败 时返回
false,初始化成功返回true
在 LLMManager 中的实际调用:
cpp
// LLMManager.cpp --- 发送消息前先检查模型是否可用
if(!it->second->isAvailable()){
ERR("model not available, modelName = {}", modelName);
return "";
}
这说明
isAvailable()是一道安全门,防止在模型未就绪时发送请求。
4.3 getModelName / getModelDesc --- 获取模型信息
cpp
virtual std::string getModelName() const = 0;
virtual std::string getModelDesc() const = 0;
两个简单的 getter 方法,都标记为 const:
getModelName()--- 返回模型名称,如"deepseek-chat"、"gpt-4o-mini"getModelDesc()--- 返回模型描述,用于前端展示
在 LLMManager 中的实际调用:
cpp
// LLMManager.cpp --- 初始化成功后,获取模型描述信息
_modelInfos[modelName]._modelDesc = it->second->getModelDesc();
LLMManager在初始化模型后调用getModelDesc(),把描述信息存入ModelInfo,供上层查询。
4.4 sendMessage --- 全量发送消息
cpp
virtual std::string sendMessage(
const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam) = 0;
逐个参数分析:
| 参数 | 类型 | 说明 |
|---|---|---|
messages |
const std::vector<Message>& |
完整的对话历史(user + assistant 交替排列) |
requestParam |
const std::map<std::string, std::string>& |
请求级别的附加参数(如温度、maxTokens 等) |
- 返回值:模型生成的完整回复文本(一次性全部返回)
messages传的是const引用,不会拷贝整个 vector 。const保证函数内部不会修改消息列表。
全量 vs 流式的区别:
plain
全量(sendMessage) 流式(sendMessageStream)
───────────────── ─────────────────────
请求 ──→ 等待... 请求 ──→ 收到 "你"
等待... 收到 "好"
等待... 收到 "!"
一次性返回完整回复 收到 "我是AI助手"
收到 [结束标记]
优点:简单直接 优点:用户体验好,逐字显示
缺点:等待时间长 缺点:实现复杂,需要解析流式数据
4.5 sendMessageStream --- 流式发送消息
cpp
virtual std::string sendMessageStream(
const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam,
std::function<void(const std::string&, bool)> callback) = 0;
前两个参数和 sendMessage 一样,新增的是第三个参数 callback:
cpp
std::function<void(const std::string&, bool)>
这是一个回调函数 ,类型是 std::function,签名为 void(const string&, bool):
| 回调参数 | 含义 |
|---|---|
const string& |
本次增量数据(一小段文本) |
bool |
是否是最后一个增量数据(流式结束标记) |
回调的工作原理:
plain
模型返回流式数据 Provider 内部解析 通过 callback 回调给上层
────────────── ─────────────── ────────────────────
data: {"content":"你"} → 解析出 "你" → callback("你", false)
data: {"content":"好"} → 解析出 "好" → callback("好", false)
data: {"content":"!"} → 解析出 "!" → callback("!", false)
data: [DONE] → 流式结束 → callback("", true)
上层(如 ChatServer)注册这个回调,每收到一段文本就立即通过 HTTP SSE 推送给前端,实现"打字机"效果。
为什么用 std::function 而不是函数指针?
std::function 是 C++11 引入的通用可调用对象包装器,比函数指针更强大:
- 可以包装普通函数
- 可以包装Lambda 表达式(本项目中大量使用)
- 可以包装成员函数 (配合
std::bind) - 可以包装仿函数 (重载了
operator()的类)
项目中实际使用 Lambda 作为回调:
cpp
// ChatServer.cpp 中的实际用法(后续博客会详细解析)
sdk.sendMessageStream(sessionId, content,
[&res](const std::string& chunk, bool isDone){
// chunk: 增量文本
// isDone: 是否结束
res << "data: " << chunk << "\n\n";
});
五、protected 成员
cpp
protected:
bool _isAvailable = false; // 标记模型是否有效
std::string _apiKey; // API密钥
std::string _endpoint; // 模型API endpoint base url
5.1 为什么用 protected 而不是 private?
| 访问级别 | 本类 | 子类 | 外部 |
|---|---|---|---|
public |
✓ | ✓ | ✓ |
protected |
✓ | ✓ | ✗ |
private |
✓ | ✗ | ✗ |
用 protected 是因为 DeepSeekProvider、ChatGPTProvider 等子类需要直接读写这些成员:
_apiKey--- 子类的initModel()中从modelConfig取出 API Key 存入此字段,发送请求时需要用它设置 HTTP Header(Authorization: Bearer sk-xxx)_endpoint--- 子类需要知道往哪个 URL 发 HTTP 请求_isAvailable--- 子类的initModel()成功后设为true,isAvailable()直接返回它的值
如果用
private,子类就访问不到了,就必须写 getter/setter,而这些字段只在继承体系内部使用,没必要暴露给外部。
5.2 成员默认值
cpp
bool _isAvailable = false;
C++11 引入的类内初始值 (in-class initializer)。所有子类对象创建时,_isAvailable 自动为 false,直到 initModel() 成功才改为 true。这样能保证"未初始化的模型一定不可用"。
5.3 _apiKey 和 _endpoint 的赋值时机
这两个字段没有默认值(空字符串),在子类的 initModel() 中被赋值:
plain
子类::initModel(modelConfig)
│
├── 从 modelConfig["apiKey"] 取值 → _apiKey
├── 从 modelConfig["endpoint"] 取值 → _endpoint
├── 设置 HTTP 客户端、验证连通性...
└── 成功后 → _isAvailable = true
六、为什么 LLMProvider 不定义析构函数?
注意到 LLMProvider 没有 virtual ~LLMProvider() = default;。
这是因为本项目中 LLMProvider 的生命周期通过 std::unique_ptr<LLMProvider> 管理,LLMManager 持有的是 unique_ptr,销毁时会直接调子类的析构函数。子类(如 DeepSeekProvider)使用的是编译器默认生成的析构函数,没有需要特殊清理的资源(HTTP 客户端等成员变量会自动析构)。
实际上,作为被多态使用的基类,加上 virtual ~LLMProvider() = default; 是更规范的 C++ 做法,能确保通过基类指针 delete 子类对象时调用正确的析构链。当前代码能正常工作是因为 unique_ptr 的类型信息在编译期已知。
七、策略模式
7.1 什么是策略模式?
策略模式(Strategy Pattern) 的核心思想:定义一组算法(策略),把它们各自封装成独立的类,让它们可以互相替换。调用方只依赖统一的接口,不需要知道具体用的是哪种算法。
在本项目中:
plain
┌──────────────────────┐
│ LLMProvider │ ← 抽象策略(Strategy)
│ (纯虚接口) │
└──────────┬───────────┘
│ 继承
┌──────────────┼──────────────┬──────────────┐
↓ ↓ ↓ ↓
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ DeepSeek │ │ ChatGPT │ │ Gemini │ │ Ollama │ ← 具体策略
│ Provider │ │ Provider │ │ Provider │ │ Provider │ (ConcreteStrategy)
└───────────┘ └───────────┘ └───────────┘ └───────────┘
↑ ↑ ↑ ↑
└──────────────┴──────────────┴──────────────┘
由 LLMManager 统一管理
(Context 角色)
- Strategy(抽象策略) →
LLMProvider,定义了sendMessage等统一接口 - ConcreteStrategy(具体策略) →
DeepSeekProvider、ChatGPTProvider等,各自实现具体的 API 调用逻辑 - Context(上下文) →
LLMManager,持有LLMProvider指针,根据模型名称分发请求
7.2 策略模式在本项目中的体现
以 LLMManager::sendMessage() 为例(LLMManager.cpp):
cpp
std::string LLMManager::sendMessage(const std::string& modelName,
const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam){
// 1. 根据 modelName 找到对应的 Provider
auto it = _providers.find(modelName);
if(it == _providers.end()){
ERR("model provider not found, modelName = {}", modelName);
return "";
}
// 2. 检查是否可用
if(!it->second->isAvailable()){
ERR("model not available, modelName = {}", modelName);
return "";
}
// 3. 通过基类指针调用子类实现 ------ 多态!
return it->second->sendMessage(messages, requestParam);
}
关键在第 3 步:it->second 的类型是 unique_ptr<LLMProvider>(基类指针),但实际调用的 sendMessage 会根据对象的实际类型 (DeepSeekProvider 还是 ChatGPTProvider)自动分发到正确的子类实现。这就是 C++ 多态的工作方式。
7.3 多态的原理
cpp
// LLMManager 内部
std::map<std::string, std::unique_ptr<LLMProvider>> _providers;
// 注册时存入子类对象
_providers["deepseek-chat"] = std::make_unique<DeepSeekProvider>();
_providers["gpt-4o-mini"] = std::make_unique<ChatGPTProvider>();
// 调用时,基类指针 → 自动调到子类实现
_providers["deepseek-chat"]->sendMessage(msgs, params);
// 实际执行 DeepSeekProvider::sendMessage()
_providers["gpt-4o-mini"]->sendMessage(msgs, params);
// 实际执行 ChatGPTProvider::sendMessage()
这就是 virtual 函数的作用:编译器为每个包含虚函数的对象创建一个 虚函数表(vtable),存储每个虚函数的实际地址。运行时通过 vtable 查找,实现"基类指针调子类方法"。
cpp
LLMProvider* ptr = new DeepSeekProvider();
ptr->sendMessage(msgs, params);
编译器做的事情:
1. ptr 指向 DeepSeekProvider 对象
2. 通过对象的 vptr 找到 DeepSeekProvider 的 vtable
3. 从 vtable 中查到 sendMessage 的实际地址
4. 调用 DeepSeekProvider::sendMessage()
7.4 策略模式的好处
| 好处 | 在本项目中的体现 |
|---|---|
| 开闭原则 | 新增模型只需写一个新的 XXXProvider子类,不用改 LLMManager代码 |
| 解耦 | LLMManager不知道也不关心每个模型的具体 API 细节 |
| 可替换 | 切换模型只需换一个 Provider 注册,调用方代码完全不变 |
| 可测试 | 可以写一个 MockProvider 做单元测试,不需要真实的 API 调用 |
假设未来要接入 Claude 模型,只需要:
cpp
class ClaudeProvider : public LLMProvider{
// 实现 6 个纯虚函数
};
然后在 ChatSDK 中注册:
cpp
registerProvider("claude-3", std::make_unique<ClaudeProvider>());
LLMManager 和 ChatServer 的代码一行都不用改。
八、子类长什么样?
以 DeepSeekProvider.h 为例,看一下子类的声明:
cpp
class DeepSeekProvider : public LLMProvider{
public:
virtual bool initModel(const std::map<std::string, std::string>& modelConfig);
virtual bool isAvailable() const;
virtual std::string getModelName() const;
virtual std::string getModelDesc() const;
virtual std::string sendMessage(const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam);
virtual std::string sendMessageStream(const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam,
std::function<void(const std::string&, bool)> callback);
};
可以看到:
public LLMProvider--- 公有继承,表示"DeepSeekProvider 是一种 LLMProvider"- 重写了基类的全部 6 个纯虚函数
- 函数签名和基类完全一致(参数类型、返回类型、const 修饰)
- 没有
= 0,因为子类提供了具体实现
下一篇将深入 DeepSeekProvider 的实现,解析第一个具体 Provider 是如何调用 DeepSeek API 的。
九、总结
LLMProvider 是整个 SDK 的骨架:
- 定义统一接口 --- 6 个纯虚函数,所有模型 Provider 必须实现
- 提供公共状态 ---
protected成员让子类共享_isAvailable、_apiKey、_endpoint - 实现策略模式 ---
LLMManager通过基类指针多态调用,不依赖具体模型实现 - 回调机制 ---
sendMessageStream的std::function回调,让上层灵活处理流式数据
plain
调用链路:
ChatSDK::sendMessage(sessionId, content)
→ SessionManager 组装 messages
→ LLMManager::sendMessage(modelName, messages, params)
→ _providers[modelName]->sendMessage(messages, params) ← 多态分发
→ DeepSeekProvider::sendMessage() 或 ChatGPTProvider::sendMessage() ...
下一篇将实现第一个具体策略 ------ DeepSeekProvider,包括 HTTP 请求构造、JSON 解析、SSE 流式响应处理等核心细节。