C++服务端封装多厂商大模型SDK初始化实践

文章配图

接入层的定位

把 ChatSDK 直接暴露给浏览器不现实。SDK 内部聚合了会话管理、模型适配和持久化逻辑,本质是纯 C++ 库,没有网络入口。ChatServer 这层 HTTP 服务要做的,是替前端把请求接进来,把用户消息转交给 SDK,再把模型应答原样回传。它只承担网络 IO 和配置组装,对话逻辑全部下沉到 SDK 内部。

请求链路是单向分层的:

浏览器 / HTTP 客户端
      ↓ HTTP 请求
ChatServer(httplib::Server) —— HTTP 接入层
      ↓ 内部调用
ChatSDK —— 大模型 SDK 抽象层
      ↓
各模型 Provider(DeepSeek / ChatGPT / Gemini / Ollama)
      ↓
云端 API / Ollama 本地服务

ChatServer 不耦合任何具体厂商,所有模型信息都从外部 ServerConfig 传入,新增一个模型只需扩展配置,不动接入层代码。

配置外置:别把密钥写死在代码里

早期测试代码从 std::getenv() 读密钥,模型名在内部写死。正式实现改成全部走外部传入的 ServerConfig 结构体,解除硬编码,部署时换模型、换密钥、调推理参数都不必重新编译。

#pragma once
#include <httplib.h>
#include <memory>
#include <string>

#include <ai_chat_sdk/ChatSDK.h>
#include <ai_chat_sdk/util/myLog.h>

namespace ai_chat_server {


struct ServerConfig
{

    std::string host = "0.0.0.0";   
    int port = 8080;                
    std::string logLevel = "INFO";  


    double temperature = 0.7;       
    int maxTokens = 1024;          


    std::string deepseekAPIKey;
    std::string geminiAPIKey;
    std::string chatGPTAPIKey;


    std::string ollamaModelName;    
    std::string ollamaModelDesc;    
    std::string ollamaEndpoint;     
};


class ChatServer
{
public:

    explicit ChatServer(const ServerConfig& config);
    ~ChatServer();

private:

    std::unique_ptr<httplib::Server> _server = nullptr;

    std::shared_ptr<ai_chat_sdk::ChatSDK> _chatSDK = nullptr;
};

} 

host 默认 0.0.0.0 意味着服务监听所有网卡,生产环境应改成内网地址或配合反向代理,避免模型接口直接暴露在公网。

初始化做了哪几件事

构造函数拿到 ServerConfig 之后,按下面顺序推进:

  1. 先 make_shared 出 ChatSDK 实例;
  2. 为每个厂商分别构造配置对象——DeepSeek、ChatGPT、Gemini 走 APIConfig,Ollama 走 OllamaConfig;
  3. 把四个配置塞进 std::vector<std::shared_ptr<ai_chat_sdk::Config>>;
  4. 调 _chatSDK->initModels() 做批量初始化,校验返回值;
  5. 最后 make_unique 出 httplib 的 Server,再判一次是否创建成功。
#include "ChatServer.h"

namespace ai_chat_server {

ChatServer::ChatServer(const ServerConfig& config)
{

    _chatSDK = std::make_shared<ai_chat_sdk::ChatSDK>();


    auto deepseekConfig = std::make_shared<ai_chat_sdk::APIConfig>();
    deepseekConfig->_modelName = "deepseek‑chat";
    deepseekConfig->_apiKey = config.deepseekAPIKey;
    deepseekConfig->_temperature = config.temperature;
    deepseekConfig->_maxTokens = config.maxTokens;


    auto chatGPTConfig = std::make_shared<ai_chat_sdk::APIConfig>();
    chatGPTConfig->_modelName = "gpt‑4o‑mini";
    chatGPTConfig->_apiKey = config.chatGPTAPIKey;
    chatGPTConfig->_temperature = config.temperature;
    chatGPTConfig->_maxTokens = config.maxTokens;


    auto geminiConfig = std::make_shared<ai_chat_sdk::APIConfig>();
    geminiConfig->_modelName = "gemini‑2.0‑flash";
    geminiConfig->_apiKey = config.geminiAPIKey;
    geminiConfig->_temperature = config.temperature;
    geminiConfig->_maxTokens = config.maxTokens;


    auto ollamaConfig = std::make_shared<ai_chat_sdk::OllamaConfig>();
    ollamaConfig->_modelName = config.ollamaModelName;
    ollamaConfig->_modelDesc = config.ollamaModelDesc;
    ollamaConfig->_endpoint = config.ollamaEndpoint;
    ollamaConfig->_temperature = config.temperature;
    ollamaConfig->_maxTokens = config.maxTokens;


    std::vector<std::shared_ptr<ai_chat_sdk::Config>> modelConfigs = {
        deepseekConfig,
        chatGPTConfig,
        geminiConfig,
        ollamaConfig
    };


    if (!_chatSDK->initModels(modelConfigs))
    {
        ERR("ChatSDK init Failed!!!");
        return;
    }


    _server = std::make_unique<httplib::Server>();
    if (!_server)
    {
        ERR("ChatServer httplib Server init Failed!!!");
        return;
    }
}

ChatServer::~ChatServer()
{

}

} 

