一、本篇概述

DeepSeekProvider 是第一个实现 LLMProvider 纯虚接口的具体类,也是理解其他三个 Provider(豆包、通义千问、Ollama)的基础。本篇聚焦非流式部分(全量请求):

DeepSeekProvider
  ├── initModel()        ← 本篇:初始化 API Key、Endpoint
  ├── isAvailable()      ← 本篇:返回可用状态
  ├── getModelName()     ← 本篇:返回固定模型名
  ├── getModelDesc()     ← 本篇:返回模型描述
  ├── sendMessage()      ← 本篇:全量请求,一次性返回完整回复
  └── sendMessageStream()← 下篇:流式请求,逐段返回增量数据

二、头文件与依赖

2.1 DeepSeekProvider.h

#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    // LLMProvider 类
    class DeepSeekProvider : public LLMProvider{
    public:
        // 初始化模型
        virtual bool initModel(const std::map<std::string, std::string>& modelConfig);
        // 检测模型是否有效
        virtual bool isAvailable() const;
        // 获取模型名称
        virtual std::string getModelName() const;
        // 获取模型描述
        virtual std::string getModelDesc() const;
        // 发送消息 - 全量返回
        virtual std::string sendMessage(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam);
        // 发送消息 - 增量返回 - 流式响应
        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);
    };
} // end ai_chat_sdk

注意:所需要的头文件我们已经全部包含在了#include "LLMProvider.h"中,所以不需要重复写。

2.2 DeepSeekProvider.cpp 的依赖

#include "../include/DeepSeekProvider.h"
#include "../include/util/myLog.h"
#include <cstdint>
#include <jsoncpp/json/json.h>
#include <httplib.h>
#include <jsoncpp/json/reader.h>
头文件 引入原因
"DeepSeekProvider.h" 本类的声明
"util/myLog.h" 日志宏:INFOERRWARN
<cstdint> size_t 等标准整数类型(流式处理函数参数用到)
<jsoncpp/json/json.h> JsonCpp 核心:Json::ValueJson::StreamWriterBuilder
<httplib.h> cpp-httplib:HTTP 客户端,发送请求到 DeepSeek API
<jsoncpp/json/reader.h> JsonCpp 读取:Json::CharReaderBuilderparseFromStream

其中 <jsoncpp/json/json.h><jsoncpp/json/reader.h> 都是 JsonCpp 的头文件,前者提供 JSON 值的构建和序列化,后者提供 JSON 字符串的反序列化(解析)。


三、initModel — 模型初始化

3.1 完整源码

bool DeepSeekProvider::initModel(const std::map<std::string, std::string>& modelConfig){
    // 初始化API Key
    auto it = modelConfig.find("api_key");
    if(it == modelConfig.end()){
        ERR("DeepSeekProvider initModel api_key not found");
        return false;
    }else{
        _apiKey = it->second;
    }

    // 初始化Base URL
    it = modelConfig.find("endpoint");
    if(it == modelConfig.end()){
        _endpoint = "https://api.deepseek.com";
    }else{
        _endpoint = it->second;
    }

    _isAvailable = true;
    INFO("DeepSeekProvider initModel success, endpoint: {}",_endpoint);
    return true;
}

3.2 逐行解析

第 1 步:获取 API Key

auto it = modelConfig.find("api_key");
if(it == modelConfig.end()){
    ERR("DeepSeekProvider initModel api_key not found");
    return false;
}else{
    _apiKey = it->second;
}
  • modelConfig.find("api_key") — 在 map 中查找 key 为 "api_key" 的条目,返回一个迭代器
  • it == modelConfig.end() — 如果迭代器等于 end(),说明没找到
  • it->second — map 的每个元素是 pair<const string, string>first 是 key,second 是 value,所以 it->second 就是 API Key 的值
  • return false — API Key 是必填项,没有就直接返回初始化失败
  • _apiKey = it->second — 将 API Key 存入基类的 protected 成员,后续发送请求时要放在 HTTP Header 中

第 2 步:获取 Endpoint(有默认值)

it = modelConfig.find("endpoint");
if(it == modelConfig.end()){
    _endpoint = "https://api.deepseek.com";
}else{
    _endpoint = it->second;
}
  • 逻辑和获取 API Key 一样,区别在于:endpoint 不是必填的
  • 如果用户没有提供 endpoint,就使用 DeepSeek 官方默认地址 "https://api.deepseek.com"
  • 这种设计方便了开发者:正常使用时不需要关心 endpoint,只有在使用代理或私有部署时才需要修改

第 3 步:标记可用

_isAvailable = true;

初始化成功后,将基类的 _isAvailable 设为 true。之后调用 isAvailable() 就会返回 true,表示模型可以使用了。

