




接入层的定位
把 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 之后,按下面顺序推进:
- 先
make_shared出 ChatSDK 实例; - 为每个厂商分别构造配置对象——DeepSeek、ChatGPT、Gemini 走
APIConfig,Ollama 走OllamaConfig; - 把四个配置塞进
std::vector<std::shared_ptr<ai_chat_sdk::Config>>; - 调
_chatSDK->initModels()做批量初始化,校验返回值; - 最后
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