前言

在完成了SDK的底层架构设计与抽象基类LLMProvider的定义后,接下来的核心任务是实现具体的模型接入逻辑。本文将以DeepSeek模型为例,详细阐述如何通过C++代码构建HTTP请求、处理API鉴权、解析JSON响应,并通过严格的单元测试验证模块功能的正确性。

如果文章中的代码存在问题,可以直接去仓库拉取最新代码
Ai_Model_SDK

https://gitee.com/caijiuuyk/ai_-model_-sdk

第一章:DeepSeek Provider类的派生与初始化

根据策略模式的设计,我们需要创建一个继承自LLMProvider的具体实现类DeepSeekProvider。该类负责管理DeepSeek模型的生命周期、配置加载以及消息发送逻辑。

1.1 头文件定义

include目录下创建DeepSeekProvider.h。该文件声明了DeepSeek提供者类,重写了基类中的纯虚函数。可以看到,除了基础的初始化和元数据获取接口外,重点在于sendMessage(全量返回)和sendMessageStream(流式返回)的声明。

SDK/include/DeepSeekProvider.h 代码如下:

#include"LLMProvider.h"
#include<string.h>
#include<map>
#include<vector>
#include"common.h"

namespace kk
{
    // DeepSeekProvider类,继承自抽象基类LLMProvider
    class DeepSeekProvider : public LLMProvider
    {
    public:
        // 初始化模型
        // modelConfig: 包含apiKey, endpoint等配置信息的映射
        virtual void initModel(const std::map<std::string,std::string>& modelConfig);

        // 检查模型是否可用
        virtual bool isAvailable() const;

        // 获取模型名称
        virtual std::string getModelName() const;

        // 获取模型描述
        virtual std::string getModelDesc() const;

        // 发送消息 ---全量返回   非流式响应
        // messages: 上下文消息列表
        // requestParam: 运行时参数(如temperature, maxTokens)
        std::string sendMessage(const std::vector<Message>& messages,const std::map<std::string,std::string>& requestParam);

        // 发送消息 ---全量返回   流式响应
        // callback: 回调函数,用于处理增量返回的Token
        virtual std::string sendMessageStream(const std::vector<Message>& messages,
                                const std::map<std::string,std::string>& requestParam,
                                std::function<void(const std::string&,bool)> callback);

    };
}

1.2 初始化逻辑实现

src目录下创建DeepSeekProvider.cpp。初始化的核心逻辑在于解析传入的配置参数(modelConfig),提取API Key和Endpoint(API接入点),并设置模型的可用状态。这里使用了我们之前封装的myLog.h进行日志记录,以便在初始化失败时快速定位问题。

SDK/src/DeepSeekProvider.cpp 初始化部分代码:

#include"../include/DeepSeekProvider.h"
#include"../include/util/myLog.h"

namespace kk
{
    // 初始化模型实现
    bool DeepSeekProvider::initModel(const std::map<std::string,std::string>& modelConfig)
    {
        // 1. 初始化API key
        auto it =modelConfig.find("apiKey");
        if(it==modelConfig.end())// 没找到apikey的话
        {
            ERR("DeepSeekProvider initModel:apiKey not found");// 打印错误日志
            return false;
        }
        _apiKey=it->second;// 找到了赋值

        // 2. 初始化endpoint
        it=modelConfig.find("endpoint");
        if(it==modelConfig.end())// 没找到endpoint的话
        {
            ERR("DeepSeekProvider initModel:endpoint not found");// 打印错误日志
            return false;
        }
        _endpoint=it->second;// 找到了赋值
        _isAvailable=true;// 标记模型可用

        // 初始化成功,打印关键信息(注意生产环境不要打印完整的Key)
        INFO("DeepSeekProvider initModel:success,apiKey:%s,endpoint:%s",_apiKey.c_str(),_endpoint.c_str());
        return true;
    }

    // 检测模型是否可用
    bool DeepSeekProvider::isAvailable() const
    {
        return _isAvailable;
    }

