《C++ 项目:手写大模型接入 SDK》Provider分析与实现

🔥小叶-duck:个人主页

❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》

《Linux系统从入门到实践》《Linux网络从入门到实践》

《Qt 方寸极境》 《MySQL》

✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

[一、模型 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 策略模式实现思路

  1. 定义顶层抽象策略接口,声明统一的行为函数
  2. 每一种方案,单独实现一个子类,继承这个抽象接口,重写行为方法
  3. 提供一个上下文管理类,持有策略对象,在运行时,你可以随时切换策略

示例代码:

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,固定步骤:

  1. 获取厂商分配的 API-Key
  2. 阅读对应厂商 HTTP 接口文档
  3. 在代码中拼接 HTTP 请求报文,发起网络请求
  4. 解析返回的 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

  1. 提取所有模型共有的接口 ,放到顶层基类
  2. 将接口声明为纯虚函数 virtual func() = 0 ,LLMProvider 成为抽象类
  3. 抽象类不允许实例化,它只用来定义接口契约,本身没有任何业务实现
  4. 每一个厂商的模型接入类,继承这个抽象基类,并且重写全部纯虚函数

继承关系:

  • **基类:**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 架构优势总结

  1. **消灭大量 if-else 分支:**依托 C++ 多态 + 策略模式,程序运行阶段动态切换模型策略。
  2. **符合开闭原则:**新增大模型时,只需要写一个继承 LLMProvider 的派生类,上层调用代码完全不用修改。
  3. **SDK 对外接口统一:**SDK 使用者只需要面向抽象接口开发,不用关心每家模型的 HTTP、JSON 报文细节。
  4. **接口强制约束:**纯虚函数强制子类必须实现全部接口能力,不会出现接口遗漏、功能缺失的问题。
  5. 兼容多种部署形态:同一套框架,既可以对接云端厂商 API,也可以接入本地 Ollama 部署模型,代码复用性强。

1.6 面试易错考点总结

  1. 包含纯虚函数的类属于抽象类,抽象类不能直接实例化对象。
  2. 多态继承场景,基类析构函数 必须声明为 virtual;如果不写虚析构,通过基类指针释放子类对象,子类析构不会调用,可能造成内存泄漏。
  3. 纯虚函数语法:virtual 返回类型 函数名(参数列表) = 0;
  4. **策略模式核心:**封装可变的业务行为,运行时替换策略,隔离调用方和具体实现。
  5. protected权限的使用场景:基类成员希望子类能够直接访问,但是不对外暴露给外部调用,就使用 protected,不能用 private。

结束语

到这里,我们完成了LLMProvider抽象基类的设计与策略模式架构的落地。我们从 if-else 硬编码的痛点出发,理解了策略模式的设计思路,借助 C++ 继承多态抽取出统一的抽象接口,将不同模型服务商的差异化调用逻辑解耦。

这套抽象层是我们 SDK 的核心骨架,它遵守开闭原则:后续新增 DeepSeek、Chat-GPT 等其他模型接入实现时,只需要新增子类,不需要改动上层业务调用代码。

相关推荐
denlaku1 小时前
JDK 22 新特性详解
java·开发语言
会编程的小孩1 小时前
qt_day1
c++
Dawson Zhu1 小时前
【CUDA编程入门⑦】HPC算子开发实战:从优化方法论到业务落地
人工智能·语言模型·架构·aigc·agi
INGNIGHT2 小时前
377.组合总和 Ⅳ(背包)
c++·算法·1024程序员节
“AI国潮设计-小江”2 小时前
AIGC实战:Python+SDXL批量生成英歌舞IP立牌,解锁猪猪蛋糕“反差萌”流量密码(附完整API源码)
开发语言·人工智能·python·prompt·aigc
H.莓飛2 小时前
【数据结构】排序_OJ题(二):划分与选择
linux·服务器·c语言·开发语言·数据结构·visual studio
CRMEB系统商城2 小时前
服装连锁的导购分销与企微私域
java·大数据·开发语言·人工智能·小程序·php·1024程序员节
凉茶钱2 小时前
stack、queue、priority_queue、容器适配器完整梳理
开发语言·c++·queue·1024程序员节·stack
wuminyu2 小时前
JVM规范学习总结-Java源码经过javac编译后产物
java·linux·c语言·jvm·c++