3.3 modelConfig 从哪来?

LLMManager::initModel() 中,上层传入一个 map<string, string>

// 调用方构造配置
std::map<std::string, std::string> config;
config["api_key"] = "sk-xxxxxxxxxxxxxxxx";
config["endpoint"] = "https://api.deepseek.com";   // 可选

// 传入 LLMManager
llmManager.initModel("deepseek-chat", config);

LLMManager 内部找到对应的 DeepSeekProvider 实例,调用它的 initModel(config),配置就传到了这里(之后会实现讲解)。


四、isAvailable / getModelName / getModelDesc

这三个函数都很简单,直接返回固定值:

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

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

// 获取模型的描述信息
std::string DeepSeekProvider::getModelDesc() const{
    return "一款实用性强、中文优化的通用对话助手,适合日常问答与创作";
}
函数 返回值 说明
isAvailable() _isAvailablebool initModel() 设置,初始化前为 false
getModelName() "deepseek-chat" DeepSeek 的模型标识,发送 API 请求时放在 JSON 中
getModelDesc() 中文描述字符串 用于前端展示,让用户知道这个模型的特点

getModelName() 返回的 "deepseek-chat" 就是 DeepSeek API 要求的 model 字段值,在 sendMessage() 构造请求体时直接使用。


五、sendMessage — 全量发送消息

5.1 完整源码

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;
    if(requestParam.find("temperature") != requestParam.end()){
        temperature = std::stod(requestParam.at("temperature"));
    }
    if(requestParam.find("max_tokens") != requestParam.end()){
        maxTokens = std::stoi(requestParam.at("max_tokens"));
    }

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

    // 3. 构造请求体
    Json::Value requestBody;
    requestBody["model"] = getModelName();
    requestBody["messages"] = messageArray;
    requestBody["temperature"] = temperature;
    requestBody["max_tokens"] = maxTokens;

    // 4. 序列化
    Json::StreamWriterBuilder writerBuilder;
    writerBuilder["indentation"] = "";
    std::string requestBodyStr = Json::writeString(writerBuilder, requestBody);
    INFO("DeepSeekProvider sendMessage requestBody: {}", requestBodyStr);

    // 5. 使用cpp-httplib库构造HTTP客户端
    httplib::Client client(_endpoint.c_str());
    client.set_connection_timeout(30, 0);     // 连接超时时间为30秒
    client.set_read_timeout(60, 0);           // 设置超时时间为60秒

    // 设置请求头
    httplib::Headers headers = {
        {"Authorization", "Bearer " + _apiKey},
        {"Content-Type", "application/json"}
    };

    // 6. 发送POST请求
    auto response = client.Post("/chat/completions", headers, requestBodyStr, "application/json");
    if(!response){
        ERR("DeepSeekProvider sendMessage POST request failed");
        return "";
    }
    INFO("DeepSeekProvider sendMessage POST request success, status : {}", response->status);

    // 检测响应是否成功
    if(response->status != 200){
        return "";
    }

    // 7. 解析响应体
    Json::Value responseBody;
    Json::CharReaderBuilder readerBuilder;
    std::string parseError;
    std::istringstream responseStream(response->body);
    if(Json::parseFromStream(readerBuilder, responseStream, &responseBody, &parseError)){
        // 获取message数组
        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 response text: {}", replyContent);
                return replyContent;
            }
        }
    }

    // 8. json解析失败
    ERR("DeepSeekProvider sendMessage POST response body parse failed, error");
    return "deepseek response json parse failed";
}

5.2 逐步解析

第 1 步:安全检查
if(!isAvailable()){
    ERR("DeepSeekProvider sendMessage model not available");
    return "";
}

在发送请求前先检查模型是否初始化成功。如果 initModel() 没被调用过或者调用失败,_isAvailable 就是 false,这里会直接返回空字符串。这是一道防御性编程,防止在模型未就绪时发起无效的网络请求。

第 2 步:构造请求参数
double temperature = 0.7;
int maxTokens = 2048;
if(requestParam.find("temperature") != requestParam.end()){
    temperature = std::stod(requestParam.at("temperature"));
}
if(requestParam.find("max_tokens") != requestParam.end()){
    maxTokens = std::stoi(requestParam.at("max_tokens"));
}
  • 先设置默认值:temperature = 0.7maxTokens = 2048(如下我没有设置请求它们两个的参数则采取我们设置的默认值)

  • 如果 requestParam 中有对应的 key,就用用户提供的值覆盖默认值

  • std::stod()string to double,将字符串转为浮点数

  • std::stoi()string to int,将字符串转为整数

  • .at 就是根据key 获取相应的value