    // 获取模型名称
    std::string DeepSeekProvider::getModelName() const
    {
        return "deepseek-chat";
    }

    // 获取模型的描述信息
    std::string DeepSeekProvider::getModelDesc() const
    {
        return "deepseek-chat模型是一个基于Transformer架构的对话模型,由DeepSeek公司开发。它可以用于生成自然语言对话,支持多轮对话。";
    }
    
    // sendMessage 和 sendMessageStream 的具体实现将在下文详述
}

第二章:HTTP通信与JSON处理核心实现

为了实现sendMessage功能,我们需要引入HTTP客户端库来发送网络请求,并引入JSON库来处理数据交互。

2.1 依赖库安装:cpp-httplib

本项目选用cpp-httplib作为HTTP客户端,它是一个单头文件(header-only)的跨平台C++库,使用非常便捷。

安装方式一:Git克隆
可以直接从GitHub克隆源码,并将头文件复制到系统目录:

dev@dev-host:~/workspace$ git clone https://github.com/yhirose/cpp-httplib.git

# 注意:cpp-httplib是header-only库,只需要一个头文件即可
# 将httplib.h拷贝到系统目录下,这样在程序中#include <httplib.h>时能直接找到
bit@bit08:~/cpp-httplib/cpp-httplib$ sudo cp httplib.h /usr/include

安装方式二:压缩包解压
如果在服务器端Git网络受限,可以通过上传压缩包的方式进行安装。

unzip cpp-httplib-master.zip

解压后的文件结构如下,只需关注核心的httplib.h文件。

服务器上解压cpp-httplib压缩包后的文件列表截图

2.2 全量消息发送逻辑详解

sendMessage函数的实现是SDK的核心之一。其工作流程如下:

  1. 状态校验:确认模型已初始化且可用。
  2. 参数构造:解析用户传入的requestParam(如温度、MaxTokens),设置默认值。
  3. JSON序列化:使用jsoncpp库,将消息历史(vector<Message>)和配置参数组装成符合DeepSeek API规范的JSON对象,并序列化为字符串。
  4. HTTP请求构建:实例化httplib::Client,设置超时时间,配置Header(包含Bearer Token认证)。
  5. 发送与接收:发送POST请求,等待服务器的全量响应。
  6. 错误处理:检查HTTP状态码(期望为200)和网络错误。
  7. JSON反序列化:解析API返回的JSON数据,提取choices[0].message.content字段作为最终回复。

以下是DeepSeekProvider.cpp中关于sendMessage的完整实现代码:

#include"../include/DeepSeekProvider.h"
#include"../include/util/myLog.h"
#include<jsoncpp/json/json.h>
#include<httplib.h>

namespace kk
{
    // ... 前文的initModel等代码 ...

