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)及以下系统库:

bash 复制代码
# gflags:用于解析命令行参数
sudo apt-get install libgflags-dev

# spdlog:高性能C++日志库,提供异步日志支持
sudo apt-get install libspdlog-dev

# fmt:格式化库,spdlog的核心依赖
sudo apt-get install fmt

# jsoncpp:用于处理模型API交互中的JSON数据序列化与反序列化
sudo apt-get install libjsoncpp-dev

# gtest:Google单元测试框架
sudo apt-get install libgtest-dev

# OpenSSL:支持HTTPS加密通信,保障API Key安全
sudo apt-get install libssl-dev

# cmake:跨平台构建系统
sudo apt-get install cmake

# pkg-config:编译时库文件路径查找工具
sudo apt install pkg-config

# curl:命令行网络工具,用于初步测试API连通性
sudo apt install curl
源码获取与目录结构

获取SDK源码主要有两种方式:通过Git克隆或直接上传压缩包。

bash 复制代码
git clone 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,执行以下命令:

    bash 复制代码
    mkdir build && cd build
  2. 生成Makefile

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

    bash 复制代码
    cmake ..


  3. 编译与依赖修复

    执行 make 命令开始编译。在初次编译时,系统抛出了错误,提示找不到 httplib.h

    这是因为项目内部使用了 cpp-httplib 库但环境未安装。需执行以下命令补全依赖:

    bash 复制代码
    sudo apt-get update && sudo apt-get install -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(流式发送)等标准接口。

安装库文件

编译完成后,执行安装命令将头文件和库文件部署到系统目录:

bash 复制代码
sudo make install

验证安装结果,头文件位于 /usr/local/include/ai_chat_sdk/,库文件位于 /usr/local/lib/

1.5 客户端集成演示 (ChatDemo)

为了验证SDK的可用性,编写一个名为 chatDemo.cpp 的客户端程序。该程序演示了如何调用DeepSeek模型进行流式对话。

cpp 复制代码
#include<ai_chat_sdk/chat_sdk.h>

#include<ai_chat_sdk/my_logger.h>//打印日志

#include<iostream>

#include<limits>

  

