🔥小叶-duck:个人主页

 ❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
《Qt 方寸极境》 《MySQL》

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


目录

前言

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

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

  1.2 ChatGPT Responses API 流式事件体系

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

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

二、ChatGPT 流式响应接口实现

  2.1 接口定义与职责边界

    2.1.1 方法签名

    2.1.2 回调契约

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

    2.2.1 模型可用性校验

    2.2.2 构造并获取请求参数

    2.2.3 组装 messages 对话数组

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

    2.2.5 JSON 序列化请求体

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

    2.2.7 请求头构造

    2.2.8 定义流式全局变量

    2.2.9 构建 httplib::Request 请求对象

    2.2.10 response_handler:响应头校验回调

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

    2.2.12 发起 HTTP POST 请求

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

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

三、流式响应单元测试

  3.1 初始化与配置

  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 方法签名

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 模型可用性校验

if (!IsAvailable())
{
    ERR("ChatGPTProvider sendMessageStream model not available");
    return std::nullopt;
}

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

    2.2.2 构造并获取请求参数

// 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 数组,携带上下文实现多轮对话。

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 构造请求体,开启流式开关

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 序列化请求体

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

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

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

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 请求头构造

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

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

    2.2.8 定义流式全局变量

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 请求对象

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:响应头校验回调

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:,通过choices[0].delta.content取出增量文本;
      ChatGPT Responses API 有多种不同的 eventType 事件类型,不同事件携带不同含义,需要分支判断处理。

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 请求

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

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

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

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

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

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

// 发送消息:增量调用,实时返回响应
// 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 初始化与配置

#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 入口函数

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 规范编写差异化的解析逻辑,就可以新增一个模型接入。

Logo

AtomGit AI 社区提供模型库、数据集、Agent、Token等资源

更多推荐