    // 发送消息 ---全量返回   非流式响应
    std::string DeepSeekProvider::sendMessage(const std::vector<Message>& messages,const std::map<std::string,std::string>& requestParam)
    {
        // 1、检测模型是否可用
        if(!isAvailable())
        {
            ERR("DeepSeekProvider sendMessage:model not available");
            return "";
        }

        // 2、构造请求参数
        double temperature=0.7;// 默认温度
        int maxTokens=2048;    // 默认最大token
        if(requestParam.find("temperature")!=requestParam.end())
        {
            temperature=std::stod(requestParam.at("temperature"));
        }
        if(requestParam.find("maxTokens")!=requestParam.end())
        {
            maxTokens=std::stoi(requestParam.at("maxTokens"));
        }

        // 3、构造历史消息 JSON数组
        Json::Value messageArray(Json::arrayValue);
        for(const auto& message:messages)
        {
            Json::Value messageJson(Json::objectValue);
            messageJson["role"]=message._role;
            messageJson["content"]=message._content;
            messageArray.append(messageJson);
        }

        // 4、构造整个请求体 JSON对象
        Json::Value requestBody(Json::objectValue);
        requestBody["model"]=getModelName(); // 设置模型名称
        requestBody["messages"]=messageArray; // 设置上下文
        requestBody["temperature"]=temperature;
        requestBody["max_tokens"]=maxTokens;

        // 5、序列化:将JSON对象转为字符串
        Json::StreamWriterBuilder writerBuilder;
        writerBuilder["indentation"]=""; // 设置缩进为空,压缩数据传输量
        std::string requestBodyStr=Json::writeString(writerBuilder,requestBody);
        INFO("DeepSeekProvider sendMessage:requestBody:%s",requestBodyStr.c_str());

        
        // 6、使用cpp-httplib库构造http客户端
        // _endpoint 需为 base url,例如 https://api.deepseek.com
        httplib::Client client(_endpoint.c_str());
        client.set_connection_timeout(30); // 连接超时30秒
        client.set_read_timeout(60,0);     // 读取超时60秒,防止大模型生成时间过长导致断连

        // 7、设置请求头
        httplib::Headers headers=
        {
            {"Authorization", "Bearer " + _apiKey}, // 鉴权的核心
            {"Content-Type", "application/json"}
        };

        // 8、发送POST请求
        auto response=client.Post("/v1/chat/completions",headers,requestBodyStr,"application/json");
        if(!response) // 网络层面的请求失败
        {
            ERR("DeepSeekProvider sendMessage:request failed,error message:{}",httplib::to_string(response.error()));
            return "";
        }
        INFO("DeepSeekProvider sendMessage:POST request success ,status code:{}",response->status);
        INFO("DeepSeekProvider sendMessage:response body:{}",response->body);
        
        // 9、检测HTTP响应状态
        if(response->status!=200)
            return "";

        // 10、解析响应体
        Json::Value responseBody;
        Json::CharReaderBuilder readerBuilder;
        std::string parseError;
        std::istringstream responseStream(response->body);
        
        // 解析JSON字符串
        if(Json::parseFromStream(readerBuilder,responseStream,&responseBody,&parseError))
        {
            // 导航至 message 内容:responseBody["choices"][0]["message"]["content"]
            if(responseBody.isMember("choices")&&responseBody["choices"].isArray()&&!responseBody["choices"].empty())
            {
                auto choice=responseBody["choices"][0];
                if(choice.isMember("message")&&choice["message"].isMember("content"))
                {
                    std::string replyContent=choice["message"]["content"].asString();
                    INFO("DeepSeekProvider sendMessage:replyContent:{}",replyContent);
                    return replyContent;
                }
            }

            // JSON结构不符合预期
            ERR("DeepSeekProvider sendMessage:JSON parse failed,error message:{}",parseError);
            return "json parse failed";
        }
        
        return "json parse failed";
    }

    // 占位实现,流式响应将在后续章节详细讲解
    std::string DeepSeekProvider::sendMessageStream(const std::vector<Message>& messages,
                                            const std::map<std::string,std::string>& requestParam,
                                            std::function<void(const std::string&,bool)> callback)
    {
        return "stream not implemented yet";
    }
}

第三章:基于Google Test的单元测试

代码编写完成后,必须进行严格的测试。本项目引入C++领域最权威的单元测试框架 Google Test (gtest)

3.1 GTest环境搭建

在Ubuntu环境下,可以通过apt快速安装GTest开发库:

sudo apt-get install libgtest-dev

3.2 编写测试用例

在项目根目录下创建一个test文件夹,并编写testLLM.cpp。测试用例的设计涵盖了从对象创建、初始化到发送消息的完整流程。为了安全起见,API Key通过环境变量获取,支持回退到硬编码的Key(仅用于本地调试)。

test/testLLM.cpp 代码:

#include<gtest/gtest.h>
#include"../SDK/include/DeepSeekProvider.h"
#include"../SDK/include/util/myLog.h"