void sendMessageStream(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,[](const std::string&response,bool done){

        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;

}

  

int main()

{

  

    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 Key

    deepseek.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;

  

    int uerOP=1;//用户操作

    while(true)

    {

        std::cout<<"-------------------1、send message  0、exit------------------------"<<std::endl;

        std::cin>>uerOP;

        if(uerOP==0)

        {

            break;

        }

  

        getchar();//输入用户操作选择之后,需要接受到缓冲区中的回车

        sendMessageStream(chatSDK,session_id);//给会话发送消息

    }

    return 0;

}

配合相应的 CMakeLists.txt 进行编译:

cmake 复制代码
# 项目名称为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 核心结构体定义

为了适应不同模型,我们提取了公共配置和描述信息:

cpp 复制代码
#pragma once
#include <string>
#include <ctime>
#include <vector>

namespace ai_chat_sdk
{
    // 消息结构:对应API中的message对象
    struct Message
    {
        std::string _id;
        std::string _role;    // user, assistant, system
        std::string _content; // 消息内容
        std::time_t _timestamp;

        Message(const std::string& role, const std::string& content)
            : _role(role), _content(content) {}
    };

    // 基础配置:所有模型通用的参数
    struct Config
    {
        std::string _modelName;
        double _temperature=0.7; // 控制随机性
        int _maxTokens=2048;     // 控制长度
    };

    // API配置:继承自Config,增加鉴权信息
    struct APIConfig:public Config
    {
        std::string _apiKey;
    };

    // 模型元数据:用于服务发现和状态检查
    struct ModelInfo
    {
        std::string _modelName;
        std::string _modelDesc;
        std::string _provider;
        std::string _endpoint;
        bool _isAvailable=false;

        ModelInfo(const std::string& modelName="", const std::string& modelDesc="", const std::string& provider="", const std::string& endpoint="")
            : _modelName(modelName), _modelDesc(modelDesc), _provider(provider), _endpoint(endpoint) {}
    };

    // 会话结构:管理对话上下文
    struct Session
    {
        std::string _sessionId;
        std::string _modelName;
        std::vector<Message> _messages; // 消息历史
        std::time_t _createAt;
        std::time_t _updateAt;

        Session( const std::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__ 自动捕获调用位置。

cpp 复制代码
#pragma once
#include<spdlog/spdlog.h>

namespace kk
{
    class Logger
    {
    public:
        // 初始化日志:指定名称、文件路径、级别
        static void initLogger(const std::string &loggerName,const std::string &loggerFile,spdlog::level::level_enum logLevel=spdlog::level::info);
        static std::shared_ptr<spdlog::logger> getLogger();
    private:
        Logger(); // 私有构造
        // 禁止拷贝
        Logger(const Logger&) = delete;
        Logger& operator=(const Logger&) = delete;
    private:
        static std::shared_ptr<spdlog::logger> _logger;
        static std::mutex _mutex;
    };

// 宏定义示例:自动格式化文件名与行号
#define DBG(format, ...)kk::Logger::getLogger()->debug(std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__, ##__VA_ARGS__)

//定义其他日志级别宏

#define TRACE(format, ...)kk::Logger::getLogger()->log(spdlog::level::info,std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__, ##__VA_ARGS__)

#define INF(format, ...)kk::Logger::getLogger()->info(std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__, ##__VA_ARGS__)

#define WRN(format, ...)kk::Logger::getLogger()->warn(std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__, ##__VA_ARGS__)

#define ERR(format, ...)kk::Logger::getLogger()->error(std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__, ##__VA_ARGS__)

#define CRIT(format, ...)kk::Logger::getLogger()->critical(std::string("[{:>10s}:{<4d}]")+format,__FILE__,__LINE__, ##__VA_ARGS__)
}

实现文件 (util/myLog.cpp)

实现了双重检查锁定(Double-Checked Locking)以保证初始化的线程安全,并配置异步线程池。

cpp 复制代码
#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>

namespace kk

{

    std::shared_ptr<spdlog::logger> Logger::_logger=nullptr;//初始化日志器为空指针

    std::mutex Logger::_mutex;//初始化日志器互斥锁

  
  

    //构造函数  

    Logger::Logger()

    {}

  

    void Logger::initLogger(const std::string &loggerName,const std::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)

cpp 复制代码
#include<string.h>
#include<map>
#include<vector>
#include"common.h"

namespace kk
{
    class LLMProvider
    {
    public:
        // 纯虚函数:初始化模型配置
        virtual void initModel(const std::map<std::string,std::string>& modelConfig)=0;
        
        // 纯虚函数:检查模型是否可用
        virtual bool isAvailable() const=0;
        
        // 纯虚函数:获取模型元数据
        virtual std::string getModelName() const=0;
        virtual std::string getModelDesc() const=0;
        
        // 核心接口:全量发送消息
        void sendMessage(const std::vector<Message>& messages,const std::map<std::string,std::string>& requestParam);
        
        // 核心接口:流式发送消息,通过回调函数(std::function)返回增量数据
        void sendMessageStream(const std::vector<Message>& messages,
                        const std::map<std::string,std::string>& requestParam,
                        std::function<void(const std::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)。

相关推荐
漫随流水2 小时前
leetcode算法(513.找树左下角的值)
数据结构·算法·leetcode·二叉树
小屁猪qAq2 小时前
设计模式总纲
开发语言·c++·设计模式
Howrun7772 小时前
C++标准线程库-全面讲解
开发语言·c++
陈奕昆2 小时前
保姆级教程!零基础解锁大疆无人机开发:MSDK/PSDK/ 上云 API 实战指南[特殊字符]
无人机·sdk·大疆·企业级大疆二次开发
全栈游侠2 小时前
数据结构 -数组
数据结构
tod1132 小时前
从零手写一个面试级 C++ vector:内存模型、拷贝语义与扩容策略全解析
c++·面试·职场和发展·stl·vector
OopspoO2 小时前
C++杂记——构造函数
c++
淦。。。。3 小时前
题解:P14013 [POCamp 2023] 送钱 / The Generous Traveler
开发语言·c++·经验分享·学习·其他·娱乐·新浪微博
天赐学c语言3 小时前
1.18 - 滑动窗口最大值 && 子类的指针转换为父类的指针,指针的值是否会改变
数据结构·c++·算法·leecode