这里用 find() 而不是 [] 运算符,因为 [] 在 key 不存在时会自动插入一个默认值,而 find() 只是查找,不会修改 map。

第 3 步:构造消息数组
Json::Value messageArray(Json::arrayValue);
for(const auto& message : messages){
    Json::Value messageObject;
    messageObject["role"] = message._role;
    messageObject["content"] = message._content;
    messageArray.append(messageObject);
}

逐行解析:

  • Json::Value messageArray(Json::arrayValue) — 定义 Json::Value 类型的变量 messageArray并初始化成 JSON 数组类型
  • for(const auto& message : messages) — 范围 for 循环,const 引用避免拷贝
  • Json::Value messageObject — 创建一个 JSON 对象(默认是 objectValue 类型)
  • messageObject["role"] = message._role — 设置 JSON 字段,role 可能是 "user""assistant"
  • messageObject["content"] = message._content — 设置消息正文
  • messageArray.append(messageObject) — 将这个消息对象追加到数组末尾

构造出的 JSON 数组结构:

[
    {"role": "user", "content": "什么是智能指针?"},
    {"role": "assistant", "content": "智能指针是 C++11 引入的..."},
    {"role": "user", "content": "详细说说 unique_ptr"}
]

DeepSeek API 要求把完整的对话历史传过来,模型才能理解上下文。

第 4 步:构造请求体并序列化
Json::Value requestBody;
requestBody["model"] = getModelName();
requestBody["messages"] = messageArray;
requestBody["temperature"] = temperature;
requestBody["max_tokens"] = maxTokens;

把所有参数组装成一个完整的 JSON 对象:

{
    "model": "deepseek-chat",
    "messages": [...],
    "temperature": 0.7,
    "max_tokens": 2048
}
Json::StreamWriterBuilder writerBuilder;
writerBuilder["indentation"] = "";
std::string requestBodyStr = Json::writeString(writerBuilder, requestBody);
  • StreamWriterBuilder — JsonCpp 的 JSON 序列化工具
  • writerBuilder["indentation"] = "" — 设置缩进为空字符串,即紧凑格式(不换行不缩进),减少网络传输量
  • Json::writeString() — 将 Json::Value 根据writerBuilder的配置将requestBody序列化为 std::string

如果用有缩进的格式,输出会是这样的(方便调试但不适合传输):

{
    "model": "deepseek-chat",
    "messages": [
        {
            "role": "user",
            "content": "你好"
        }
    ],
    "temperature": 0.7,
    "max_tokens": 2048
}

紧凑格式(indentation = ""):

{"model":"deepseek-chat","messages":[{"role":"user","content":"你好"}],"temperature":0.7,"max_tokens":2048}
第 5 步:构造 HTTP 客户端
httplib::Client client(_endpoint.c_str()); //初始化的时候已经给_endpoint赋值
client.set_connection_timeout(30, 0);     // 连接超时时间为30秒
client.set_read_timeout(60, 0);           // 设置超时时间为60秒
  • httplib::Client — cpp-httplib 库的 HTTP 客户端类
  • _endpoint.c_str() — 将 std::string 转为 C 风格字符串(const char*),cpp-httplib 的构造函数需要这个类型
  • set_connection_timeout(30, 0) — 设置连接超时为 30 秒 0 微秒。如果 30 秒内无法建立 TCP 连接就放弃
  • set_read_timeout(60, 0) — 设置读取超时为 60 秒。如果 60 秒内没有收到响应就放弃

为什么全量请求只设 60 秒? 因为全量请求是等模型生成完毕后一次性返回,一般几秒到十几秒就能完成。后面的流式请求会设 300 秒,因为流式是边生成边传输,持续时间更长。

httplib::Headers headers = {
    {"Authorization", "Bearer " + _apiKey},
    {"Content-Type", "application/json"}
};
  • httplib::Headers — 本质是 std::multimap<std::string, std::string>,允许同名 header 出现多次
  • "Authorization", "Bearer " + _apiKey — API 认证头。Bearer 是 OAuth 2.0 标准的认证方式,后面跟 API Key。DeepSeek 用这个来验证调用者身份
  • "Content-Type", "application/json" — 告诉服务器请求体是 JSON 格式

"Bearer " + _apiKey 利用了 std::stringoperator+ 拼接,等价于 "Bearer sk-xxxxxxxx"

第 6 步:发送 POST 请求
auto response = client.Post("/chat/completions", headers, requestBodyStr, "application/json");

client.Post() 的四个参数:

参数 说明
第 1 个(路径) "/chat/completions" DeepSeek 的对话 API 端点
第 2 个(请求头) headers 包含认证和内容类型
第 3 个(请求体) requestBodyStr 序列化后的 JSON 字符串
第 4 个(内容类型) "application/json" 重复指定内容类型(httplib 要求)

