《C++ 项目:手写大模型接入 SDK》ChatGPT接口设计(中):全量响应实现和测试

🔥小叶-duck:个人主页

❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》

《Linux系统从入门到实践》《Linux网络从入门到实践》

《Qt 方寸极境》 《MySQL》

✨未择之路,不须回头
已
择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一、ChatGPTProvider.cpp:成员函数实现及解析

[1.1 断模型是否有效](#1.1 断模型是否有效)

[1.2 构造并获取请求参数](#1.2 构造并获取请求参数)

[1.2.1 设置参数默认值](#1.2.1 设置参数默认值)

[1.2.2 从 requestParams 读取用户传入参数](#1.2.2 从 requestParams 读取用户传入参数)

[1.2.3 组装历史消息 messages 数组](#1.2.3 组装历史消息 messages 数组)

[1.3 构造请求体](#1.3 构造请求体)

[1.4 对 requestBody 进行序列化](#1.4 对 requestBody 进行序列化)

[1.5 使用 cpp-httplib 构造 HTTP 客户端](#1.5 使用 cpp-httplib 构造 HTTP 客户端)

[1.5.1 设置超时时间](#1.5.1 设置超时时间)

[1.5.2 从环境变量中读取代理配置](#1.5.2 从环境变量中读取代理配置)

[1.5.3 构造请求头](#1.5.3 构造请求头)

[1.6 向指定服务器发送 POST 请求](#1.6 向指定服务器发送 POST 请求)

[1.7 解析响应体](#1.7 解析响应体)

[1.8 从 responseBody 中提取响应内容](#1.8 从 responseBody 中提取响应内容)

[二、GTest 全量响应代码测试(TestLLM.cpp)](#二、GTest 全量响应代码测试(TestLLM.cpp))

[2.1 CMakeLists.txt](#2.1 CMakeLists.txt)

[2.2 运行报错分析:400 unsupported_content_type 问题](#2.2 运行报错分析:400 unsupported_content_type 问题)

[2.2.1 报错现象](#2.2.1 报错现象)

[2.2.2 报错根本原因](#2.2.2 报错根本原因)

[2.2.3 疑问:同样写法,为什么 DeepSeek 调用没有报错?](#2.2.3 疑问:同样写法,为什么 DeepSeek 调用没有报错?)

[2.2.4 标准修复方案](#2.2.4 标准修复方案)

[三、全量接口 sendMessage 完整代码](#三、全量接口 sendMessage 完整代码)

结束语


前言

在上篇文章,我们梳理完 OpenAI 两套 API 的接口规范,理解了 Chat Completions 与新一代 Responses API 在报文结构、字段定义上的差异。本章我们将落地实现 ChatGPTProvider 的全量响应 sendMessage 接口,基于 LLMProvider 统一抽象基类,完成对接 Responses API 的完整业务代码。

我们会按代码执行流程逐段拆解每一处实现逻辑,包括模型可用性校验、请求参数解析、JSON 报文组装、httplib 客户端与代理配置、网络请求发送、返回 JSON 解析等环节。随后借助 GTest 编写测试用例,使用 CMake 进行编译构建,在联调过程中,我们会重点分析本次遇到的经典踩坑问题:400 unsupported_content_type,搞清楚为什么相同的写法在 DeepSeek 接口可以正常调用,在 OpenAI 网关却直接报错,理解不同厂商网关的校验宽容度差异,给出一套能够兼容多家大模型服务商的规范写法。

一、ChatGPTProvider.cpp:成员函数实现及解析

本节我们实现 ChatGPTProvider 的全量响应 sendMessage 接口,该接口遵循我们项目统一的 LLMProvider 基类契约,对接 OpenAI 新一代 Responses API。整体逻辑按照:**"可用性校验 → 请求参数解析 → JSON 请求体组装 → HTTP 客户端配置与代理读取 → 发起 POST 请求 → 响应 JSON 解析 → 提取返回文本"**的顺序实现。

1.1 断模型是否有效

在发起任何网络请求之前,先调用 IsAvailable() 做前置校验,判断当前 Provider 是否已经完成初始化、密钥与基础地址是否配置正常。

  • 如果模型不可用,直接打错误日志,返回 std::nullopt 向上层告知调用失败;
  • 提前拦截无效调用,避免后续执行无效的网络请求,节约资源。
cpp 复制代码
if (!IsAvailable())
{
    ERR("ChatGPTProvider sendMessage model not available");
    return std::nullopt;
}

1.2 构造并获取请求参数

参数分为两类:有默认值的模型控制参数,以及必须由调用方传入 的模型名称 model。

设计约定: api-key、baseUrl 属于所有模型通用配置,放在基类;model、temperature、max_output_tokens 属于单次请求参数,从入参 requestParams 中读取。

1.2.1 设置参数默认值

如果调用方没有传入,我们给温度、最大输出 token 数设置默认值:

cpp 复制代码
double temperature = 0.7;
int max_output_tokens = 2048;
std::string model;

1.2.2 从 requestParams 读取用户传入参数

遍历查询参数字典,用户传入则覆盖默认值; model 没有默认值,强制要求调用方传入,缺失直接报错返回。

cpp 复制代码
auto it = requestParams.find("temperature");
if (it != requestParams.end())
{
    temperature = std::stod(it->second);
}

it = requestParams.find("max_output_tokens");
if (it != requestParams.end())
{
    max_output_tokens = std::stoi(it->second);
}

it = requestParams.find("model");
if (it != requestParams.end())
{
    model = it->second;
}
else
{
    ERR("model name not found in requestParams, must config it");
    return std::nullopt;
}

1.2.3 组装历史消息 messages 数组

遍历传入的对话消息列表 messages,按照 OpenAI Responses API 规范组装 JSON 数组,字段固定为 role、content,和接口文档对齐。

cpp 复制代码
Json::Value messageArray(type::Json::arrayValue);
for (auto &message : messages)
{
    Json::Value messageObject;
    messageObject["role"] = message._role;
    messageObject["content"] = message._content;
    messageArray.append(messageObject);
}

1.3 构造请求体

按照 Responses API 的报文规范拼装顶层字段:model、temperature、max_output_tokens、input(input 对应我们组装好的消息数组)。

注意区分: 老版 Chat Completions 使用 messages 字段,新一代 Responses API 使用 input,这是两套 API 很关键的区别。

cpp 复制代码
Json::Value requestBody;
requestBody["model"] = model;
requestBody["temperature"] = temperature;
requestBody["max_output_tokens"] = max_output_tokens;
requestBody["input"] = messageArray;

1.4 对 requestBody 进行序列化

使用 JsonCpp 将 Json::Value 对象序列化为字符串,作为 HTTP POST 的 body 载荷,同时打印日志方便调试,查看实际发送给 OpenAI 的请求结构。

cpp 复制代码
Json::StreamWriterBuilder writerbuilder;
std::string requestBodyStr = Json::writeString(writerbuilder, requestBody);
INFO("ChatGPTProvider sendMessage requestbody : {}", requestBodyStr);

1.5 使用 cpp-httplib 构造 HTTP 客户端

1.5.1 设置超时时间

设置读超时、连接超时,防止程序卡在网络阻塞,是生产网络请求必备的保护机制。

cpp 复制代码
httplib::Client client(_baseUrl);
client.set_read_timeout(60, 0);
client.set_connection_timeout(20, 0);

1.5.2 从环境变量中读取代理配置

OpenAI 接口在国内直连无法访问,这里设计为从环境变量 MY_APP_PROXY 读取代理地址,格式强制要求 host:port。

  • 读取环境变量,判断是否存在;
  • 存在则拆分主机与端口,给 httplib 客户端设置代理;
  • 环境变量不存在,直接报错返回,提示用户必须配置代理。
cpp 复制代码
char *proxy_env = std::getenv("MY_APP_PROXY");
if (proxy_env != nullptr)
{
    std::string proxy_str(proxy_env);
    auto pos = proxy_str.find(":");
    if (pos == std::string::npos)
    {
        ERR("ChatGPTProvider sendMessage proxy_env format error, must be host:port");
        return std::nullopt;
    }
    std::string proxy_host = proxy_str.substr(0, pos);
    int proxy_port = std::stoi(proxy_str.substr(pos + 1));
    client.set_proxy(proxy_host, proxy_port);
}
else
{
    ERR("ChatGPTProvider sendMessage proxy_env not found, must config it: MY_APP_PROXY=host:port");
    return std::nullopt;
}

1.5.3 构造请求头

按照 OpenAI 认证规范,使用 Bearer Token 格式传递 api-key。

cpp 复制代码
httplib::Headers headers = {
    {"Authorization", "Bearer " + _apiKey},
    {"Content-Type", "application/json"} //错误写法,后面会解释
};

1.6 向指定服务器发送 POST 请求

调用 httplib 的 Post 方法,访问 Responses 接口路径 /v1/responses,带上头、请求体;

两层判断:

第一层判断网络层面是否请求成功(连接失败、超时等网络错误);

第二层判断 HTTP 状态码是否等于 200。非 200 状态码代表业务侧报错(配额耗尽、密钥错误、模型不存在等),打印状态码与返回的错误报文,方便定位问题。

cpp 复制代码
auto resp = client.Post("/v1/responses", headers, requestBodyStr, "application/json");
if (!resp)
{
    ERR("ChatGPTProvider sendMessage failed, error: {}", httplib::to_string(resp.error()));
    return std::nullopt;
}
if (resp->status != 200)
{
    ERR("ChatGPTProvider sendMessage Post request failed, status: {}, body: {}", resp->status, resp->body);
    return std::nullopt;
}
INFO("ChatGPTProvider sendMessage success, status: {}, body: {}", resp->status, resp->body);

1.7 解析响应体

利用 JsonCpp 的 CharReader 流式解析返回的 JSON 字符串,如果报文格式非法、JSON 解析失败,打日志并返回空。

cpp 复制代码
Json::CharReaderBuilder readerbuilder;
Json::Value responseBody;
std::istringstream respBodyStream(resp->body);
std::string parse_err_msg;
if (!Json::parseFromStream(readerbuilder, &respBodyStream, &responseBody, &parse_err_msg))
{
    ERR("ChatGPTProvider sendMessage parse responseBody failed, err_msg: {}", parse_err_msg);
    return std::nullopt;
}

1.8 从 responseBody 中提取响应内容

按照 Responses API 返回的 JSON 结构逐层读取:

  1. 先判断顶层是否包含 output 数组;
  2. 取出 output[0] 对象,校验包含 content 数组;
  3. 取出 content[0] 对象,读取 text 字段,这个就是大模型返回的回答文本;
  4. 任意一层字段缺失,都打印对应的错误日志,返回 std::nullopt。
cpp 复制代码
if (responseBody.isMember("output") && responseBody["output"].isArray() && !responseBody["output"].empty())
{
    Json::Value outputObject = responseBody["output"][0];
    if (outputObject.isMember("content") && outputObject["content"].isArray() && !outputObject["content"].empty())
    {
        Json::Value content = outputObject["content"][0];
        if (content.isMember("text"))
        {
            std::string responseText = content["text"].asString();
            INFO("ChatGPTProvider sendMessage responseText: {}", responseText);
            return responseText;
        }
        else
        {
            ERR("ChatGPTProvider sendMessage responseBody content not found text field");
            return std::nullopt;
        }
    }
    else
    {
        ERR("ChatGPTProvider sendMessage responseBody output no content field");
        return std::nullopt;
    }
}
else
{
    ERR("ChatGPTProvider sendMessage responseBody no output field");
    return std::nullopt;
}

二、GTest 全量响应代码测试(TestLLM.cpp)

前置: GTest 第三方库在前面的文章中我们已经详细进行了讲解,感兴趣了解的可以看我之前的文章,这里是基于 TEST 宏写的单元测试,自动输出测试成功 / 失败信息。《C++ 工具库实战》GTest 单元测试:断言、全局环境、局部夹具与底层原理_gtest框架原理-CSDN博客

cpp 复制代码
#include "../SDK/include/ChatGPTProvider.h"
#include "../SDK/include/DeepSeekProvider.h"
#include "../SDK/include/GeminiProvider.h"
#include <gtest/gtest.h>
#include <spdlog/common.h>

using namespace ai_chat_sdk;

TEST(ChatGPTProviderTest, sendMessage)
{
    // 1、创建ChatGPTProvider对象
    std::shared_ptr<LLMProvider> chatgptProvider = std::make_shared<ChatGPTProvider>();
    // 1.1 检查ChatGPTProvider是否初始化成功
    ASSERT_TRUE(chatgptProvider != nullptr);

    // 2、构建模型配置
    std::unordered_map<std::string, std::string> modelConfig;
    modelConfig["api_key"] = getenv("chatgpt_apikey") ? getenv("chatgpt_apikey") : "";
    modelConfig["base_url"] = "https://api.openai.com";

    // 3、初始化ChatGPTProvider
    chatgptProvider->initModel(modelConfig);
    // 3.1 检查模型是否有效
    ASSERT_TRUE(chatgptProvider->IsAvailable());

    // 4、构建请求参数配置(不写则使用默认值)
    std::unordered_map<std::string, std::string> requestConfig;
    requestConfig["model"] = "gpt-4o-mini";
    requestConfig["max_output_tokens"] = "2048";
    requestConfig["temperature"] = "0.7";

    // 5、构建消息列表
    std::vector<Message> messages;
    messages.push_back({"user", "你好, 请简单介绍一下你自己"});

    // 6、调用sendMessage发送消息
    auto response = chatgptProvider->sendMessage(messages, requestConfig);
    // 6.1 检查响应是否为空
    ASSERT_TRUE(response.has_value());
    INFO("response: {}", response.value());
}

int main(int argc, char **argv)
{
    // 初始化Google Test框架
    testing::InitGoogleTest(&argc, argv);

    // 初始化日志系统
    Logger::logger_init("testLLM", "stdout", spdlog::level::info);

    // 运行所有测试用例
    return RUN_ALL_TESTS();
}

测试代码核心设计(和我们之前 SDK 设计保持一致)

  1. modelConfig(初始化配置) :api-key、base-url,属于这个 Provider 实例固定不变的属性,initModel阶段一次性加载;
  2. requestConfig(请求参数):model 名称、temperature、max_output_tokens 等,每次调用 sendMessage 都可以独立传参,同一个 Provider 实例可以用不同参数发请求;
  3. messages 数组 :维护会话上下文,体现大模型无状态特性,我们自己在 SDK 侧拼接历史消息,传给接口,模型依靠 messages 数组理解多轮对话;
  4. 返回值是std::optional<std::string>:optional 区分「正常拿到回答」和「网络失败 / 报文解析失败 / 接口报错」两种场景,has_value()用来判断是否成功。

问题:为什么不直接硬编码 API Key?

API 密钥属于敏感凭证,如果直接写死在代码里,代码上传仓库会造成密钥泄露 。工程上常用环境变量的方式注入密钥。 操作步骤:

cpp 复制代码
vim ~/.bashrc
# 在文件末尾添加
export chatgpt_apikey="你的ChatGPT密钥"
# 生效配置
source ~/.bashrc
  • 执行source ~/.bashrc之后,当前终端以及后续新开终端都能读取到这个环境变量;
  • getenv("chatgpt_apikey")就是 C 标准库函数,读取系统环境变量的值;
  • 优势: 代码里不存放密钥,不同模型的 key 可以分开配置,切换模型时只改环境变量即可。

2.1 CMakeLists.txt

bash 复制代码
# 设置Cmake最低版本要求
cmake_minimum_required(VERSION 3.10)

# 项目名称
project(testLLM)

# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 要求必须使用C++17标准

# 设置构建类型为Debug
set(CMAKE_BUILD_TYPE Debug)

# 添加可执行文件
add_executable(testLLM TestLLM.cpp 
               ../SDK/src/DeepSeekProvider.cpp 
               ../SDK/src/util/MyLog.cpp 
               ../SDK/src/ChatGPTProvider.cpp)   

#设置生成可执行文件的输出目录
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
# ${CMAKE_BINARY_DIR} 表示生成可执行文件的所在目录,即cmake指令执行所在目录

# 添加头文件搜索路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../SDK/include)
# ${CMAKE_CURRENT_SOURCE_DIR} 表示当前CMakeLists.txt所在目录
# ${CMAKE_SOURCE_DIR} 表示项目根目录,即所有CMakeLists.txt所在路径中最外层目录

# 查找第三方库:确保在使用前已经通过find_package命令找到并配置了这些库
find_package(GTest REQUIRED)
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
find_package(jsoncpp REQUIRED)
find_package(OpenSSL REQUIRED)

# 链接第三方库
target_link_libraries(testLLM gtest fmt spdlog jsoncpp pthread OpenSSL::SSL OpenSSL::Crypto)

在 CMakeLists.txt 文件当中,只需要将 ChatGPTProvider 源文件的所在位置添加到add_executable当中即可。

2.2 运行报错分析:400 unsupported_content_type 问题

2.2.1 报错现象

我们执行 ChatGPTProvider 的全量响应测试用例时,接口返回 400 Bad Request,错误提示:

unsupported_content_type: "application/json, application/json". This API method only accepts 'application/json' requests, but you specified the header Content-Type: application/json, application/json

GTest 测试结果标记测试用例 Failure,has_value() 返回 false,调用失败。

核心问题:HTTP 请求里的 Content-Type 重复拼接,出现了双份 application/json。

2.2.2 报错根本原因

HTTP 规范 中,Content-Type 是单值请求头 ,一个 Header 只能携带一个 MIME 类型。 在我们原来的代码写法里出现两处同时设置 Content-Type:

  1. 在 httplib::Headers 手动加入了一条 {"Content-Type", "application/json"}
  2. 在调用 client.Post() 的最后一个入参,又传入了 "application/json"

cpp-httplib 底层不会自动去重,会把两处的值用逗号拼接在一起,最终发给 OpenAI 网关的请求头变成:

bash 复制代码
Content-Type: application/json, application/json

OpenAI 的网关校验严格,直接判定这是不符合 RFC 规范的非法请求,直接拒绝并返回 400。

2.2.3 疑问:同样写法,为什么 DeepSeek 调用没有报错?

这里不是代码逻辑有分支,而是不同厂商 API 网关的宽容度(容错设计)不一样,分为三点原因:

  1. 底层 Web 框架容错设计不同:
    DeepSeek 这类国内大模型 API,后端大多基于 FastAPI、Gin、Flask 这类通用 Web 框架。这类框架在解析请求头时,只要逗号分隔的列表里面包含 application/json,就会正常提取 body 的 JSON,忽略多余重复部分,属于 "宽松兼容"。
    而 OpenAI 接入网关是自研高性能网关,严格遵循 RFC 标准,只要 Content-Type 是逗号分隔的复合类型,直接判定请求非法,拒绝解析报文。
  2. **Web 框架标准差异(Starlette 陷阱):**OpenAI 网关使用高标准校验逻辑,对同一个 Header 出现多个值的场景直接拦截;国内很多中转、API 服务为兼容老旧第三方 SDK,对这类不规范 Header 做了兼容处理。
  3. **安全防刷策略的区别(快速失败 Fail-fast):**OpenAI 面向全球公网,流量巨大,网关采用快速失败策略。只要请求格式不符合规范,直接拒绝,不继续解析请求体,减少服务器算力消耗。国内 API 服务压力模型不同,优先选择兼容不规范请求。

2.2.4 标准修复方案

**只保留一处设置 Content-Type,推荐删掉 Headers 里的 Content-Type,仅在 Post 接口参数中传递。**修改前后对比:

cpp 复制代码
// 错误写法:两处同时配置
httplib::Headers headers = {
    {"Authorization", "Bearer " + _apiKey},
    {"Content-Type", "application/json"}  // 这里多余
};
auto resp = client.Post(path, headers, requestBodyStr, "application/json");

// 正确写法:只在Post参数指定Content-Type
httplib::Headers headers = {
    {"Authorization", "Bearer " + _apiKey}
};
auto resp = client.Post(path, headers, requestBodyStr, "application/json");

修改后,发出的请求头只会有一份合法的 Content-Type: application/json,既能通过 OpenAI 严格的网关校验,同时 DeepSeek 等宽松网关也可以正常识别,一套代码兼容所有模型服务商。

三、全量接口 sendMessage 完整代码

cpp 复制代码
// 发送消息:全量调用,返回完整响应
std::optional<std::string> ChatGPTProvider::sendMessage(const std::vector<Message> &messages,
                                                        const std::unordered_map<std::string, std::string> &requestParams)
{
    // 1、判断模型是否有效
    if (!IsAvailable())
    {
        ERR("ChatGPTProvider sendMessage model not available");
        // 模型无效,返回空值nullopt
        return std::nullopt;
    }

    // 2、构造并获取请求参数
    // 2.1 构造默认请求参数:如果requestParams中没有指定temperature、max_output_tokens,则使用默认值
    double temperature = 0.7;
    int max_output_tokens = 2048;
    std::string model; // 请求参数model无法默认值,必须用户手动配置
    // 2.2 从requestParams中获取temperature、max_output_tokens参数:如果requestParams中指定了,则使用指定值
    // 注意:请求参数model在不同的模型中是必填项,但是这个参数由于模型版本的更新,可能会有不同的字段名称,所以就不能使用默认值了,需要用户自行指定正确的model
    auto it = requestParams.find("temperature");
    if (it != requestParams.end())
    {
        temperature = std::stod(it->second);
    }
    it = requestParams.find("max_output_tokens");
    if (it != requestParams.end())
    {
        max_output_tokens = std::stoi(it->second);
    }
    // 请求参数model必须用户手动配置,如果没有配置则直接返回空值nullopt报错告诉用户
    it = requestParams.find("model");
    if (it != requestParams.end())
    {
        model = it->second;
    }
    else
    {
        ERR("model name not found in requestParams, must config it");
        return std::nullopt;
    }

    // 2.3 构造历史消息:获取请求体中的 messages 这个Json数组
    Json::Value messageArray(Json::arrayValue);
    for (auto &message : messages)
    {
        Json::Value messageObject;
        // 这里的字段名称必须与ChatGPT模型的接口文档中要求的一致
        messageObject["role"] = message._role;
        messageObject["content"] = message._content;
        messageArray.append(messageObject);
    }

    // 3、构造请求体
    //  和历史消息构造一样,请求体的字段名称必须与ChatGPT模型的接口文档中要求的一致
    Json::Value requestBody;
    requestBody["model"] = model;
    requestBody["temperature"] = temperature;
    requestBody["max_output_tokens"] = max_output_tokens;
    requestBody["input"] = messageArray;

    // 4、对requestBody进行序列化
    Json::StreamWriterBuilder writerbuilder;
    std::string requestBodyStr = Json::writeString(writerbuilder, requestBody);
    INFO("ChatGPTProvider sendMessage requestBody: {}", requestBodyStr);

    // 5、使用cpp-httplib构建HTTP客户端
    httplib::Client client(_baseUrl);
    // 5.1 设置超时时间
    client.set_read_timeout(60, 0);
    client.set_connection_timeout(20, 0);

    // 5.2 关键点:从环境变量中获取代理配置
    // 说明:每个用户的代理配置不同可能各不相同,所以这里就需要用户自己通过设定环境变量的方式来配置代理
    char *proxy_env = std::getenv("MY_APP_PROXY");
    if (proxy_env != nullptr)
    {
        std::string proxy_str(proxy_env);
        auto pos = proxy_str.find(":");
        if (pos == std::string::npos)
        {
            ERR("ChatGPTProvider sendMessage proxy_env format error, must be host:port");
            return std::nullopt;
        }
        else
        {
            std::string proxy_host = proxy_str.substr(0, pos);
            int proxy_port = std::stoi(proxy_str.substr(pos + 1));
            client.set_proxy(proxy_host, proxy_port);
        }
    }
    else
    {
        // 针对ChatGPT模型的特殊情况:如果用户没有配置代理环境变量,那么请求无法访问到ChatGPT模型的服务器
        ERR("ChatGPTProvider sendMessage proxy_env not found, must config it: MY_APP_PROXY=host:port");
        return std::nullopt;
    }

    // 5.3 构建请求头
    httplib::Headers headers = {
        {"Authorization", "Bearer " + _apikey},
        // {"Content-Type", "application/json"}
    };

    // 6、向指定服务器发送POST请求
    auto resp = client.Post("/v1/responses", headers, requestBodyStr, "application/json");
    if (!resp)
    {
        ERR("ChatGPTProvider sendMessage failed, error: {}", httplib::to_string(resp.error()));
        return std::nullopt;
    }

    if (resp->status != 200)
    {
        ERR("ChatGPTProvider sendMessage Post request failed, status: {}, body: {}", resp->status, resp->body);
        return std::nullopt;
    }
    INFO("ChatGPTProvider sendMessage success, status: {}, body: {}", resp->status, resp->body);

    // 7、解析响应体
    Json::CharReaderBuilder readerbuilder;
    Json::Value responseBody;
    std::istringstream respBodyStream(resp->body);
    std::string parse_err_msg;
    if (!Json::parseFromStream(readerbuilder, respBodyStream, &responseBody, &parse_err_msg))
    {
        ERR("ChatGPTProvider sendMessage parse responseBody failed, err_msg: {}", parse_err_msg);
        return std::nullopt;
    }

    // 8、从responceBody中提取响应内容
    // 8.1 检查responseBody是否包含output字段
    if (responseBody.isMember("output") && responseBody["output"].isArray() && !responseBody["output"].empty())
    {
        // 8.2 检查output[0]是否包含content字段
        Json::Value outputObject = responseBody["output"][0];
        if (outputObject.isMember("content") && outputObject["content"].isArray() && !outputObject["content"].empty())
        {
            Json::Value content = outputObject["content"][0];
            // 8.3 检查content[0]是否包含text字段
            if (content.isMember("text"))
            {
                std::string responseText = content["text"].asString();
                INFO("ChatGPTProvider sendMessage responseText: {}", responseText);
                return responseText;
            }
            else
            {
                ERR("ChatGPTProvider sendMessage responseBody content not found text field");
                return std::nullopt;
            }
        }
        else
        {
            ERR("ChatGPTProvider sendMessage responseBody output no content field");
            return std::nullopt;
        }
    }
    else
    {
        ERR("ChatGPTProvider sendMessage responseBody no output field");
        return std::nullopt;
    }
}

结束语

到这里,ChatGPTProvider 全量响应 sendMessage 接口的代码实现、测试联调与问题修复就全部完成了。我们顺着代码执行链路逐段剖析了对接 OpenAI Responses API 的全部逻辑,同时在测试环节踩中了 cpp-httplib 请求头重复设置 Content-Type 的典型坑点,理清了不同厂商网关校验策略存在差异的底层原因,也得到了一套通用、规范的 HTTP 请求写法。

对比之前实现的 DeepSeekProvider,能够直观感受到这套 LLMProvider 抽象层的价值:上层调用接口保持一致,不同模型厂商、不同版本 API 之间的报文差异、协议细节全部收敛封装在子类内部。

本章我们完成的是非流式全量响应接口,后续我们会继续实现流式 SSE 版本的 sendMessageStream 接口,实现逐字返回的对话效果,补齐 ChatGPTProvider 的剩余能力。

相关推荐
東隅已逝,桑榆非晚2 小时前
算法3.双指针法-快乐数
c++·笔记·学习·算法
eggwyw2 小时前
C++相交链表和反转链表详解
开发语言·c++
杜 硕2 小时前
从零开始的C++生活——第六讲——02
c++·算法·生活
无言听风雨丶3 小时前
systemd 与信号 / 僵尸进程:Linux 进程管理的底层逻辑与排错
linux·服务器·网络
小超人诞生日记3 小时前
C++ STL 反向迭代器:从源码框架到适配器实现
c++·算法
青少儿编程课堂3 小时前
蓝桥杯青少组图形化 + 全国青少年信息素养大赛 + 电子学会Scratch考级:校园成语接龙大挑战解析
c++·python·算法·bfs·信息学竞赛
小贾要学习3 小时前
【C++后端开发项目】Reactor 模型 / C++ 仿写 muduo HTTP 服务器:让底层网络库真正跑起网页
linux·服务器·c++·http·网络编程·muduo·reactor模型
SDWAN_Cheap3 小时前
BGP是什么,广播IP是什么?两个容易混淆的概念拆解
网络·tcp/ip·bgp·广播ip
程序员JerrySUN3 小时前
PCIe Gen5 SSD 为什么读得快、写得慢?从存储原理到 Linux 性能实测
linux·服务器·网络