【C++ AI 大模型接入 SDK】 - DeepSeek 模型接入(上)
一、本篇概述
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" |
日志宏:INFO、ERR、WARN 等 |
<cstdint> |
size_t 等标准整数类型(流式处理函数参数用到) |
<jsoncpp/json/json.h> |
JsonCpp 核心:Json::Value、Json::StreamWriterBuilder |
<httplib.h> |
cpp-httplib:HTTP 客户端,发送请求到 DeepSeek API |
<jsoncpp/json/reader.h> |
JsonCpp 读取:Json::CharReaderBuilder、parseFromStream |
其中 <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() |
_isAvailable(bool) |
由 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.7,maxTokens = 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::string 的 operator+ 拼接,等价于 "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 响应)→
true,response有值 - 网络层面失败(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()){
三重检查:
isMember("choices")— JSON 中是否存在choices字段isArray()—choices是否是数组类型!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 层级深入到message→content.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 流式数据的接收与处理机制。
更多推荐
所有评论(0)