C++大模型SDK开发实录:从架构设计到流式交互实现
在当今的人工智能应用开发中,如何高效、统一地接入不同厂商的大语言模型(如DeepSeek、OpenAI、Gemini)是一个核心挑战。本项目旨在构建一个名为ChatSDK的C++开发包,屏蔽底层API差异,提供统一的会话管理、全量与流式消息发送功能。
本文将首先展示项目的最终形态与环境部署流程,随后深入剖析从零开始构建该SDK的每一个技术细节,涵盖API协议分析、数据架构设计、高性能日志系统封装以及策略模式在模型接入层中的应用。
第一章:成品演示与快速上手
在深入底层代码之前,首先展示ChatSDK的最终运行效果以及如何在服务器环境中部署和使用该SDK。
1.1 项目功能与运行效果
ChatSDK 旨在为C++开发者提供一套简洁的接口,支持以下核心功能:
- 多模型支持:通过统一配置接入不同厂商模型。
- 会话管理:创建会话、获取列表、删除会话及历史消息回溯。
- 双模式响应:支持同步全量返回(适合后台处理)和异步流式返回(适合打字机效果)。
下图展示了基于ChatSDK编写的终端演示程序(Demo)的运行界面。可以看到,用户与模型进行了交互,程序成功输出了DeepSeek模型的回复内容。
1.2 环境依赖与构建工具链
本项目基于Linux环境开发,依赖一系列成熟的开源库来处理网络、日志、JSON解析及测试。
基础依赖安装
在服务器端(推荐使用Trae CN连接),首先需要配置C++编译环境(clangd, CMake Tools)及以下系统库:
# gflags:用于解析命令行参数sudoapt-getinstalllibgflags-dev# spdlog:高性能C++日志库,提供异步日志支持sudoapt-getinstalllibspdlog-dev# fmt:格式化库,spdlog的核心依赖sudoapt-getinstallfmt# jsoncpp:用于处理模型API交互中的JSON数据序列化与反序列化sudoapt-getinstalllibjsoncpp-dev# gtest:Google单元测试框架sudoapt-getinstalllibgtest-dev# OpenSSL:支持HTTPS加密通信,保障API Key安全sudoapt-getinstalllibssl-dev# cmake:跨平台构建系统sudoapt-getinstallcmake# pkg-config:编译时库文件路径查找工具sudoaptinstallpkg-config# curl:命令行网络工具,用于初步测试API连通性sudoaptinstallcurl源码获取与目录结构
获取SDK源码主要有两种方式:通过Git克隆或直接上传压缩包。
gitclone https://gitee.com/zhibite-edu/ai-model-acess-dev.git如果网络环境受限,可将源码下载为Zip包上传至服务器并解压。如下图所示,在服务器中创建ChatSDK文件夹,解压后的源码位于SDK-source目录中。
1.3 编译构建与排错流程
项目采用 CMake 进行构建管理。标准的构建流程推荐采用“外部构建”方式,即在源码目录外创建一个build文件夹,以保持源码整洁。
初始化构建目录:
进入SDK源码路径ChatSDK/SDK-source/ai-model-acess-tech-master/AIModelAcessTech/sdk,执行以下命令:mkdirbuild&&cdbuild生成Makefile:
在build目录下执行 CMake 配置命令,读取上级目录的CMakeLists.txt:cmake..编译与依赖修复:
执行make命令开始编译。在初次编译时,系统抛出了错误,提示找不到httplib.h。这是因为项目内部使用了
cpp-httplib库但环境未安装。需执行以下命令补全依赖:sudoapt-getupdate&&sudoapt-getinstall-y libcpp-httplib-dev修复后重新执行
make,编译成功。
1.4 核心架构解析与安装
ChatSDK 采用了清晰的分层架构,核心逻辑代码位于src目录下,其中util文件夹包含了模型接入的具体实现。
- 核心框架类:
ChatSDK.cpp:对外门面,封装内部复杂性。LLMManager.cpp:模型生命周期管理与路由。SessionManager.cpp:会话状态与内存管理。DataManager.cpp:基于SQLite的数据持久化层。
- 模型提供者类:
ChatGPTProvider.cpp/GeminiProvider.cpp/DeepSeekProvider.cpp:实现了具体的API调用逻辑,支持自定义 Endpoint 以适配中转站。
API 接口概览:
SDK提供了initModels(初始化)、createSession(创建会话)、sendMessage(全量发送)及sendMessageStream(流式发送)等标准接口。
安装库文件:
编译完成后,执行安装命令将头文件和库文件部署到系统目录:
sudomakeinstall验证安装结果,头文件位于/usr/local/include/ai_chat_sdk/,库文件位于/usr/local/lib/。
1.5 客户端集成演示 (ChatDemo)
为了验证SDK的可用性,编写一个名为chatDemo.cpp的客户端程序。该程序演示了如何调用DeepSeek模型进行流式对话。
#include<ai_chat_sdk/chat_sdk.h>#include<ai_chat_sdk/my_logger.h>//打印日志#include<iostream>#include<limits>voidsendMessageStream(ai_chat_sdk::ChatSDK&chat_SDK,std::string session_id){std::cout<<"-----------------------------发送消息-----------------------------"<<std::endl;std::string message;std::getline(std::cin,message);//获取用户输入chat_SDK.sendMessageStream(session_id,message,[](conststd::string&response,booldone){std::cout<<"assistant消息 >"<<response<<std::endl;if(done)//如果是最后一条消息{std::cout<<"-----------------------------消息接收完成-----------------------------"<<std::endl;}});//发送消息std::cout<<"-----------------------------消息发送完成-----------------------------"<<std::endl;std::cout<<"请输入您的消息:"<<std::endl;std::cin.ignore(std::numeric_limits<std::streamsize>::max(),'\n');//清除输入缓冲区std::cout<<"您输入的消息是:"<<message<<std::endl;}intmain(){bite::Logger::init_logger("aiChatDemo","stdout",spdlog::level::info);//创建一个控制台日志记录器ai_chat_sdk::ChatSDK chatSDK;//创建一个SDK的对象//配置deepseek模型ai_chat_sdk::ApiConfig deepseek;//创建一个配置对象deepseek.api_key=std::getenv("deepseek_apikey");//设置API Keydeepseek.temperature=0.7;//设置温a度deepseek.max_tokens=2048;//设置最大token数deepseek.model_name="deepseek-chat";//设置模型名称std::vector<std::shared_ptr<ai_chat_sdk::Config>>configs;//创建一个配置对象的向量configs.push_back(std::make_shared<ai_chat_sdk::ApiConfig>(deepseek));//添加Deepseek的配置对象//初始化模型chatSDK.initModels(configs);std::cout<<"-----------------------------创建会话-----------------------------"<<std::endl;std::string session_id=chatSDK.createSession("deepseek-chat");//创建一个会话std::cout<<"session_id:"<<session_id<<std::endl;intuerOP=1;//用户操作while(true){std::cout<<"-------------------1、send message 0、exit------------------------"<<std::endl;std::cin>>uerOP;if(uerOP==0){break;}getchar();//输入用户操作选择之后,需要接受到缓冲区中的回车sendMessageStream(chatSDK,session_id);//给会话发送消息}return0;}配合相应的CMakeLists.txt进行编译:
# 项目名称为AIChatDemo project(AIChatDemo) # 设置C++标准,使用C++17标准进行编译,REQUIRED表示编译器如果不支持C++17则报错 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 设置构建类型为Debug set(CMAKE_BUILD_TYPE Debug) # 添加可执行文件 add_executable(AIChatDemo chatDemo.cpp) # 查找OpenSSL包 find_package(OpenSSL REQUIRED) # 将OpenSSL的头文件目录添加到编译器的搜索路径中 include_directories(${OPENSSL_INCLUDE_DIR}) # 设置库的目录,链接器会在这个目录中查找需要的库文件ai_chat_sdk库文件 link_directories(/usr/local/lib) # 链接SDK target_link_libraries(AIChatDemo PRIVATE ai_chat_sdk fmt jsoncpp OpenSSL::SSL OpenSSL::Crypto gflags spdlog sqlite3 )
运行编译后的程序,即可实现本章开头展示的对话效果。
第二章:API 协议调研与调试 (DeepSeek)
在开始编码实现SDK之前,必须深入理解目标模型(以DeepSeek为例)的API交互协议。我们使用Postman工具进行预研。
2.1 鉴权与环境配置
首先,将API Key配置在Postman的全局环境变量中,避免硬编码,确保安全性。
在HTTP请求头(Headers)中,设置Content-Type为application/json,并配置Authorization字段,值为Bearer {{api_key}}。
2.2 接口参数与请求体
DeepSeek的API兼容OpenAI规范。我们需要构建JSON格式的请求体(Body),包含模型名称、消息历史列表等。
请求的URL Endpoint 设置为https://api.deepseek.com。
2.3 响应模式测试
流式响应 (Streaming):
将stream参数设置为true。此时,服务器通过 Server-Sent Events (SSE) 协议分块返回数据。在Postman中可以看到数据是逐步到达的,每块数据包含一个 token。
非流式响应 (Blocking):
将stream参数设置为false。服务器会等待所有内容生成完毕后,一次性返回一个完整的JSON对象。
这一调研过程决定了SDK底层网络模块必须同时支持 HTTP 长连接读取(用于流式)和普通请求读取(用于全量)。
第三章:通用数据结构设计
基于API调研结果,我们抽象出一套通用的数据结构,用于在SDK内部各个模块(模型管理、会话管理、持久化)之间传递数据。这些定义位于include/common.h。
3.1 核心结构体定义
为了适应不同模型,我们提取了公共配置和描述信息:
#pragmaonce#include<string>#include<ctime>#include<vector>namespaceai_chat_sdk{// 消息结构:对应API中的message对象structMessage{std::string _id;std::string _role;// user, assistant, systemstd::string _content;// 消息内容std::time_t _timestamp;Message(conststd::string&role,conststd::string&content):_role(role),_content(content){}};// 基础配置:所有模型通用的参数structConfig{std::string _modelName;double_temperature=0.7;// 控制随机性int_maxTokens=2048;// 控制长度};// API配置:继承自Config,增加鉴权信息structAPIConfig:publicConfig{std::string _apiKey;};// 模型元数据:用于服务发现和状态检查structModelInfo{std::string _modelName;std::string _modelDesc;std::string _provider;std::string _endpoint;bool_isAvailable=false;ModelInfo(conststd::string&modelName="",conststd::string&modelDesc="",conststd::string&provider="",conststd::string&endpoint=""):_modelName(modelName),_modelDesc(modelDesc),_provider(provider),_endpoint(endpoint){}};// 会话结构:管理对话上下文structSession{std::string _sessionId;std::string _modelName;std::vector<Message>_messages;// 消息历史std::time_t _createAt;std::time_t _updateAt;Session(conststd::string&modelName=""):_modelName(modelName){}};}这些结构体构成了SDK的数据骨架,Session对象将由SessionManager在内存中维护,并由DataManager序列化到数据库中。
第四章:基础设施——spdlog日志库封装
在C++工程中,std::cout无法满足多线程安全、日志分级(Trace/Debug/Info/Error)、文件持久化及格式化输出的需求。因此,本项目基于spdlog进行了单例封装。
4.1 封装优势与设计理念
- 级别管理:在开发期开启DEBUG,生产期切换至INFO或ERROR,避免日志文件膨胀。
- 格式化:自动注入时间戳、线程ID、文件名及行号,便于定位问题。
- 异步高性能:利用独立线程处理磁盘I/O,避免阻塞主业务线程。
- 线程安全:通过内部锁机制保证多线程并发写入不冲突。
封装采用了单例模式(Singleton Pattern),确保全局只有一个日志管理器实例。
4.2 实现代码解析
头文件 (util/myLog.h):
定义了Logger类及便捷宏。宏定义利用__FILE__和__LINE__自动捕获调用位置。
#pragmaonce#include<spdlog/spdlog.h>namespacekk{classLogger{public:// 初始化日志:指定名称、文件路径、级别staticvoidinitLogger(conststd::string&loggerName,conststd::string&loggerFile,spdlog::level::level_enum logLevel=spdlog::level::info);staticstd::shared_ptr<spdlog::logger>getLogger();private:Logger();// 私有构造// 禁止拷贝Logger(constLogger&)=delete;Logger&operator=(constLogger&)=delete;private:staticstd::shared_ptr<spdlog::logger>_logger;staticstd::mutex _mutex;};// 宏定义示例:自动格式化文件名与行号#defineDBG(format,...)kk::Logger::getLogger()->debug(std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)//定义其他日志级别宏#defineTRACE(format,...)kk::Logger::getLogger()->log(spdlog::level::info,std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)#defineINF(format,...)kk::Logger::getLogger()->info(std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)#defineWRN(format,...)kk::Logger::getLogger()->warn(std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)#defineERR(format,...)kk::Logger::getLogger()->error(std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)#defineCRIT(format,...)kk::Logger::getLogger()->critical(std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)}实现文件 (util/myLog.cpp):
实现了双重检查锁定(Double-Checked Locking)以保证初始化的线程安全,并配置异步线程池。
#include"../../include/util/mylog.h"//将头文件包含进来#include<memory>#include<spdlog/spdlog.h>#include<spdlog/sinks/basic_file_sink.h>#include<spdlog/sinks/stdout_color_sinks.h>#include<spdlog/async.h>namespacekk{std::shared_ptr<spdlog::logger>Logger::_logger=nullptr;//初始化日志器为空指针std::mutex Logger::_mutex;//初始化日志器互斥锁//构造函数Logger::Logger(){}voidLogger::initLogger(conststd::string&loggerName,conststd::string&loggerFile,spdlog::level::level_enum logLevel){if(nullptr==_logger)//如果这个实例是空的,说明我们的日志器还没有被创建{//创建一把锁std::lock_guard<std::mutex>lock(_mutex);//再次检查是否为空,因为在多线程环境下,可能会有其他线程先创建了实例if(nullptr==_logger){//设置全局刷新策略,当日志级别大于等于logLevel时,刷新日志spdlog::flush_on(logLevel);//启用异步日志,即将日志信息存储放到队列中,有后台线程负责写入//参数1:队列大小,参数2:后台线程数spdlog::init_thread_pool(32768,1);//初始化线程池,32768个线程,1个队列if("stdout"==loggerFile)//如果日志文件路径为stdout,则将日志输出到标准输出流,就是控制台{//创建一个带颜色的输出到控制台的日志器_logger=spdlog::stdout_color_mt(loggerName);}else{//创建一个文件输出的日志器,日志会被写入到指定的文件中_logger=spdlog::basic_logger_mt<spdlog::async_logger>(loggerName,loggerFile);//创建一个异步日志器}}//格式设置//[%H:%M:%S] 时间//[%n] 日志器名称//[%l] 日志级别,左对齐,宽度为7//[%v] 日志消息_logger->set_pattern("[%H:%M:%S][%n] [%-7l] %v");//设置日志格式_logger->set_level(logLevel);//设置日志级别}}//获取日志器的方法std::shared_ptr<spdlog::logger>Logger::getLogger(){return_logger;}}//end kk第五章:核心抽象——Provider策略模式实现
为了应对不同模型提供商(DeepSeek, ChatGPT, Gemini)的具体实现差异,同时对上层业务保持接口统一,我们采用了策略模式(Strategy Pattern)。
5.1 抽象基类设计 (LLMProvider)
我们定义了一个纯虚基类LLMProvider,它规定了所有模型类必须实现的行为:初始化、可用性检查、获取元数据以及发送消息。由于它是抽象类,无法被直接实例化,只能作为接口规范。
头文件 (include/LLMprovider.h):
#include<string.h>#include<map>#include<vector>#include"common.h"namespacekk{classLLMProvider{public:// 纯虚函数:初始化模型配置virtualvoidinitModel(conststd::map<std::string,std::string>&modelConfig)=0;// 纯虚函数:检查模型是否可用virtualboolisAvailable()const=0;// 纯虚函数:获取模型元数据virtualstd::stringgetModelName()const=0;virtualstd::stringgetModelDesc()const=0;// 核心接口:全量发送消息voidsendMessage(conststd::vector<Message>&messages,conststd::map<std::string,std::string>&requestParam);// 核心接口:流式发送消息,通过回调函数(std::function)返回增量数据voidsendMessageStream(conststd::vector<Message>&messages,conststd::map<std::string,std::string>&requestParam,std::function<void(conststd::string&,bool)>callback);private:bool_isAvailable=false;std::string _apiKey;std::string _endpoint;};}5.2 派生类实现逻辑
具体的模型类(如DeepSeekProvider)将继承该基类。在initModel中,子类会解析配置参数:
- API Key:从配置中提取。
- Endpoint:默认使用官方地址,但允许通过配置覆盖,从而支持API中转站(Proxy)。
在sendMessageStream的实现中,子类将构造特定的HTTP请求,并利用curl或httplib的回调机制,每当收到一段SSE数据块时,就解析并调用上层传入的callback函数,从而实现流式效果。
通过这种设计,当需要添加新模型(例如Claude)时,只需新增一个继承自LLMProvider的类,而无需修改现有的会话管理或界面代码,完美符合开闭原则(Open-Closed Principle)。