
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[一、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 字段标记事件类型,不同事件含义完全不同,我们需要区分处理:
response.created:对话任务创建成功,拿到本次 response 的唯一 id,会话正式启动;response.in_progress:任务正在生成中,属于状态通知,一般不需要提取文本;response.output_item.added:一条输出单元(message)开始;response.output_text.delta:增量文本事件 ,业务核心,delta字段存放模型实时吐出的片段文字,我们要把这个片段回调给上层;response.output_item.done:当前这一条输出单元生成完毕,里面会携带当前 item 的全部完整文本,后续我们完整内容可以通过这个事件类型中的内容进行拼接,而上面的response.output_text.delta 只负责回调流式数据给上层即可;response.completed:整轮对话全部结束,所有输出单元生成完成,流式链路收尾;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;
代码解析:
httplib::Request request;:实例化 httplib 的请求结构体,这个结构体承载一次 HTTP 请求全部要素,和 DeepSeek 流式请求的结构体类型一致。request.method = "POST";:指定请求方法为 POST。Responses API 需要在请求体携带 JSON 参数,因此使用 POST,和 DeepSeek Chat Completions 接口请求方式保持一致。request.path = "/v1/responses";
这里是和 DeepSeek 重要差异点: DeepSeek 的对话接口路径是/v1/chat/completions(Chat Completions 体系);
ChatGPT 这里是/v1/responses,属于 OpenAI 新的 Responses API 的专属路由路径,路径写错会直接 404。request.headers = headers;:把前面构造好的请求头赋值进去,包含鉴权头、Accept: text/event-stream、Content-Type,用来告诉服务端我们要 SSE 流式返回。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;
};
重点对比总结:
- DeepSeek Chat Completions:只有一种事件 ,
data:包裹,只需要读取delta.content;事件结束标识是[DONE]; - 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 规范编写差异化的解析逻辑,就可以新增一个模型接入。