C++后端项目:统一大模型接入 SDK(三)

目录

一、开篇

二、先看一个生活类比

三、回到项目,如果不策略模式,代码会变什么样?

[四、策略模式的解法:LLMProvider 抽象基类](#四、策略模式的解法:LLMProvider 抽象基类)

[4.1 源码](#4.1 源码)

[4.2 逐行拆解](#4.2 逐行拆解)

[4.3 三个子类的实现结构](#4.3 三个子类的实现结构)

五、LLMManager:把这些策略管理起来

六、开闭原则的完美体现

七、问答专场

八、总结


一、开篇

任何一个项目,随着时间推移,需求都会越来越多。加新功能 → 改老代码 → 代码越来越臃肿 → 改一个地方影响一片 → 变成屎山。

好的设计模式就是对抗代码腐烂的武器。

这一篇,我们来讲这个项目最核心的设计------策略模式 。面试官如果问你这个项目,十有八九会从这里开始问。把这篇文章吃透,面试你就稳了一半。

二、先看一个生活类比

假设你是一个大学生,每天要去实验室。你有三种方式:

  • 走路去:免费但慢
  • 骑车去:快一点但得找车
  • 坐公交去:最快但要花钱

用代码写出来,最直接的方式是:

cpp 复制代码
// if-else 大法
void goToLab(const std::string& way) {
    if (way == "walk") {
        std::cout << "走路去实验室,耗时20分钟" << std::endl;
    } else if (way == "bike") {
        std::cout << "骑车去实验室,耗时10分钟" << std::endl;
    } else if (way == "bus") {
        std::cout << "坐公交去实验室,耗时5分钟" << std::endl;
    }
}

问题在哪? 如果以后有了新的交通方式(比如滑板、打车),你就要改 goToLab 这个函数,增加一个 else if。改着改着这个函数就几百行了,而且每次改都可能引入 bug。

策略模式的做法:

cpp 复制代码
// 抽象策略:定义"去实验室"的接口
class TransportStrategy {
public:
    virtual void go() = 0;   // "去实验室"这个行为
    virtual ~TransportStrategy() = default;
};

// 具体策略1:走路
class WalkStrategy : public TransportStrategy {
    virtual void go() override {
        std::cout << "走路去实验室,耗时20分钟" << std::endl;
    }
};

// 具体策略2:骑车
class BikeStrategy : public TransportStrategy {
    virtual void go() override {
        std::cout << "骑车去实验室,耗时10分钟" << std::endl;
    }
};

// 具体策略3:公交
class BusStrategy : public TransportStrategy {
    virtual void go() override {
        std::cout << "坐公交去实验室,耗时5分钟" << std::endl;
    }
};

// 上下文:学生
class Student {
    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();   // 坐公交去------换策略不用改Student代码!

    return 0;
}

优势:

  • Student 类不需要知道任何交通方式的具体实现
  • 加新的交通方式(滑板),只需要新增 SkateboardStrategy不需要改 Student 的代码

这就是策略模式------定义一系列算法,把它们一个个封装起来,让它们可以互相替换。算法的变化独立于使用算法的客户。

三、回到项目,如果不策略模式,代码会变什么样?

我们的项目需要对接 DeepSeek、ChatGPT、Gemini 三家 AI 厂商。如果不做设计,代码会写成这样

cpp 复制代码
// 所有逻辑写在一个函数里
std::string sendMessage(const std::string& model, 
                        const std::vector<Message>& messages,
                        const std::map<std::string, std::string>& params) {
    
    std::string jsonBody;
    std::string endpoint;
    std::string path;
    
    if (model == "deepseek-chat") {
        // DeepSeek 的请求体格式
        jsonBody = buildDeepSeekBody(messages, params);
        endpoint = "https://api.deepseek.com";
        path = "/v1/chat/completions";
    } else if (model == "gpt-4o-mini") {
        // ChatGPT 的请求体格式(字段名完全不同)
        jsonBody = buildChatGPTBody(messages, params);
        endpoint = "https://api.openai.com";
        path = "/v1/responses";
    } else if (model == "gemini-2.0-flash") {
        // Gemini 又是另一套
        jsonBody = buildGeminiBody(messages, params);
        endpoint = "https://generativelanguage.googleapis.com";
        path = "/v1beta/models/gemini-2.0-flash:generateContent";
    } else {
        return "unsupported model";
    }
    
    // 发送 HTTP 请求...
    // 解析响应...
    // 每个模型的响应结构也不一样
}

这个函数的恐怖之处:

问题 说明
越长越大 加一个新模型,这个函数就膨胀一次
职责不单一 一个函数同时管了 N 个模型的细节
测试困难 没法单独测试某个模型的逻辑
新人上手难 新人要看懂所有模型才能改一行代码
合并冲突 多人在同一个函数里改,Git 冲突不断

这就是典型的"违反开闭原则"(对扩展开放,对修改关闭)。

加一个新功能(新模型),要修改已有的代码(这个巨大的 if-else 函数)。

四、策略模式的解法:LLMProvider 抽象基类

4.1 源码

cpp 复制代码
#pragma once
#include <functional>
#include <string>
#include <map>
#include <vector>
#include "common.h"

namespace ai_chat_sdk {

// LLMProvider:抽象基类(策略模式中的"抽象策略")
class LLMProvider {
public:
    // 初始化模型(注入 API Key、endpoint 等配置)
    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;

protected:
    bool _isAvailable = false;    // 标记模型是否初始化成功
    std::string _apiKey;          // API 密钥
    std::string _endpoint;        // API 基础地址
};

} // end namespace

4.2 逐行拆解

纯虚函数 = 0 的含义:

  • 等号后面的 = 0 表示这个函数是纯虚函数
  • 包含纯虚函数的类叫抽象类 ,不能实例化(不能 new LLMProvider()
  • 子类必须实现所有纯虚函数,否则子类也是抽象类,也不能实例化
  • 这就形成了一道强制契约:任何想当"AI Provider"的类,都必须实现这 6 个方法

五个纯虚函数各司其职:

函数 作用 返回值
initModel 注入配置(API Key、地址等) true/false
isAvailable 检查是否初始化成功 true/false
getModelName 返回模型标识,如 "deepseek-chat" string
getModelDesc 返回模型描述,给前端展示用 string
sendMessage 发送消息,等待完整回复 回复文本
sendMessageStream 发送消息,回调式流式接收 完整回复文本

两个重要的 protected 成员变量:

cpp 复制代码
protected:
    bool _isAvailable = false;
    std::string _apiKey;
    std::string _endpoint;
  • protected 表示:子类可以直接访问,外部不能访问
  • 每个 Provider 初始化时设置 _apiKey_endpointsendMessage 里直接用
  • _isAvailable 默认 falseinitModel 成功后才设为 true

回调函数的设计:

cpp 复制代码
std::function<void(const std::string&, bool)> callback
  • 第一个参数 const std::string&:AI 本次返回的增量文本片段
  • 第二个参数 boolfalse 表示还有后续;true 表示这是最后一块数据
  • std::function 是 C++11 的通用函数包装器,可以接受 lambda、函数指针、bind 表达式等

这个回调的设计模式叫做观察者模式的变体------Provider 产生数据,通过回调通知消费者,消费者自己决定怎么处理这些数据。

4.3 三个子类的实现结构

DeepSeekProvider:

cpp 复制代码
class DeepSeekProvider : public LLMProvider {
public:
    virtual bool initModel(const std::map<std::string, std::string>& config) override;
    virtual bool isAvailable() const override;
    virtual std::string getModelName() const override;   // 返回 "deepseek-chat"
    virtual std::string getModelDesc() const override;
    virtual std::string sendMessage(...) override;
    virtual std::string sendMessageStream(...) override;
};

GeminiProvider 和 OllamaLLMProvider 同理

关键点:接口完全一样,但内部实现完全不同。 DeepSeek 调 /v1/chat/completions,ChatGPT 调 /v1/responses,Gemini 调 /v1beta/models/...:generateContent。但因为都继承自 LLMProvider,上层代码感知不到这些差异

五、LLMManager:把这些策略管理起来

光有策略还不够,还需要一个地方来注册所有策略 ,并按模型名称路由

cpp 复制代码
class LLMManager {
public:
    // 注册一个 Provider
    bool registerProvider(const std::string& modelName, 
                          std::unique_ptr<LLMProvider> provider);

    // 初始化指定模型
    bool initModel(const std::string& modelName,
                   const std::map<std::string, std::string>& params);

    // 检查模型是否可用
    bool isModelAvailable(const std::string& modelName) const;

    // 获取可用模型列表
    std::vector<ModelInfo> getAvailableModels() const;

    // 发送消息(内部按 modelName 找到对应的 Provider,调它的 sendMessage)
    std::string sendMessage(const std::string& modelName, ...);
    std::string sendMessageStream(const std::string& modelName, ..., callback);

private:
    // 注册表:模型名称 → Provider(unique_ptr 独占所有权)
    std::map<std::string, std::unique_ptr<LLMProvider>> _providers;
    // 模型信息表
    std::map<std::string, ModelInfo> _modelInfos;
};

核心方法 sendMessage 的实现:

cpp 复制代码
std::string LLMManager::sendMessage(const std::string& modelName,
                                    const std::vector<Message>& messages,
                                    const std::map<std::string, std::string>& params) {
    // 1. 根据模型名称找到对应的 Provider
    auto it = _providers.find(modelName);
    if (it == _providers.end()) {
        ERR("model provider not found: {}", modelName);
        return "";
    }

    // 2. 检查是否可用
    if (!it->second->isAvailable()) {
        ERR("model not available: {}", modelName);
        return "";
    }

    // 3. 调用多态,it->second 是 LLMProvider*,实际指向 DeepSeekProvider
    return it->second->sendMessage(messages, params);
}

关键点就在第 3 步:it->second->sendMessage(messages, params)

it->second 的类型是 unique_ptr<LLMProvider>,但实际指向的是 DeepSeekProviderChatGPTProvider 对象。调用 sendMessage 时,C++ 的虚函数机制会动态地找到实际对象的 sendMessage 方法。 这就是多态(polymorphism)

六、开闭原则的完美体现

现在问你:如果要加一个新模型,比如阿里的通义千问(Qwen),需要改几行代码?

答案是:新增两个文件,改一行代码。

cpp 复制代码
// 改一行:在 ChatSDK::registerAllProvider() 中
// 原来的代码:
_llmManager.registerProvider("deepseek-chat", make_unique<DeepSeekProvider>());
_llmManager.registerProvider("gpt-4o-mini", make_unique<ChatGPTProvider>());
_llmManager.registerProvider("gemini-2.0-flash", make_unique<GeminiProvider>());

// 新加一行:
_llmManager.registerProvider("qwen-max", make_unique<QwenProvider>());

不需要修改的代码:

  • ❌ 不需要改 LLMProvider.h(基类不变)
  • ❌ 不需要改 LLMManager.cpp(路由逻辑不变)
  • ❌ 不需要改 ChatSDK.cpp(外观模式不变)
  • ❌ 不需要改 ChatServer.cpp(HTTP 路由不变)
  • ❌ 不需要改 DeepSeekProvider.cpp(已有的不受影响)

只新增(对扩展开放),不修改(对修改关闭) 。这就是开闭原则

七、问答专场

Q1:说说策略模式在项目中的应用?

我们这个项目需要对接多家 AI 厂商的 API。每家厂商的调用方式、请求格式、响应结构都不一样。如果用 if-else 来判断模型类型,代码会变得臃肿且难以维护。

所以我们设计了 LLMProvider 抽象基类,定义六个纯虚函数作为契约。DeepSeekProvider、ChatGPTProvider、GeminiProvider 分别继承并实现自己的版本。LLMManager 维护一个模型名称到 Provider 的映射表,调用时按名称找到对应的 Provider,利用 C++ 的虚函数多态机制自动调用正确的实现。

这样当我们要加新模型时,只需要新增一个 Provider 子类,在注册表里加一行,不需要修改任何现有代码。符合开闭原则

Q2:纯虚函数和虚函数有什么区别?

对比 虚函数(virtual) 纯虚函数(virtual ... = 0)
基类是否需要实现 可以有自己的实现 不能有实现
子类是否必须重写 可以不重写(用基类的) 必须重写
基类能否实例化 可以 不能(抽象类)

Q3:为什么用 unique_ptr 而不是 shared_ptr 管理 Provider?

复制代码
std::map<std::string, std::unique_ptr<LLMProvider>> _providers;
  • unique_ptr 表示独占所有权:Provider 的生命周期由 LLMManager 独占管理
  • unique_ptr 没有引用计数,零开销抽象
  • shared_ptr 适合共享所有权场景,这里不需要

Q4:std::move 在这里的作用?

复制代码
_llmManager.registerProvider("deepseek-chat", std::move(deepseekProvider));

unique_ptr 不能拷贝(拷贝构造函数被 delete 了),只能通过移动语义转移所有权。std::movedeepseekProvider 的所有权转移给 _providers 内部,原来的 deepseekProvider 变成空指针。

Q5:回调函数 std::function<void(const string&, bool)>,为什么不用函数指针?

  • std::function 可以包装 lambda、函数指针、bind 表达式等
  • C++ 的函数指针不能捕获外部变量,而 lambda 可以
  • 在实际使用中,sendMessageStream 的回调经常需要捕获 this 指针或局部变量,lambda 更方便

八、总结

这一篇的核心就一句话:

面向接口编程,而不是面向实现编程。

把变化的部分(各厂商的 API 差异)封装在独立的类中,让稳定的部分(调用逻辑、会话管理、HTTP 路由)依赖于稳定的抽象接口。

整个项目中用到策略模式的文件:

cpp 复制代码
LLMProvider.h          ← 抽象策略(6 个纯虚函数的接口)
DeepSeekProvider.cpp   ← 具体策略 1
ChatGPTProvider.cpp    ← 具体策略 2
GeminiProvider.cpp     ← 具体策略 3
OllamaLLMProvider.cpp  ← 具体策略 4
LLMManager.cpp         ← 策略的路由/管理器
ChatSDK.cpp            ← 策略的使用者(通过 LLMManager 间接使用)
相关推荐
Brilliantwxx2 小时前
【C++】 继承与多态(下)
开发语言·c++
C+++Python2 小时前
C++考试语法知识
开发语言·c++
凯瑟琳.奥古斯特3 小时前
操作系统核心结构解析
java·开发语言·c++·python·职场和发展
郭郭的柳柳在学FPGA3 小时前
千兆以太网@——帧格式
java·开发语言·网络
handler013 小时前
【Linux 网络】一文读懂 HTTP 协议
linux·c语言·网络·c++·笔记·网络协议·http
我还记得那天3 小时前
用C语言实现一个简易扫雷小游戏
c语言·开发语言
段ヤシ.3 小时前
回顾Java知识点,面试题汇总Day10(持续更新)
java·开发语言·spring
小明同学013 小时前
C++后端项目:统一大模型接入 SDK(二)
开发语言·c++
我不是懒洋洋3 小时前
【C++】类和对象( 类的定义、实例化、 this指针、 C++和C语言实现Stack对比)
c语言·开发语言·数据结构·c++·经验分享·算法·visual studio