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