/chat/completions 是 DeepSeek 兼容 OpenAI 格式的对话端点,chat/completions 表示"对话补全"。

if(!response){
    ERR("DeepSeekProvider sendMessage POST request failed");
    return "";
}

client.Post() 返回 httplib::Result,它重载了 operator bool()

  • 网络层面成功(收到 HTTP 响应)→ trueresponse 有值
  • 网络层面失败(DNS 解析失败、连接超时、网络断开)→ false
if(response->status != 200){
    return "";
}
  • response->status — HTTP 状态码,200 表示成功
  • 常见的非 200 状态码:
    • 401 — API Key 无效
    • 429 — 请求太频繁,被限流
    • 500 — DeepSeek 服务器内部错误
第 7 步:解析响应体
Json::Value responseBody;
Json::CharReaderBuilder readerBuilder;
std::string parseError;
std::istringstream responseStream(response->body);
if(Json::parseFromStream(readerBuilder, responseStream, &responseBody, &parseError)){...}
  • Json::CharReaderBuilder — JsonCpp 的 JSON 解析器构建器
  • std::istringstream responseStream(response->body) — 将响应体字符串包装成输入字符串流,因为 parseFromStream 需要流参数
  • Json::parseFromStream() — 从流中解析 JSON,成功返回 true,解析结果存在 responseBody 中,错误信息存在 parseError
  • if(Json::parseFromStream(readerBuilder, responseStream, &responseBody, &parseError)){...}

DeepSeek 返回的 JSON 结构:

{
    "id": "chatcmpl-xxx",
    "object": "chat.completion",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "智能指针是 C++11 引入的..."
            },
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 20,
        "completion_tokens": 100,
        "total_tokens": 120
    }
}

接下来逐层提取数据:

if(responseBody.isMember("choices") && responseBody["choices"].isArray() && !responseBody["choices"].empty()){

三重检查:

  1. isMember("choices") — JSON 中是否存在 choices 字段
  2. isArray()choices 是否是数组类型
  3. !empty() — 数组是否不为空
    auto choice = responseBody["choices"][0];
    if(choice.isMember("message") && choice["message"].isMember("content")){
        std::string replyContent = choice["message"]["content"].asString(); //将 Json::Value转为 std::string
  • responseBody["choices"][0] — 取第一个 choice(通常只有一个)
  • choice["message"]["content"] — 沿着 JSON 层级深入到 messagecontent
  • .asString() — 将 Json::Value 转为 std::string
第 8 步:解析失败兜底
ERR("DeepSeekProvider sendMessage POST response body parse failed, error");
return "deepseek response json parse failed";

如果走到这里,说明 JSON 解析失败或者 JSON 结构不符合预期。注意这里返回的不是空字符串,而是一个错误提示字符串。这是为了让上层知道"请求成功了但解析失败",区别于"请求本身失败"(返回空字符串)。

5.3 sendMessage 完整流程图

sendMessage(messages, requestParam)
  │
  ├─ 1. 检查 isAvailable()
  │     └─ false → 返回 ""
  │
  ├─ 2. 从 requestParam 取 temperature / max_tokens(构建请求参数)
  │
  │
  ├─ 3. 构建历史消息
  │
  │
  ├─ 4. 构造 JSON 请求体
  │     └─ {model, messages, temperature, max_tokens}
  │
  ├─ 5. 序列化为字符串
  │
  ├─ 6. 创建 HTTP Client + 设置超时 + 设置 Header
  │
  ├─ 7. 发送请求:POST → https://api.deepseek.com/chat/completions
  │     ├─ 网络失败 → 返回 ""
  │     └─ status != 200 → 返回 ""(检测响应是否成功)
  │
  ├─ 8. 解析 JSON 响应
  │     └─ choices[0].message.content → 返回回复文本(成功)
  │
  └─ 8. 解析失败 → 返回错误提示字符串

六、总结

本篇覆盖了 DeepSeekProvider 的非流式部分:

函数 作用
initModel() 从 map 中提取 API Key(必填)和 Endpoint(有默认值)
isAvailable() 返回 _isAvailable,由 initModel() 设置
getModelName() 返回固定值 "deepseek-chat"
getModelDesc() 返回中文描述
sendMessage() 构造 JSON → HTTP POST → 解析 JSON 响应 → 返回回复文本

sendMessage() 的核心流程是:构造请求 → 发送请求 → 解析响应,这与其他三个 Provider 的全量请求模式完全相同,区别只在于 API 端点和 JSON 字段的细节。

下一篇将深入 sendMessageStream(),解析 SSE 流式数据的接收与处理机制。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