OllamaConfig 和 APIConfig 都继承自 ai_chat_sdk::Config,所以 vector 里必须用 shared_ptr<Config> 基类指针来装,否则类型对不上。

智能指针的所有权取舍

_server 用 unique_ptr。httplib 的 Server 是整个服务生命周期里的唯一实例,独占所有权最干净,禁止拷贝还能防止被意外复制,出作用域自动析构,服务资源不留尾巴。

_chatSDK 用 shared_ptr。它要被 HTTP 回调和多个请求上下文共享访问,引用计数管理生命周期,谁都能安全地持有一份。但这里有个容易被忽略的坑:shared_ptr 只保证控制块的引用计数线程安全,它不管指向的那个对象。多个线程同时调同一个 ChatSDK 实例的方法,shared_ptr 不会帮你加锁。高并发下必须自己确认 ChatSDK 内部是线程安全的,否则会出现数据竞争。

安装机制与链接依赖

ChatSDK 编译完执行 sudo make install,脚本把头文件复制到系统目录、库文件复制到系统库目录,业务代码就能用 #include <ai_chat_sdk/ChatSDK.h> 尖括号方式引入。安装后头文件落在 /usr/local/include/ai_chat_sdk/,静态库在 /usr/local/lib/libai_chat_sdk.a。

cpp-httplib 本身是单头文件库,以头文件形式参与编译,链接阶段必须带上 -lpthread。如果给 ChatServer 开 HTTPS,还得在编译期定义 CPPHTTPLIB_OPENSSL_SUPPORT,并额外链接 OpenSSL 的 ssl 和 crypto。

错误处理:不抛异常,靠返回值说话

initModels() 返回布尔值表示 SDK 初始化成败。失败时走 ERR() 日志宏打印,构造函数直接 return,不抛异常。上层调用方拿不到异常,只能在调用前检查 _server 和 _chatSDK 是不是 nullptr 来判断可用性。这种策略的好处是初始化阶段不会因异常打断整个进程,代价是调用方必须养成先判空再使用的习惯,否则拿着空指针去 listen 会直接崩。

还没接上的最后一段

当前只完成了构造阶段的初始化,要让服务真正跑起来,还差这几步:

  • 注册 HTTP 路由回调,接住前端 POST 过来的聊天请求;
  • 在回调内部调 _chatSDK 的发送消息接口;
  • 把 SDK 返回的应答包成 HTTP 响应回给浏览器;
  • 调 _server->listen(host, port) 进入事件循环。

几个会咬人的细节

  • 类、结构体必须包在 ai_chat_server 命名空间里,漏了编译直接找不到符号;
  • ChatSDK 大小写敏感,ChatSDk 这种拼写错误编译器毫不客气地报错;
  • make_unique / make_shared 不需要手写 new,优先用标准库工厂函数,避免裸指针泄漏;
  • 构造函数里即便初始化失败,对象依然被创建出来,所以上层在调用 listen 之前必须先校验两个智能指针是否有效。

上线前的排障与验证

把 SDK 装到 /usr/local/lib 之后,最典型的坑是链接期报 undefined reference to ...。静态库的符号解析是左到右的,依赖库必须排在引用它的目标之后,所以 target_link_libraries 里 -lai_chat_sdk 要放在它的依赖前面。若是把 SDK 改成动态库发布,装完还得跑一次 sudo ldconfig 刷新动态链接器缓存,否则运行期会找不到 .so。

验证安装是否到位,几条命令就够:

# 确认库文件装到了系统路径
find /usr/local -name "*ai_chat_sdk*"

# 查静态库里是否导出了 initModels 符号
nm /usr/local/lib/libai_chat_sdk.a | grep initModels

# 编译产物运行前,看它依赖了哪些动态库
ldd ./chat_server

服务起起来之后,用 curl 打一下健康检查接口确认监听正常:

curl -i http://127.0.0.1:8080/health

另外,API Key 现在明文躺在 ServerConfig 里,打印日志或提交配置时容易泄露。生产上应当走环境变量、配置中心或密钥管理服务注入,别把密钥写进代码仓库或明文配置文件。

0

评论0

请先
显示验证码
没有账号?注册  忘记密码?