news 2026/6/4 0:23:00

C++大模型SDK开发实录(一):spdlog日志封装、通用数据结构定义与策略模式应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++大模型SDK开发实录(一):spdlog日志封装、通用数据结构定义与策略模式应用

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文件夹,以保持源码整洁。

  1. 初始化构建目录
    进入SDK源码路径ChatSDK/SDK-source/ai-model-acess-tech-master/AIModelAcessTech/sdk,执行以下命令:

    mkdirbuild&&cdbuild

  2. 生成Makefile
    build目录下执行 CMake 配置命令,读取上级目录的CMakeLists.txt

    cmake..


  3. 编译与依赖修复
    执行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-Typeapplication/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请求,并利用curlhttplib的回调机制,每当收到一段SSE数据块时,就解析并调用上层传入的callback函数,从而实现流式效果。

通过这种设计,当需要添加新模型(例如Claude)时,只需新增一个继承自LLMProvider的类,而无需修改现有的会话管理或界面代码,完美符合开闭原则(Open-Closed Principle)。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/21 19:25:29

计算机深度学习毕设实战-基于人工智能python-CNN深度学习的蝴蝶识别

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/5/30 19:32:18

深度学习毕设项目推荐-基于python-CNN深度学习的蝴蝶识别

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/6/1 9:03:12

大模型分布式训练通信优化:从Ring All-Reduce到分层压缩的实战演进

摘要&#xff1a;本文深度揭秘大模型分布式训练中的通信瓶颈与优化体系。通过Ring All-Reduce的拓扑感知改进、梯度压缩算法&#xff08;PowerSGDEF21&#xff09;的融合实现、以及通信-计算重叠的流水线设计&#xff0c;在千卡集群上训练175B模型时&#xff0c;通信耗时占比从…

作者头像 李华
网站建设 2026/5/31 3:12:23

一文搞懂 C++ 仿函数与适配器:从概念到实战代码

如果你在学 C STL&#xff0c;可能会对 “仿函数”“适配器” 这两个词感到陌生 —— 明明有函数指针&#xff0c;为啥要搞仿函数&#xff1f;栈和队列看着像独立容器&#xff0c;怎么又和 “适配器” 挂钩了&#xff1f;其实这两个概念的核心特别简单&#xff1a;仿函数是 “像…

作者头像 李华
网站建设 2026/5/31 16:48:26

2024年9月GESP真题及题解(C++七级): 矩阵移动

2024年9月GESP真题及题解(C七级): 矩阵移动 题目描述 小杨有一个 nmn \times mnm 的矩阵&#xff0c;仅包含 01? 三种字符。矩阵的行从上到下编号依次为 1,2,…,n1,2,\dots, n1,2,…,n&#xff0c;列从左到右编号依次为 1,2,…,m1, 2, \dots, m1,2,…,m。小杨开始在矩阵的左上…

作者头像 李华