// 测试DeepSeekProvider的核心流程
TEST(DeepSeekProviderTest,sendMessage)
{
    // 1. 创建Provider实例
    auto Provider=std::make_shared<kk::DeepSeekProvider>();
    ASSERT_TRUE(Provider!=nullptr); // 断言指针有效

    // 2. 准备初始化参数
    std::map<std::string,std::string> modelParam;
    const char* env_apikey = std::getenv("deepseek_apikey");
    // 优先读取环境变量,否则使用备用Key
    modelParam["apiKey"] = env_apikey ? env_apikey : "sk-xxxxxxxxxxxxxxxxxxxxxxxx"; 
    // 支持配置自定义Endpoint,例如中转站地址
    modelParam["endpoint"]="https://api.deepseek.com"; 

    // 3. 初始化并检查可用性
    Provider->initModel(modelParam);
    ASSERT_TRUE(Provider->isAvailable());

    // 4. 准备请求运行时参数
    std::map<std::string,std::string> requestParam=
    {
        {"temperature","0.5"},
        {"max_tokens","2048"}
    };

    // 5. 构造测试消息
    std::vector<kk::Message> messages;
    messages.push_back({"user","你是谁?"});

    // 6. 调用核心接口并验证
    std::string response=Provider->sendMessage(messages,requestParam);
    ASSERT_TRUE(!response.empty()); // 断言回复不为空
}

int main(int argc, char **argv)
{
    // 初始化日志库,开启DEBUG级别以便查看详细通信过程
    kk::Logger::initLogger("testLLM","stdout",spdlog::level::debug);
    INFO("Test start...");

    // 初始化GTest框架
    testing::InitGoogleTest(&argc, argv);
    
    // 运行所有注册的TEST宏
    return RUN_ALL_TESTS();
}

3.3 构建配置与编译

test目录下创建独立的CMakeLists.txt,用于构建测试可执行文件。需要特别注意的是,这里必须正确链接OpenSSL、JsonCpp、Fmt、Spdlog以及GTest库。

test/CMakeLists.txt 配置:

# 设置Cmake的最小版本为3.10
cmake_minimum_required(VERSION 3.10)

# 项目名称
project(testLLM)

# 设置C++标准为C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(CMAKE_BUILD_TYPE Debug)

# 添加可执行文件,包含测试代码和SDK源码
add_executable(testLLM testLLM.cpp
../SDK/src/DeepSeekProvider.cpp
../SDK/src/util/myLog.cpp)

# 设置输出目录
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_SOURCE_DIR})

# 添加头文件搜索路径
include_directories(${CMAKE_PROJECT_INCLUDE_DIR}/ ../SDK/include)

# 查找并链接OpenSSL
find_package(OpenSSL REQUIRED)
include_directories(${OPENSSL_INCLUDE_DIR})

# 宏定义:告诉cpp-httplib支持HTTPS
target_compile_definitions(testLLM PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)

# 链接所有依赖库
target_link_libraries(testLLM spdlog gtest jsoncpp fmt OpenSSL::Crypto OpenSSL::SSL)

目录结构确认
确保文件布局如下所示:
测试目录的文件结构截图,显示testLLM.cpp和CMakeLists.txt

3.4 编译与运行

进入test目录,创建build文件夹并进行编译:

mkdir build && cd build
cmake ..

CMake配置成功后,会生成Makefile:
CMake配置成功后的终端输出截图
build目录下生成的文件列表

执行make进行编译。在开发过程中,可能会遇到库链接顺序或头文件缺失导致的编译错误,需要根据报错信息仔细调整CMakeLists.txt
编译过程中的报错截图,提示开发过程中可能遇到的困难

修复问题后编译成功,运行生成的可执行文件./testLLM。可以看到控制台输出了请求的JSON Body、服务器返回的状态码以及解析后的回复内容。这里演示了通过自定义Endpoint连接API中转站的场景。

单元测试成功运行的截图,显示了模型回复的内容

至此,我们已经成功实现了DeepSeek模型的全量消息发送功能,并通过单元测试验证了从网络通信到JSON解析的完整链路。下一章将深入最具挑战性的部分——流式响应(Streaming Response) 的实现。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