【C++ AI 大模型接入 SDK】 - LLMProvider 抽象基类与策略模式


大家好,我是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--- modelConfigrequestParam用键值对传参(名字+内容)
<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 引用,不会拷贝整个 vectorconst 保证函数内部不会修改消息列表。

全量 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 是因为 DeepSeekProviderChatGPTProvider 等子类需要直接读写这些成员:

  • _apiKey --- 子类的 initModel() 中从 modelConfig 取出 API Key 存入此字段,发送请求时需要用它设置 HTTP Header(Authorization: Bearer sk-xxx
  • _endpoint --- 子类需要知道往哪个 URL 发 HTTP 请求
  • _isAvailable --- 子类的 initModel() 成功后设为 trueisAvailable() 直接返回它的值

如果用 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(具体策略)DeepSeekProviderChatGPTProvider 等,各自实现具体的 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>());

LLMManagerChatServer 的代码一行都不用改


八、子类长什么样?

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 的骨架

  1. 定义统一接口 --- 6 个纯虚函数,所有模型 Provider 必须实现
  2. 提供公共状态 --- protected 成员让子类共享 _isAvailable_apiKey_endpoint
  3. 实现策略模式 --- LLMManager 通过基类指针多态调用,不依赖具体模型实现
  4. 回调机制 --- sendMessageStreamstd::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 流式响应处理等核心细节。

相关推荐
Sylvia-girl5 小时前
R语言概述
开发语言·r语言
BirdenT8 小时前
20260519紫题训练
c++·算法
Highcharts.js13 小时前
倒置百分比堆叠面积图表示列详解|Highcharts大气成分图表代码
开发语言·信息可视化·highcharts·图表开发·面积图·图表示例·推叠图
csdn_aspnet13 小时前
C语言 Lomuto分区算法(Lomuto Partition Algorithm)
c语言·开发语言·算法
晨曦中的暮雨13 小时前
4.15腾讯 CSIG云服务产线 一面
java·开发语言
存在morning13 小时前
【GO语言开发实践】二 GO 并发快速上手
大数据·开发语言·golang
xiaoerbuyu123314 小时前
开源Java 邮箱 基于SpringBoot+Vue前后端分离的电子邮件
java·开发语言
C+++Python15 小时前
C++ 进阶学习完整指南
java·c++·学习
sparEE15 小时前
c++值类别、右值引用和移动语义
开发语言·c++