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

🔥小叶-duck:个人主页

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

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

《Qt 方寸极境》 《MySQL》

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


目录

前言

[一、Responses API 流式响应前置铺垫](#一、Responses API 流式响应前置铺垫)

[1.1 为什么必须做 SSE 半包缓冲处理](#1.1 为什么必须做 SSE 半包缓冲处理)

[1.2 ChatGPT Responses API 流式事件体系](#1.2 ChatGPT Responses API 流式事件体系)

[1.3 不同事件的 data 字段解析规则](#1.3 不同事件的 data 字段解析规则)

[1.4 关键坑点:200 状态码不代表调用成功](#1.4 关键坑点:200 状态码不代表调用成功)

[二、ChatGPT 流式响应接口实现](#二、ChatGPT 流式响应接口实现)

[2.1 接口定义与职责边界](#2.1 接口定义与职责边界)

[2.1.1 方法签名](#2.1.1 方法签名)

[2.1.2 回调契约](#2.1.2 回调契约)

[2.2 完整实现流程与代码逐步解析](#2.2 完整实现流程与代码逐步解析)

[2.2.1 模型可用性校验](#2.2.1 模型可用性校验)

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

[2.2.3 组装 messages 对话数组](#2.2.3 组装 messages 对话数组)

[2.2.4 构造请求体,开启流式开关](#2.2.4 构造请求体,开启流式开关)

[2.2.5 JSON 序列化请求体](#2.2.5 JSON 序列化请求体)

[2.2.6 初始化 httplib 客户端、超时、代理配置](#2.2.6 初始化 httplib 客户端、超时、代理配置)

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

[2.2.8 定义流式全局变量](#2.2.8 定义流式全局变量)

[2.2.9 构建 httplib::Request 请求对象](#2.2.9 构建 httplib::Request 请求对象)

[2.2.10 response_handler:响应头校验回调](#2.2.10 response_handler:响应头校验回调)

[2.2.11 content_receiver:SSE 分片解析核心回调](#2.2.11 content_receiver:SSE 分片解析核心回调)

[2.2.12 发起 HTTP POST 请求](#2.2.12 发起 HTTP POST 请求)

[2.2.13 收尾判断,返回完整结果](#2.2.13 收尾判断,返回完整结果)

[2.3 流式响应接口完整代码(附注释分析)](#2.3 流式响应接口完整代码(附注释分析))

三、流式响应单元测试

[3.1 初始化与配置](#3.1 初始化与配置)

[3.2 入口函数](#3.2 入口函数)

结束语


前言

在前面章节中,我们已经基于LLMProvider抽象基类,完成了 DeepSeek 基于 Chat Completions 接口的流式接入实现,熟悉了 SSE 流式响应的基础解析流程,也体会到这套抽象框架带来的可扩展能力。

OpenAI 主推的 Responses API,和传统 Chat Completions API 在请求结构、流式事件体系上存在明显差异,不能直接复用原有解析逻辑。本章我们就聚焦 ChatGPT Responses API 的流式接口开发。

我们会先做好前置理论铺垫,理解 Responses API 的事件类型、data 字段解析规则,同时着重说明 SSE 半包缓冲处理的必要性,理清一个很容易踩的坑:HTTP 200 状态码并不代表业务调用成功。之后按照完整流程,一步步实现ChatGPTProvider的流式发送接口,从参数组装、请求构建,到两个核心回调response_handler、content_receiver的编写,完成 SSE 分片数据的解析。

通过本章的落地,我们可以直观感受到:依托之前设计的抽象基类,只需要针对性改写和 Responses API 适配的 20% 差异化代码,就能快速接入一套全新规范的大模型接口,验证我们这套 SDK 架构的扩展性价值。

一、Responses API 流式响应前置铺垫

在正式动手实现 ChatGPTProvider 的流式接口 sendMessageStream 之前,我们先梳理协议层面的关键要点,提前扫清后续代码里容易踩坑的疑点。

1.1 为什么必须做 SSE 半包缓冲处理

我们在前面接入 DeepSeek 的流式调试日志里,观察到貌似所有的 HTTP chunk 里面都携带的是完整的 SSE 事件,当时我们就觉得看起来好像不需要额外做半包处理。

但这只是服务端输出特性带来的偶然现象,不是协议保证 。

TCP 是面向字节流的协议,HTTP Chunk 只是对字节流做了分块封装,它和 SSE 事件边界没有绑定关系:

  • **半包:**一个完整 SSE 事件被切分到两个不同 chunk 里,一次收到半截数据,剩下的内容在后面的 chunk 到达;
  • **粘包:**单个 HTTP chunk 里面一次性塞进来多个完整 SSE 事件。

本次 ChatGPT Responses API 的流式日志,就会出现了单块数据内事件不完整的场景。

所以无论前期测试有没有碰到半包,SSE 解析逻辑必须维护一个缓冲区 buffer,持续拼接数据,以 \n\n 作为分隔符切分独立事件,这是流式解析的强制要求,不能省略。

核心代码思路: 持续把收到的 data 追加进 buffer,循环查找 \n\n,截取一段完整事件交给事件处理器,然后从 buffer 擦除已经处理过的内容,剩余残留数据保留,等待下一次回调继续拼接。

1.2 ChatGPT Responses API 流式事件体系

ChatGPT Responses API 的流式输出,是一套完整的事件驱动机制,每一条 SSE 都携带 event 字段标记事件类型,不同事件含义完全不同,我们需要区分处理:

  1. response.created:对话任务创建成功,拿到本次 response 的唯一 id,会话正式启动;
  2. response.in_progress:任务正在生成中,属于状态通知,一般不需要提取文本;
  3. response.output_item.added:一条输出单元(message)开始;
  4. response.output_text.delta :增量文本事件 ,业务核心,delta 字段存放模型实时吐出的片段文字,我们要把这个片段回调给上层;
  5. response.output_item.done:当前这一条输出单元生成完毕,里面会携带当前 item 的全部完整文本,后续我们完整内容可以通过这个事件类型中的内容进行拼接,而上面的response.output_text.delta 只负责回调流式数据给上层即可;
  6. response.completed:整轮对话全部结束,所有输出单元生成完成,流式链路收尾;
  7. error:错误事件,这个是我们重点要留意的分支。

1.3 不同事件的 data 字段解析规则

每一类 event 对应的 data 内部 JSON 结构不一样,不能用同一套逻辑去解析:

  • response.output_text.delta:关注 delta 字段,这是实时增量文字;
  • response.output_item.done:完整文本放在 item.content[0].text,可以作为兜底校验;
  • error:error.message 存放错误描述,code 标记错误类型(余额不足、参数非法等)。

**开发注意:**所有事件定义以 OpenAI 官方文档的 Streaming events 为准,写代码的时候要对照文档,不要只依赖测试日志。

1.4 关键坑点:200 状态码不代表调用成功

我们在 Apifox 中测试的时候发现一个极易踩坑的问题:

当账号余额耗尽、配额不足这类业务错误发生时,HTTP 响应状态码依旧是 200 OK,不会返回 401/429 这类错误码。

错误信息不会放在 HTTP 头部,而是封装成一条独立的 event: error SSE 事件,混在流式事件流里面下发。

这就意味着:

不能依靠判断 HTTP 状态码等于 200 就认为接口调用成功;

必须在 content_receiver 事件回调里面识别 error 事件,捕获业务异常,向上层抛出错误,否则程序会误以为正常生成,逻辑直接出错。

二、ChatGPT 流式响应接口实现

在上篇文章我们已经完成了 DeepSeek 基于 Chat Completions API 的流式接口开发,掌握了 HTTP Chunk、SSE 事件块、httplib 流式回调的底层规则与各类边界异常场景。

本章我们基于 OpenAI 全新的 Responses API,落地编码实现sendMessageStream流式增量接口,同样实现大模型打字机逐字输出效果。

2.1 接口定义与职责边界

2.1.1 方法签名

cpp 复制代码
namespace ai_chat_sdk {
class ChatGPTProvider : public LLMProvider
{
public:
    /**
     * @brief 发送消息并接收流式增量响应
     * @param messages 历史对话消息列表
     * @param requestParams 请求参数映射(temperature、max_output_tokens等)
     * @param callback 增量数据回调函数,由调用方实现业务逻辑
     * @return std::optional<std::string> 完整的响应文本(所有增量拼接结果)
     */
    std::optional<std::string> sendMessageStream(
        const std::vector<Message>& messages,
        const std::unordered_map<std::string, std::string>& requestParams,
        std::function<void(const std::string &response, bool isEnd)> callback
    );
};
}

这个接口作为上层对外调用入口,返回值是拼接完成后的完整回答内容;核心交互依靠传入的回调函数向外推送分片数据。

2.1.2 回调契约

回调函数 std::function<void(const std::string &response, bool isEnd)> 是流式 SDK 解耦的关键,约定如下:

  • **第一个入参:**模型返回的增量文本片段,仅携带本次新生成的内容,不是完整回复;
  • 第二个入参: 流结束标记。true代表所有增量数据推送完毕,流正常结束;false代表还有后续分片;
  • **职责划分:**SDK 只负责完成 HTTP 请求、SSE 协议解析、提取增量文本并触发回调;打印、界面渲染、持久化存储这类业务逻辑全部交给上层调用者实现,底层 Provider 不耦合业务。

回调接口签名和 DeepSeek 保持一致,上层业务代码可以无缝切换不同 Provider,这也是我们抽象 LLMProvider 基类的价值。

2.2 完整实现流程与代码逐步解析

2.2.1 模型可用性校验

cpp 复制代码
if (!IsAvailable())
{
    ERR("ChatGPTProvider sendMessageStream model not available");
    return std::nullopt;
}

和 DeepSeek 逻辑一致,先判断当前 Provider 是否已经初始化完成、配置好 api-key 与 baseUrl,未就绪直接返回空,终止流程。

2.2.2 构造并获取请求参数

cpp 复制代码
// 2.1 构造默认请求参数;如果requestParams中没有指定temperature、max_output_tokens,则使用默认值
double temperature = 0.7;
int max_output_tokens = 2048;
std::string 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必须用户手动配置,如果没有配置则直接返回空值
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;
}

和 DeepSeek 区别:

DeepSeek(Chat Completions)参数字段是**max_tokens** ;

ChatGPT Responses API 使用字段 max_output_tokens,字段名称不能混用,这是 OpenAI 两套 API 的典型差异。

2.2.3 组装 messages 对话数组

循环遍历传入的历史消息列表,按照 ChatGPT 接口文档要求,组装role和content字段,构造 JSON 数组,携带上下文实现多轮对话。

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

2.2.4 构造请求体,开启流式开关

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

核心差异点:

  • DeepSeek Chat Completions:消息字段是**messages**;
  • ChatGPT Responses API:消息数组字段是 input;
  • 同样需要设置stream:true开启流式返回,不开启则一次性返回完整报文。

2.2.5 JSON 序列化请求体

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

序列化逻辑和 DeepSeek 完全一致,使用 jsoncpp 序列化 JSON 对象。

2.2.6 初始化 httplib 客户端、超时、代理配置

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

// 从环境变量读取代理配置
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;
}

网络客户端、超时逻辑和 DeepSeek 完全一致,这部分代码可以复用。

然后不管是全量响应还是流式响应,因为 OpenAI 接口在国内直连无法访问 ,所以我们需要从环境变量 MY_APP_PROXY 读取代理地址 ,格式强制要求 host:port。

  • 读取环境变量,判断是否存在;
  • 存在则拆分主机与端口,给 httplib 客户端设置代理;
  • 环境变量不存在,直接报错返回,提示用户必须配置代理。

2.2.7 请求头构造

cpp 复制代码
httplib::Headers headers = {
    {"Accept", "text/event-stream"},
    {"Content-Type", "application/json"},
    {"Authorization", "Bearer " + apiKey}
};

请求头和 DeepSeek 流式接口一致,Accept: text/event-stream 是 SSE 流式协议的标识,告诉服务端返回 SSE 事件流。

2.2.8 定义流式全局变量

cpp 复制代码
std::string buffer;
std::string fullResponse;
bool streamEnd = false;
bool isError = false;
std::string errorMsg;
  • buffer:SSE 半包缓冲区,用来缓存不完整的 chunk,保证每次解析的是完整 SSE 事件;
  • fullResponse:拼接全部增量文本,函数最终返回这个完整字符串;
  • streamEnd、isError:流结束标记、异常标记;

和 DeepSeek 的缓冲区设计思路完全相同,这是 SSE 解析的通用方案。

2.2.9 构建 httplib::Request 请求对象

cpp 复制代码
httplib::Request request;
request.method = "POST";
request.path = "/v1/responses";
request.headers = headers;
request.body = requestBodyStr;

代码解析:

  1. httplib::Request request;:实例化 httplib 的请求结构体,这个结构体承载一次 HTTP 请求全部要素,和 DeepSeek 流式请求的结构体类型一致。
  2. request.method = "POST";:指定请求方法为 POST。Responses API 需要在请求体携带 JSON 参数,因此使用 POST,和 DeepSeek Chat Completions 接口请求方式保持一致。
  3. request.path = "/v1/responses";
    这里是和 DeepSeek 重要差异点: DeepSeek 的对话接口路径是 /v1/chat/completions(Chat Completions 体系);
    ChatGPT 这里是 /v1/responses,属于 OpenAI 新的 Responses API 的专属路由路径,路径写错会直接 404。
  4. request.headers = headers;:把前面构造好的请求头赋值进去,包含鉴权头、Accept: text/event-stream、Content-Type,用来告诉服务端我们要 SSE 流式返回。
  5. request.body = requestBodyStr;:填入序列化完成的 JSON 请求报文,也就是我们组装好的 model、input、temperature、stream 等参数。

补充说明:

这一步只是组装请求报文,还没有真正发送网络请求 。组装完成之后,我们才会继续挂载response_handler、content_receiver两个流式回调,最后调用client.send(request)发起网络调用。

整个请求组装的框架写法和 DeepSeek 完全相同,只需要修改 path 路径、请求体字段,就适配两套不同 API。

2.2.10 response_handler:响应头校验回调

cpp 复制代码
request.response_handler = [&](const httplib::Response &res) -> bool
{
    if (res.status != 200)
    {
        isError = true;
        ERR("ChatGPTProvider sendMessageStream status not 200, status: {}, body: {}", res.status, res.body);
        return false;
    }
    return true;
};

在收到 HTTP 响应头的时候触发,只校验 HTTP 状态码。逻辑和 DeepSeek 版本一致。

2.2.11 content_receiver:SSE 分片解析核心回调

这是两个流式实现最大的不同之处 ,也是 Responses API 和 Chat Completions API 最核心的区别:

DeepSeek(Chat Completions)SSE 事件固定为 data:, 通过choices0.delta.content 取出增量文本;

ChatGPT Responses API 有多种不同的 eventType 事件类型,不同事件携带不同含义,需要分支判断处理。

cpp 复制代码
request.content_receiver = [&](const char *data, size_t data_length, uint64_t offset, uint64_t total_length) -> bool
{
    if (isError)
    {
        return false;
    }
    // 1. 追加数据到缓冲区,处理半包
    buffer.append(data, data_length);
    INFO("ChatGPTProvider sendMessageStream data: {}", buffer);

    // 2. 循环切割完整SSE事件块(以 \n\n 作为结束分隔符)
    size_t pos;
    while ((pos = buffer.find("\n\n")) != std::string::npos)
    {
        std::string event = buffer.substr(0, pos);
        buffer.erase(0, pos + 2);

        std::string eventLine;
        std::string eventData;
        std::string eventType;
        std::istringstream eventStream(event);

        while (std::getline(eventStream, eventLine))
        {
            if (eventLine.empty() || eventLine[0] == ':')
            {
                continue;
            }
            if (eventLine.compare(0, 7, "event: ") == 0)
            {
                eventType = eventLine.substr(7);
            }
            else if (eventLine.compare(0, 6, "data: ") == 0)
            {
                eventData = eventLine.substr(6);
            }
        }
        // 3. 反序列化data对应的json
        Json::CharReaderBuilder readerbuilder;
        Json::Value modelDataJson;
        std::istringstream modelDataStream(eventData);
        if (!Json::parseFromStream(readerbuilder, &modelDataStream, &modelDataJson, &errorMsg))
        {
            ERR("ChatGPTProvider sendMessageStream parse eventData error: {}", errorMsg);
            continue;
        }

        // ========== Responses API 多事件分支(和DeepSeek最大区别)==========
        if (eventType == "response.output_text.delta")
        {
            // 增量文本分片,回调推送给上层
            if (modelDataJson.isMember("delta") && modelDataJson["delta"].isString())
            {
                std::string delta = modelDataJson["delta"].asString();
                callback(delta, streamEnd);
                fullResponse += delta;
            }
        }
        else if (eventType == "response.output_item.done")
        {
            // 单个输出项完成,收集文本片段
            if (modelDataJson.isMember("item") && modelDataJson["item"].isObject())
            {
                Json::Value item = modelDataJson["item"];
                if (item.isMember("content") && item["content"].isArray() && !item["content"].empty())
                {
                    Json::Value content = item["content"][0];
                    if (content.isMember("text") && content["text"].isString())
                    {
                        fullResponse += content["text"].asString();
                    }
                }
            }
        }
        else if (eventType == "response.completed")
        {
            // 整个会话流全部结束
            streamEnd = true;
            callback("", true);
            return true;
        }
        else if (eventType == "error")
        {
            // 错误事件
            isError = true;
            if (modelDataJson.isMember("error") && modelDataJson["error"].isObject())
            {
                if (modelDataJson["error"].isMember("message") && modelDataJson["error"]["message"].isString())
                {
                    errorMsg = modelDataJson["error"]["message"].asString();
                }
            }
            ERR("ChatGPTProvider sendMessageStream error: {}", errorMsg);
            callback(errorMsg, true);
            streamEnd = true;
        }
        else if (eventType == "response.incomplete")
        {
            // 输出截断、max_output_tokens超限场景
            isError = true;
            if (modelDataJson.isMember("response") && modelDataJson["response"].isObject())
            {
                Json::Value response = modelDataJson["response"];
                if (response.isMember("incomplete_details") && response["incomplete_details"].isObject())
                {
                    Json::Value incomplete_details = response["incomplete_details"];
                    if (incomplete_details.isMember("reason") && incomplete_details["reason"].isString())
                    {
                        std::string reason = incomplete_details["reason"].asString();
                        if (reason == "max_output_tokens")
                        {
                            errorMsg = "[系统提示:输出超出最大令牌数,请适当增加 max_output_tokens 参数]";
                        }
                        else
                        {
                            errorMsg = "[系统提示:本次响应未完整生成,原因:" + reason + "]";
                        }
                    }
                }
            }
            streamEnd = true;
            callback(errorMsg, true);
        }
        else if (eventType == "response.failed")
        {
            // 请求失败事件
            isError = true;
            if (modelDataJson.isMember("response") && modelDataJson["response"].isObject())
            {
                Json::Value response = modelDataJson["response"];
                if (response.isMember("error") && response["error"].isObject())
                {
                    if (response["error"].isMember("message") && response["error"]["message"].isString())
                    {
                        errorMsg = response["error"]["message"].asString();
                    }
                }
            }
            ERR("ChatGPTProvider sendMessageStream failed: {}", errorMsg);
            callback(errorMsg, true);
            streamEnd = true;
        }
    }
    return true;
};

重点对比总结:

  1. DeepSeek Chat Completions:只有一种事件 ,data: 包裹,只需要读取delta.content;事件结束标识是[DONE];
  2. ChatGPT Responses API:多事件驱动 ,eventType区分事件类型:
    • response.output_text.delta:增量打字片段(最核心,逐字推送)
    • response.output_item.done:单条输出项结束
    • response.completed:整个对话流正常结束
    • error / response.failed:运行异常
    • response.incomplete:输出被截断(token 上限等)

这就是为什么说两套 API 虽然上层回调接口一致,但内部 20% 解析代码差异巨大,事件体系完全不同。

2.2.12 发起 HTTP POST 请求

cpp 复制代码
auto res = client.send(request);
if (!res)
{
    ERR("ChatGPTProvider sendMessageStream network error, {}", httplib::to_string(res.error()));
    return std::nullopt;
}

发送请求逻辑和 DeepSeek 完全一致,判断网络层面是否成功。

2.2.13 收尾判断,返回完整结果

cpp 复制代码
if (!streamEnd)
{
    WARN("ChatGPTProvider stream ended without response.completed");
    // 手动将第二个参数设置成true,告诉用户我们已经获取到了响应内容,但是没有收到response.completed事件类型
    callback("\n\n[系统提示: 连接意外中断,请重试]", true);
}
return fullResponce;

兜底逻辑: 防止服务端流异常、缺少结束事件,保证上层回调一定收到isEnd=true,避免上层一直等待。

2.3 流式响应接口完整代码(附注释分析)

cpp 复制代码
// 发送消息:增量调用,实时返回响应
// requestParams:针对ChatGPT模型本次请求专属参数,包含model、temperature、max_output_tokens等
std::optional<std::string> ChatGPTProvider::sendMessageStream(const std::vector<Message> &messages,
                                                              const std::unordered_map<std::string, std::string> &requestParams,
                                                              std::function<void(const std::string &response, bool isEnd)> callback)
{
    // 1、检查模型是否有效
    if (!IsAvailable())
    {
        ERR("ChatGPTProvider sendMessageStream model not available");
        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;
    // 关键:流式响应需要设置stream为true(否则是全量响应会返回完整的响应体,而不是增量响应)
    requestBody["stream"] = true;

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

    // 5、使用cpp-httplib构建HTTP客户端
    // 5.1设置超时时间
    httplib::Client client(_baseUrl);
    client.set_connection_timeout(20, 0); // 连接超时时间20秒
    client.set_read_timeout(300, 0);      // 作为流式响应,读取时间相对较长,设置超时时间为300秒

    // 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 构建请求头(关键:相较于非流式响应,流式响应需要设置Accept为text/event-stream)
    httplib::Headers headers = {
        {"Accept", "text/event-stream"},
        {"Content-Type", "application/json"},
        {"Authorization", "Bearer " + _apikey}};

    // 在构建请求对象之前,我们可以先设置一些流式处理变量,用于处理回调过程中的相关逻辑。
    std::string buffer;       // 用于存储每次读取到的Chunk数据块中的内容
    std::string fullResponce; // 用于累积存储获取到的完整的响应体
    bool streamEnd = false;   // 用于标记流式响应是否结束
    bool isError = false;     // 用于标记是否发生错误
    std::string errorMsg;     // 用于存储错误描述信息

    // 6、构建请求对象
    httplib::Request request;
    request.method = "POST";
    request.path = "/v1/responses";
    request.headers = headers;
    request.body = requestBodyStr;

    // 6.1 构建响应处理器回调函数:response_handler
    // 关键:response_handler 函数会在收到响应头时被调用,我们可以在其中判断响应是否正常(该回调函数只调用一次),
    // 如果响应异常,我们可以直接返回false,结束请求。
    // 如果响应正常,我们可以返回true,继续处理响应体。
    request.response_handler = [&](const httplib::Response &res) -> bool
    {
        if (res.status != 200)
        {
            isError = true;
            ERR("ChatGPTProvider sendMessageStream status not 200, status: {}, body: {}", res.status, res.body);
            return false;
        }
        return true;
    };

    // 6.2 构建数据接收处理器回调函数:content_receiver
    // 关键:content_receiver 函数会在收到完整Chunk数据块时被调用,我们可以在其中解析响应体(该回调函数会被多次调用),
    // 如果解析过程中发生严重错误,我们可以直接返回false,结束请求。
    // 如果解析过程正常,我们可以返回true,继续处理下一个数据块。
    request.content_receiver = [&](const char *data, size_t data_length,
                                   uint64_t offset, uint64_t total_length) -> bool
    {
        // 1、验证前面的响应头是否出错,出错了,直接返回false结束请求,不需要再接收数据
        if (isError)
        {
            return false;
        }

        // 2、追加当前所有的事件块到缓冲区,保证每次处理的事件块都是完整的
        buffer.append(data, data_length);
        INFO("ChatGPTProvider sendMessageStream data: {}", buffer);

        // 3、解析缓冲区中的事件块,判断是否为完整的 SSE 事件块
        size_t pos;
        while ((pos = buffer.find("\n\n")) != std::string::npos)
        {
            // 3.1 截取当前找到的事件块
            std::string event = buffer.substr(0, pos);
            buffer.erase(0, pos + 2); // 移除已处理的事件块,避免后续解析时重复处理

            // 3.2 关键处理:解析当前事件块中的每一行
            std::string eventline;
            // 获取data字段和event字段
            std::string eventData;
            std::string eventType;
            std::istringstream eventstream(event);
            while (std::getline(eventstream, eventline))
            {
                // 过滤空行和注释行
                if (eventline.empty() || eventline[0] == ':')
                {
                    continue;
                }
                if (eventline.compare(0, 7, "event: ") == 0)
                {
                    eventType = eventline.substr(7);
                    BUG("ChatGPTProvider sendMessageStream eventType: {}", eventType);
                }
                else if (eventline.compare(0, 6, "data: ") == 0)
                {
                    eventData = eventline.substr(6);
                    BUG("ChatGPTProvider sendMessageStream eventData: {}", eventData);
                }
            }

            // 3.3 解析当前事件块中的data字符串 -- 反序列化
            Json::CharReaderBuilder readerbuilder;
            Json::Value modeldataJson;
            std::istringstream modeldataStream(eventData);
            if (!Json::parseFromStream(readerbuilder, modeldataStream, &modeldataJson, &errorMsg))
            {
                ERR("ChatGPTProvider sendMessageStream parse eventData error: {}", errorMsg);
                continue;
            }

            // 按照事件类型进行data数据分析处理
            if (eventType == "response.output_text.delta")
            {
                if (modeldataJson.isMember("delta") && modeldataJson["delta"].isString())
                {
                    std::string delta = modeldataJson["delta"].asString();
                    callback(delta, streamEnd);
                }
            }
            else if (eventType == "response.output_item.done")
            {
                if (modeldataJson.isMember("item") && modeldataJson["item"].isObject())
                {
                    Json::Value item = modeldataJson["item"];
                    if (item.isMember("content") && item["content"].isArray() && !item["content"].empty())
                    {
                        Json::Value content = item["content"][0];
                        if (content.isMember("text") && content["text"].isString())
                        {
                            fullResponce += content["text"].asString();
                        }
                    }
                }
            }
            else if (eventType == "response.completed")
            {
                streamEnd = true;
                callback("", true);
                return true;
            }
            // 特殊情况处理
            else if (eventType == "error")
            {
                // 处理error事件
                errorMsg.clear(); // 清空之前的错误信息, 防止上一次的错误信息干扰
                if (modeldataJson.isMember("error") && modeldataJson["error"].isObject())
                {
                    Json::Value error = modeldataJson["error"];
                    if (error.isMember("message") && error["message"].isString())
                    {
                        errorMsg = error["message"].asString();
                    }
                }
                if (errorMsg.empty())
                {
                    errorMsg = "请求发生未知错误";; // 兜底
                }
                // 出错了,直接将错误信息返回给用户
                ERR("ChatGPTProvider sendMessageStream error: {}", errorMsg);
                streamEnd = true;
                callback(errorMsg, true);
                return false;
            }
            else if (eventType == "response.incomplete")
            {
                // 处理响应不完整事件
                std::string reason = "unknown";
                if (modeldataJson.isMember("response") && modeldataJson["response"].isObject())
                {
                    Json::Value response = modeldataJson["response"];
                    if (response.isMember("incomplete_details") && response["incomplete_details"].isObject())
                    {
                        Json::Value incomplete_details = response["incomplete_details"];
                        if (incomplete_details.isMember("reason") && incomplete_details["reason"].isString())
                        {
                            reason = incomplete_details["reason"].asString();
                        }
                    }
                }
                if (reason == "max_output_tokens")
                {
                    errorMsg = "[系统提示:超出最大输出令牌数,请适当增加 max_output_tokens 参数]";
                }
                else
                {
                    errorMsg = "[系统提示:本次响应未完整生成,原因:" + reason + "]";
                }
                streamEnd = true;
                callback(errorMsg, true);
                return false;
            }
            else if (eventType == "response.failed")
            {
                // 处理模型执行失败事件
                errorMsg.clear(); // 清空之前的错误信息, 防止上一次的错误信息干扰
                if (modeldataJson.isMember("response") && modeldataJson["response"].isObject())
                {
                    Json::Value response = modeldataJson["response"];
                    if (response.isMember("error") && response["error"].isObject())
                    {
                        Json::Value error = response["error"];
                        if (error.isMember("message") && error["message"].isString() && !error["message"].asString().empty())
                        {
                            errorMsg = error["message"].asString();
                        }
                    }
                }
                if (errorMsg.empty())
                {
                    errorMsg = "模型执行失败"; // 兜底
                }
                ERR("ChatGPTProvider sendMessageStream failed: {}", errorMsg);
                streamEnd = true;
                callback(errorMsg, true);
                return false;
            }
        }

        // 到这,说明当前数据块chunk中的所有事件块已经处理完,继续等待后续数据块的到来
        return true;
    };

    // 7、发送请求,获取服务器响应
    auto res = client.send(request);
    // 7.1 判断请求发送是否成功
    if (!res)
    {
        // 发送请求失败,可能出现网络问题,比如DNS解析失败,连接超时等等
        ERR("ChatGPTProvider sendMessageStream send request error: {}", httplib::to_string(res.error()));
        return std::nullopt;
    }

    if (!streamEnd)
    {
        WARN("ChatGPTProvider stream ended without response.completed");
        // 手动将第二个参数设置成true,告诉用户我们已经获取到了响应内容,但是没有收到response.completed事件类型
        callback("\n\n[系统提示: 连接意外中断,请重试]", true);
    }
    return fullResponce;
}

三、流式响应单元测试

3.1 初始化与配置

cpp 复制代码
#include "../SDK/include/ChatGPTProvider.h"
#include "../SDK/include/DeepSeekProvider.h"
#include "../SDK/include/GeminiProvider.h"
#include "../SDK/include/OllamaLLMProvider.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、调用ssendMessageStreame发送消息
    auto response = chatgptProvider->sendMessageStream(
        messages, requestConfig,
        [&](const std::string &chunk, bool isEnd)
        {
            INFO("chunk: {}", chunk);
            if (isEnd)
            {
                INFO("[DONE]");
            }
        });
    // 6.1 检查响应是否为空
    ASSERT_TRUE(response.has_value());
    INFO("response: {}", response.value());
}

3.2 入口函数

cpp 复制代码
int main(int argc, char **argv)
{
    // 初始化Google Test框架
    testing::InitGoogleTest(&argc, argv);
 
    // 初始化日志系统
    Logger::logger_init("testLLM", "stdout", spdlog::level::debug);
 
    // 运行所有测试用例
    return RUN_ALL_TESTS();
}

结束语

到这里,ChatGPT Responses API 流式响应接口的实现就全部完成了。

我们先是梳理了 Responses API 独有的流式事件体系,理解了不同事件对应的解析规则,也记住了 200 状态码不等于业务成功这个重要坑点;随后沿着目录的流程,依次完成参数组装、请求构造、SSE 分片解析回调的编写,依靠半包缓冲机制,保证流式数据解析的健壮性。

对比之前 DeepSeek 的 Chat Completions 实现,能清晰看到两套接口在路由、报文结构、流式事件上的区别。也印证了我们前期抽象 LLMProvider 的设计意义:大部分通用网络、日志、基础框架代码可以复用,只需要针对不同厂商、不同 API 规范编写差异化的解析逻辑,就可以新增一个模型接入。

相关推荐
梦帮科技37 分钟前
意识启发式量子隧穿退火算法用于全局优化 (CITA) 完整技术指南
网络·人工智能·算法·量子计算·模拟退火算法
lisw052 小时前
作为人类代理的大语言模型及其角色分类!
人工智能·学习·语言模型
落樱弥城2 小时前
Perf 逐步探查性能缺口并优化
c++·性能优化
坤坤子吖2 小时前
Linux网络编程:UDP进阶,从字典服务器到简单聊天室
linux·服务器·网络·c++·笔记·学习
Rabitebla2 小时前
【Linux学习笔记】进程篇(九):进程的创建与退出——fork、写时拷贝与退出码
linux·c++·笔记·学习
时间的拾荒人3 小时前
CMake 实战指南:从基础语法到静态库/动态库制作与链接
开发语言·c++·面试
道尔柯南3 小时前
C++ STL 入门详解:从认识标准模板库到掌握学习方法
java·c++·学习方法
爱和冰阔落3 小时前
【Linux网络】TCP 服务器怎么写?从 listen、accept 到 read/write,手写完整 Echo Server
linux·运维·服务器·网络
全栈陈序员5 小时前
【Git】从入门到精通(十)—— 撤销与恢复:git checkout与git restore
运维·网络·数据库·git