大家好我是Halcyon.平安欢迎文末添加好友交流共同进步一、本篇概述二、什么是 SSE2.1 SSE vs 普通请求2.2 SSE 数据格式三、与 sendMessage 的相同部分3.1 请求体多了 stream: true3.2 读取超时更长3.3 请求头多了 Accept四、流式处理变量五、构造 Request 对象5.1 为什么不用 client.Post()5.2 response_handler — 响应头处理器5.3 content_receiver — 数据接收处理器六、content_receiver 逐行解析6.1 错误检查6.2 追加数据到 buffer6.3 按 \n\n 分割并处理每个事件6.4 过滤无效事件6.5 提取 data 字段6.6 检查结束标记6.7 解析 JSON 增量数据6.8 多重检查提取增量内容6.9 累积并回调6.10 JSON 解析失败的容错6.11 content_receiver 返回七、发送请求与收尾7.1 发送请求7.2 确保流式正常结束7.3 返回完整回复八、完整流程图九、全量 vs 流式对比总结十、总结一、本篇概述上篇讲了initModel()和sendMessage()全量请求本篇聚焦sendMessageStream()流式请求。这是整个 SDK最复杂的函数涉及 SSE 协议解析、数据缓冲、逐块回调。sendMessageStream() 的工作方式 用户提问 → SDK 发起 HTTP 请求 → DeepSeek 边生成边返回 → SDK 逐块解析 → 每解析出一段文字就通过 callback 通知上层 → 上层ChatServer立即推送给前端 → 用户看到打字机效果二、什么是 SSE在深入代码之前需要先理解SSEServer-Sent Events服务器推送事件。2.1 SSE vs 普通请求普通 HTTP 请求sendMessage 全量 客户端 ──请求──→ 服务器 客户端 ←──等待──────── 客户端 ←──────────── 完整响应一次性 SSE 流式请求sendMessageStream 客户端 ──请求──→ 服务器 客户端 ←──chunk1── 你 客户端 ←──chunk2── 好 客户端 ←──chunk3── 客户端 ←──chunk4── 我是AI助手 客户端 ←──[DONE]── 结束2.2 SSE 数据格式SSE 是一种文本协议每条消息用\n\n两个换行分隔。DeepSeek 返回的原始数据大概长这样data: {choices:[{delta:{content:你}}]}\n\n data: {choices:[{delta:{content:好}}]}\n\n data: {choices:[{delta:{content:}}]}\n\n : comment\n\n data: {choices:[{delta:{content:我是AI助手}}]}\n\n data: [DONE]\n\n规则每条消息以data:开头以:开头的是注释行需要忽略data: [DONE]是结束标记每条消息之间用\n\n分隔三、与 sendMessage 的相同部分sendMessageStream()的前半部分构造请求参数、历史消息、请求体和sendMessage()几乎一样只有两处区别3.1 请求体多了 “stream”: truerequestBody[stream]true;这一行告诉 DeepSeek API请用流式方式返回结果。如果设为false或不设就返回完整结果。3.2 读取超时更长// sendMessage 全量 client.set_read_timeout(60, 0); // 60秒 // sendMessageStream 流式 client.set_read_timeout(300, 0); // 300秒流式响应是边生成边传输的模型生成一段文字就发一段整个过程的持续时间远比全量请求长。如果设太短长文本生成还没结束就超时了。3.3 请求头多了 Accept{Accept,text/event-stream}告诉服务器客户端期望接收 SSE 格式的流式数据。四、流式处理变量在发送请求之前先声明一组用于流式处理的变量std::string buffer;// 接受流式响应的数据块boolgotErrorfalse;// 标记响应是否成功std::string errorMsg;// 错误描述符intstatusCode0;// 响应状态码boolstreamFinishfalse;// 标记流式响应是否完成std::string fullResponse;// 累积完整的响应变量类型作用bufferstring数据缓冲区。网络传输中一次 recv 可能收到不完整的数据需要缓冲拼接gotErrorbool一旦 HTTP 状态码非 200设为true后续接收器直接终止errorMsgstring存储错误描述如HTTP status code: 401statusCodeint存储响应状态码本代码中声明了但未实际使用streamFinishbool收到[DONE]标记后设为true用于最后检查流是否正常结束fullResponsestring累积所有增量文本最终作为函数返回值返回给调用方为什么需要 buffer网络传输的特点是一次 recv 调用可能收到任意数量的数据。可能一次收到半个 SSE 事件也可能一次收到三个完整事件。所以需要一个缓冲区来拼接数据等凑够一个完整事件找到\n\n分隔符再解析。第 1 次 recv: data: {\choices\:[{\delta\:{\content\:\你\}}]}\n\nda ↑ 分隔符 ↑ 不完整 buffer 中: data: {\choices\:[{\delta\:{\content\:\你\}}]}\n\nda ├────── 完整事件可以解析 ──────┤└─ 留在 buffer 等下次 ─┘ 第 2 次 recv: ta: {\choices\:[{\delta\:{\content\:\好\}}]}\n\n 与 buffer 中残留的 da 拼接成完整事件五、构造 Request 对象5.1 为什么不用 client.Post()上篇的全量请求直接用client.Post()它是一次性发送请求、等待完整响应。但流式请求需要边接收边处理所以要用更底层的httplib::Request对象手动设置两个回调httplib::Request req;req.methodPOST;req.path/v1/chat/completions;req.headersheaders;req.bodyrequestBodyStr;字段赋值说明methodPOSTHTTP 方法path/v1/chat/completions请求路径headers之前构造的 headers包含认证、内容类型、SSE 接受头body序列化后的 JSON 字符串请求体和client.Post(path, headers, body, type)的参数是对应的只是拆成了结构体的字段。5.2 response_handler — 响应头处理器req.response_handler[](consthttplib::Responseres){if(res.status!200){gotErrortrue;errorMsgHTTP status code: std::to_string(res.status);returnfalse;// 终止请求}returntrue;// 继续接收后续数据};这个回调在收到 HTTP 响应头时被调用注意此时还没收到 body 数据[]— Lambda 按引用捕获所有外部变量这样可以修改gotError和errorMsgres.status ! 200— 如果状态码不是 200比如 401 认证失败说明请求出错了gotError true— 设置错误标记后续content_receiver会检查这个标记return false— 告诉 cpp-httplib终止请求不需要继续接收 body 数据了return true— 状态码正常继续接收后续的 body 数据执行时序client.send(req) │ ├── TCP 连接建立 ├── 发送请求数据 ├── 收到响应头 ──→ response_handler 被调用 │ ├── status ! 200 → gotErrortrue, return false → 终止 │ └── status 200 → return true → 继续 │ ├── 收到第 1 块 body ──→ content_receiver 被调用 ├── 收到第 2 块 body ──→ content_receiver 被调用 ├── ... └── 接收完毕 ──→ client.send() 返回5.3 content_receiver — 数据接收处理器这是流式处理的核心每收到一块数据就会被 cpp-httplib 调用req.content_receiver[](constchar*data,size_t len,size_t offset,size_t totalLength){四个参数的含义参数类型说明dataconst char*指向本次收到的原始数据的指针lensize_t本次收到数据的字节长度offsetsize_t当前数据在整个响应中的偏移量本代码未使用totalLengthsize_t响应体总长度本代码未使用下面逐块解析 content_receiver 的内部逻辑。六、content_receiver 逐行解析6.1 错误检查if(gotError){returnfalse;}入口先检查response_handler是否标记了错误。如果 HTTP 状态码不是 200gotError已经是true直接返回false终止接收。6.2 追加数据到 bufferbuffer.append(data,len);std::string::append(const char* s, size_t n)— 将data指向的前len个字节追加到buffer末尾。为什么不直接用buffer data因为data是原始的字节指针不一定以\0结尾。operator遇到\0就停了而append(data, len)精确地追加len个字节即使中间有\0。6.3 按 \n\n 分割并处理每个事件size_t pos0;while((posbuffer.find(\n\n))!std::string::npos){std::string chunkbuffer.substr(0,pos);buffer.erase(0,pos2);buffer.find(\n\n)— 在缓冲区中查找 SSE 事件分隔符\n\nstd::string::npos— 表示没找到是string::find失败时的返回值值为-1但类型是size_t即无符号最大值buffer.substr(0, pos)— 从位置 0 开始截取pos个字符得到一个完整的 SSE 事件文本buffer.erase(0, pos 2)— 从 buffer 中删除已处理的部分pos 2是数据长度加上\n\n的 2 个字符用 while 而不是 if 的原因一次 recv 可能收到多个完整事件需要循环处理直到 buffer 中没有完整的\n\n分隔的事件为止。buffer 内容: data: {...}\n\ndata: {...}\n\ndata: {...Incomplete ↑ 第 1 个 \n\n ↑ 第 2 个 \n\n while 循环 3 次 第 1 次提取 data: {...}删除已处理部分 第 2 次提取 data: {...}删除已处理部分 第 3 次find(\n\n) npos → 退出循环 buffer 中剩余 data: {...Incomplete 等待下次 recv 拼接6.4 过滤无效事件if(chunk.empty()||chunk[0]:){continue;}chunk.empty()— 空事件跳过chunk[0] :— SSE 协议规定以:开头的行是注释comment服务器可能用来维持连接心跳需要忽略可能收到的数据 \n\n → chunk 为空跳过 : keep-alive\n\n → chunk 以 : 开头跳过 data: {...}\n\n → 有效数据继续处理6.5 提取 data 字段if(chunk.compare(0,6,data: )0){std::string modelDatachunk.substr(6);chunk.compare(0, 6, data: )— 从 chunk 的第 0 个位置开始取 6 个字符与data: 比较。等于 0 表示匹配chunk.substr(6)— 去掉data: 前缀6 个字符得到后面的 JSON 数据等价于if(chunk.starts_with(data: )){// C20 写法std::string modelDatachunk.substr(6);}compare是 C98 就有的方法兼容性更好。6.6 检查结束标记if(modelData[DONE]){callback(,true);streamFinishtrue;returntrue;}modelData [DONE]— DeepSeek 在流式数据最后会发送data: [DONE]表示生成结束callback(, true)— 调用回调通知上层没有更多内容了第一个参数为空流式结束第二个参数为truestreamFinish true— 标记流式正常结束return true— 告诉 cpp-httplib 继续接收虽然后面不会有什么有效数据了6.7 解析 JSON 增量数据Json::Value modelDataJson;Json::CharReaderBuilder reader;std::string errors;std::istringstreammodelDataStream(modelData);if(Json::parseFromStream(reader,modelDataStream,modelDataJson,errors)){和sendMessage()中的 JSON 解析逻辑完全一样Json::CharReaderBuilder— JSON 解析器构建器std::istringstream modelDataStream(modelData)— 将data:后面的 JSON 字符串包装成流Json::parseFromStream()— 解析 JSON成功返回true每个 SSE 事件中的 JSON 长这样{choices:[{delta:{content:你}}]}注意流式响应用的是**delta**而不是message对比全量响应sendMessage流式响应sendMessageStream字段路径choices[0].message.contentchoices[0].delta.content含义完整的回复内容本次增量内容一个字或几个字出现次数一次多次逐段返回6.8 多重检查提取增量内容if(modelDataJson.isMember(choices)modelDataJson[choices].isArray()!modelDataJson[choices].empty()modelDataJson[choices][0].isMember(delta)modelDataJson[choices][0][delta].isMember(content)){std::string contentmodelDataJson[choices][0][delta][content].asString();5 层检查逐层深入isMember(choices)— 有choices字段吗isArray()— 是数组吗!empty()— 数组不为空吗choices[0].isMember(delta)— 第一个元素有delta字段吗delta.isMember(content)—delta里有content字段吗这样做的原因流式响应的某些事件可能没有**content**字段。比如模型生成结束前的最后一个事件可能只包含finish_reason: stop而没有content。不做检查直接访问会崩溃。6.9 累积并回调fullResponsecontent;callback(content,false);fullResponse content— 将增量内容追加到完整回复字符串中最终作为函数返回值callback(content, false)— 调用回调通知上层收到一段新内容第一个参数还没结束第二个参数为false这两个操作是并行的一边把碎片拼成完整回复fullResponse一边实时通知上层callback。6.10 JSON 解析失败的容错}else{WARN(DeepSeekProvider sendMessageStream parse modelDataJson error: {},errors);}如果某个 chunk 的 JSON 解析失败只打一条 WARN 日志不中断流式接收。这是合理的因为个别事件格式异常不应影响整体流程后续事件可能仍然正常。6.11 content_receiver 返回returntrue;while 循环处理完 buffer 中所有完整事件后返回true告诉 cpp-httplib继续接收后续数据。如果 buffer 中还有不完整的数据没找到\n\n会留在 buffer 中等下次content_receiver被调用时拼接。七、发送请求与收尾7.1 发送请求autoresultclient.send(req);if(!result){ERR(Network error {},to_string(result.error()));return;}client.send(req)— 发送请求。与client.Post()不同send()接受一个Request对象会使用我们设置的response_handler和content_receiverresult—httplib::Result类型重载了operator bool()网络失败时DNS 解析失败、连接超时等result为falseresult.error()返回具体的错误类型7.2 确保流式正常结束if(!streamFinish){WARN(stream ended without [DONE] marker);callback(,true);}如果整个请求结束了但streamFinish仍然是false说明没有收到**[DONE]**标记。可能的原因网络中断数据没传完服务器异常提前关闭了连接不管什么原因都要调用callback(, true)通知上层流式已结束避免上层一直在等。7.3 返回完整回复returnfullResponse;fullResponse在content_receiver中逐段累积到这里包含了模型的完整回复文本和sendMessage()返回的内容一样只是获取方式不同一个是直接从响应体提取一个是逐步拼接。八、完整流程图sendMessageStream(messages, requestParam, callback) │ ├─ 1. 检查 isAvailable() │ ├─ 2. 构造 JSON 请求体stream: true │ ├─ 3. 创建 HTTP Client超时 300 秒 │ ├─ 4. 构造 Request设置两个回调 │ ├── response_handler → 检查 HTTP 状态码 │ └── content_receiver → 逐块处理 SSE 数据 │ ├─ 5. client.send(req) ──→ 请求发出 │ │ │ ├── 收到响应头 → response_handler │ │ └── status ! 200 → gotError true │ │ │ └── 收到每块 body → content_receiver │ │ │ ├── 检查 gotError → 有错则终止 │ ├── 追加到 buffer │ ├── while (找到 \n\n) │ │ ├── 提取 chunk │ │ ├── 空行/注释 → 跳过 │ │ ├── data: [DONE] → callback(, true), 结束 │ │ └── data: {json} → 解析 JSON │ │ └── choices[0].delta.content │ │ ├── fullResponse content │ │ └── callback(content, false) │ └── return true继续接收 │ ├─ 6. 检查 streamFinish │ └── 未收到 [DONE] → callback(, true) 兜底 │ └─ 7. return fullResponse累积的完整回复九、全量 vs 流式对比总结对比项sendMessage全量sendMessageStream流式请求体无stream字段stream: true读取超时60 秒300 秒请求头AuthorizationContent-Type多一个Accept: text/event-stream请求方式client.Post()client.send(req) 回调响应格式完整 JSON多个 SSE 事件\n\n分隔内容字段choices[0].message.contentchoices[0].delta.content结束标记无整个响应就是完整的data: [DONE]返回方式一次性返回完整文本callback 逐段回调 最终返回完整文本核心区别就一句话全量是等完再给流式是边收边给。十、总结sendMessageStream()的难点在于SSE 协议解析— 需要理解data:前缀、\n\n分隔符、[DONE]结束标记Buffer 缓冲机制— 网络传输的数据边界不确定必须缓冲拼接后按\n\n分割双重回调架构—response_handler检查状态码content_receiver逐块处理数据容错处理— 注释行过滤、JSON 解析失败不中断、缺少[DONE]的兜底回调后续将实现ChatGPT 和 Gemini 模型接入它们的整体结构与 DeepSeek 类似但在 API 端点、SSE 数据格式上有重要差异。
【C++ AI 大模型接入 SDK】 - DeepSeek 模型接入(下)
发布时间:2026/5/21 16:17:49
大家好我是Halcyon.平安欢迎文末添加好友交流共同进步一、本篇概述二、什么是 SSE2.1 SSE vs 普通请求2.2 SSE 数据格式三、与 sendMessage 的相同部分3.1 请求体多了 stream: true3.2 读取超时更长3.3 请求头多了 Accept四、流式处理变量五、构造 Request 对象5.1 为什么不用 client.Post()5.2 response_handler — 响应头处理器5.3 content_receiver — 数据接收处理器六、content_receiver 逐行解析6.1 错误检查6.2 追加数据到 buffer6.3 按 \n\n 分割并处理每个事件6.4 过滤无效事件6.5 提取 data 字段6.6 检查结束标记6.7 解析 JSON 增量数据6.8 多重检查提取增量内容6.9 累积并回调6.10 JSON 解析失败的容错6.11 content_receiver 返回七、发送请求与收尾7.1 发送请求7.2 确保流式正常结束7.3 返回完整回复八、完整流程图九、全量 vs 流式对比总结十、总结一、本篇概述上篇讲了initModel()和sendMessage()全量请求本篇聚焦sendMessageStream()流式请求。这是整个 SDK最复杂的函数涉及 SSE 协议解析、数据缓冲、逐块回调。sendMessageStream() 的工作方式 用户提问 → SDK 发起 HTTP 请求 → DeepSeek 边生成边返回 → SDK 逐块解析 → 每解析出一段文字就通过 callback 通知上层 → 上层ChatServer立即推送给前端 → 用户看到打字机效果二、什么是 SSE在深入代码之前需要先理解SSEServer-Sent Events服务器推送事件。2.1 SSE vs 普通请求普通 HTTP 请求sendMessage 全量 客户端 ──请求──→ 服务器 客户端 ←──等待──────── 客户端 ←──────────── 完整响应一次性 SSE 流式请求sendMessageStream 客户端 ──请求──→ 服务器 客户端 ←──chunk1── 你 客户端 ←──chunk2── 好 客户端 ←──chunk3── 客户端 ←──chunk4── 我是AI助手 客户端 ←──[DONE]── 结束2.2 SSE 数据格式SSE 是一种文本协议每条消息用\n\n两个换行分隔。DeepSeek 返回的原始数据大概长这样data: {choices:[{delta:{content:你}}]}\n\n data: {choices:[{delta:{content:好}}]}\n\n data: {choices:[{delta:{content:}}]}\n\n : comment\n\n data: {choices:[{delta:{content:我是AI助手}}]}\n\n data: [DONE]\n\n规则每条消息以data:开头以:开头的是注释行需要忽略data: [DONE]是结束标记每条消息之间用\n\n分隔三、与 sendMessage 的相同部分sendMessageStream()的前半部分构造请求参数、历史消息、请求体和sendMessage()几乎一样只有两处区别3.1 请求体多了 “stream”: truerequestBody[stream]true;这一行告诉 DeepSeek API请用流式方式返回结果。如果设为false或不设就返回完整结果。3.2 读取超时更长// sendMessage 全量 client.set_read_timeout(60, 0); // 60秒 // sendMessageStream 流式 client.set_read_timeout(300, 0); // 300秒流式响应是边生成边传输的模型生成一段文字就发一段整个过程的持续时间远比全量请求长。如果设太短长文本生成还没结束就超时了。3.3 请求头多了 Accept{Accept,text/event-stream}告诉服务器客户端期望接收 SSE 格式的流式数据。四、流式处理变量在发送请求之前先声明一组用于流式处理的变量std::string buffer;// 接受流式响应的数据块boolgotErrorfalse;// 标记响应是否成功std::string errorMsg;// 错误描述符intstatusCode0;// 响应状态码boolstreamFinishfalse;// 标记流式响应是否完成std::string fullResponse;// 累积完整的响应变量类型作用bufferstring数据缓冲区。网络传输中一次 recv 可能收到不完整的数据需要缓冲拼接gotErrorbool一旦 HTTP 状态码非 200设为true后续接收器直接终止errorMsgstring存储错误描述如HTTP status code: 401statusCodeint存储响应状态码本代码中声明了但未实际使用streamFinishbool收到[DONE]标记后设为true用于最后检查流是否正常结束fullResponsestring累积所有增量文本最终作为函数返回值返回给调用方为什么需要 buffer网络传输的特点是一次 recv 调用可能收到任意数量的数据。可能一次收到半个 SSE 事件也可能一次收到三个完整事件。所以需要一个缓冲区来拼接数据等凑够一个完整事件找到\n\n分隔符再解析。第 1 次 recv: data: {\choices\:[{\delta\:{\content\:\你\}}]}\n\nda ↑ 分隔符 ↑ 不完整 buffer 中: data: {\choices\:[{\delta\:{\content\:\你\}}]}\n\nda ├────── 完整事件可以解析 ──────┤└─ 留在 buffer 等下次 ─┘ 第 2 次 recv: ta: {\choices\:[{\delta\:{\content\:\好\}}]}\n\n 与 buffer 中残留的 da 拼接成完整事件五、构造 Request 对象5.1 为什么不用 client.Post()上篇的全量请求直接用client.Post()它是一次性发送请求、等待完整响应。但流式请求需要边接收边处理所以要用更底层的httplib::Request对象手动设置两个回调httplib::Request req;req.methodPOST;req.path/v1/chat/completions;req.headersheaders;req.bodyrequestBodyStr;字段赋值说明methodPOSTHTTP 方法path/v1/chat/completions请求路径headers之前构造的 headers包含认证、内容类型、SSE 接受头body序列化后的 JSON 字符串请求体和client.Post(path, headers, body, type)的参数是对应的只是拆成了结构体的字段。5.2 response_handler — 响应头处理器req.response_handler[](consthttplib::Responseres){if(res.status!200){gotErrortrue;errorMsgHTTP status code: std::to_string(res.status);returnfalse;// 终止请求}returntrue;// 继续接收后续数据};这个回调在收到 HTTP 响应头时被调用注意此时还没收到 body 数据[]— Lambda 按引用捕获所有外部变量这样可以修改gotError和errorMsgres.status ! 200— 如果状态码不是 200比如 401 认证失败说明请求出错了gotError true— 设置错误标记后续content_receiver会检查这个标记return false— 告诉 cpp-httplib终止请求不需要继续接收 body 数据了return true— 状态码正常继续接收后续的 body 数据执行时序client.send(req) │ ├── TCP 连接建立 ├── 发送请求数据 ├── 收到响应头 ──→ response_handler 被调用 │ ├── status ! 200 → gotErrortrue, return false → 终止 │ └── status 200 → return true → 继续 │ ├── 收到第 1 块 body ──→ content_receiver 被调用 ├── 收到第 2 块 body ──→ content_receiver 被调用 ├── ... └── 接收完毕 ──→ client.send() 返回5.3 content_receiver — 数据接收处理器这是流式处理的核心每收到一块数据就会被 cpp-httplib 调用req.content_receiver[](constchar*data,size_t len,size_t offset,size_t totalLength){四个参数的含义参数类型说明dataconst char*指向本次收到的原始数据的指针lensize_t本次收到数据的字节长度offsetsize_t当前数据在整个响应中的偏移量本代码未使用totalLengthsize_t响应体总长度本代码未使用下面逐块解析 content_receiver 的内部逻辑。六、content_receiver 逐行解析6.1 错误检查if(gotError){returnfalse;}入口先检查response_handler是否标记了错误。如果 HTTP 状态码不是 200gotError已经是true直接返回false终止接收。6.2 追加数据到 bufferbuffer.append(data,len);std::string::append(const char* s, size_t n)— 将data指向的前len个字节追加到buffer末尾。为什么不直接用buffer data因为data是原始的字节指针不一定以\0结尾。operator遇到\0就停了而append(data, len)精确地追加len个字节即使中间有\0。6.3 按 \n\n 分割并处理每个事件size_t pos0;while((posbuffer.find(\n\n))!std::string::npos){std::string chunkbuffer.substr(0,pos);buffer.erase(0,pos2);buffer.find(\n\n)— 在缓冲区中查找 SSE 事件分隔符\n\nstd::string::npos— 表示没找到是string::find失败时的返回值值为-1但类型是size_t即无符号最大值buffer.substr(0, pos)— 从位置 0 开始截取pos个字符得到一个完整的 SSE 事件文本buffer.erase(0, pos 2)— 从 buffer 中删除已处理的部分pos 2是数据长度加上\n\n的 2 个字符用 while 而不是 if 的原因一次 recv 可能收到多个完整事件需要循环处理直到 buffer 中没有完整的\n\n分隔的事件为止。buffer 内容: data: {...}\n\ndata: {...}\n\ndata: {...Incomplete ↑ 第 1 个 \n\n ↑ 第 2 个 \n\n while 循环 3 次 第 1 次提取 data: {...}删除已处理部分 第 2 次提取 data: {...}删除已处理部分 第 3 次find(\n\n) npos → 退出循环 buffer 中剩余 data: {...Incomplete 等待下次 recv 拼接6.4 过滤无效事件if(chunk.empty()||chunk[0]:){continue;}chunk.empty()— 空事件跳过chunk[0] :— SSE 协议规定以:开头的行是注释comment服务器可能用来维持连接心跳需要忽略可能收到的数据 \n\n → chunk 为空跳过 : keep-alive\n\n → chunk 以 : 开头跳过 data: {...}\n\n → 有效数据继续处理6.5 提取 data 字段if(chunk.compare(0,6,data: )0){std::string modelDatachunk.substr(6);chunk.compare(0, 6, data: )— 从 chunk 的第 0 个位置开始取 6 个字符与data: 比较。等于 0 表示匹配chunk.substr(6)— 去掉data: 前缀6 个字符得到后面的 JSON 数据等价于if(chunk.starts_with(data: )){// C20 写法std::string modelDatachunk.substr(6);}compare是 C98 就有的方法兼容性更好。6.6 检查结束标记if(modelData[DONE]){callback(,true);streamFinishtrue;returntrue;}modelData [DONE]— DeepSeek 在流式数据最后会发送data: [DONE]表示生成结束callback(, true)— 调用回调通知上层没有更多内容了第一个参数为空流式结束第二个参数为truestreamFinish true— 标记流式正常结束return true— 告诉 cpp-httplib 继续接收虽然后面不会有什么有效数据了6.7 解析 JSON 增量数据Json::Value modelDataJson;Json::CharReaderBuilder reader;std::string errors;std::istringstreammodelDataStream(modelData);if(Json::parseFromStream(reader,modelDataStream,modelDataJson,errors)){和sendMessage()中的 JSON 解析逻辑完全一样Json::CharReaderBuilder— JSON 解析器构建器std::istringstream modelDataStream(modelData)— 将data:后面的 JSON 字符串包装成流Json::parseFromStream()— 解析 JSON成功返回true每个 SSE 事件中的 JSON 长这样{choices:[{delta:{content:你}}]}注意流式响应用的是**delta**而不是message对比全量响应sendMessage流式响应sendMessageStream字段路径choices[0].message.contentchoices[0].delta.content含义完整的回复内容本次增量内容一个字或几个字出现次数一次多次逐段返回6.8 多重检查提取增量内容if(modelDataJson.isMember(choices)modelDataJson[choices].isArray()!modelDataJson[choices].empty()modelDataJson[choices][0].isMember(delta)modelDataJson[choices][0][delta].isMember(content)){std::string contentmodelDataJson[choices][0][delta][content].asString();5 层检查逐层深入isMember(choices)— 有choices字段吗isArray()— 是数组吗!empty()— 数组不为空吗choices[0].isMember(delta)— 第一个元素有delta字段吗delta.isMember(content)—delta里有content字段吗这样做的原因流式响应的某些事件可能没有**content**字段。比如模型生成结束前的最后一个事件可能只包含finish_reason: stop而没有content。不做检查直接访问会崩溃。6.9 累积并回调fullResponsecontent;callback(content,false);fullResponse content— 将增量内容追加到完整回复字符串中最终作为函数返回值callback(content, false)— 调用回调通知上层收到一段新内容第一个参数还没结束第二个参数为false这两个操作是并行的一边把碎片拼成完整回复fullResponse一边实时通知上层callback。6.10 JSON 解析失败的容错}else{WARN(DeepSeekProvider sendMessageStream parse modelDataJson error: {},errors);}如果某个 chunk 的 JSON 解析失败只打一条 WARN 日志不中断流式接收。这是合理的因为个别事件格式异常不应影响整体流程后续事件可能仍然正常。6.11 content_receiver 返回returntrue;while 循环处理完 buffer 中所有完整事件后返回true告诉 cpp-httplib继续接收后续数据。如果 buffer 中还有不完整的数据没找到\n\n会留在 buffer 中等下次content_receiver被调用时拼接。七、发送请求与收尾7.1 发送请求autoresultclient.send(req);if(!result){ERR(Network error {},to_string(result.error()));return;}client.send(req)— 发送请求。与client.Post()不同send()接受一个Request对象会使用我们设置的response_handler和content_receiverresult—httplib::Result类型重载了operator bool()网络失败时DNS 解析失败、连接超时等result为falseresult.error()返回具体的错误类型7.2 确保流式正常结束if(!streamFinish){WARN(stream ended without [DONE] marker);callback(,true);}如果整个请求结束了但streamFinish仍然是false说明没有收到**[DONE]**标记。可能的原因网络中断数据没传完服务器异常提前关闭了连接不管什么原因都要调用callback(, true)通知上层流式已结束避免上层一直在等。7.3 返回完整回复returnfullResponse;fullResponse在content_receiver中逐段累积到这里包含了模型的完整回复文本和sendMessage()返回的内容一样只是获取方式不同一个是直接从响应体提取一个是逐步拼接。八、完整流程图sendMessageStream(messages, requestParam, callback) │ ├─ 1. 检查 isAvailable() │ ├─ 2. 构造 JSON 请求体stream: true │ ├─ 3. 创建 HTTP Client超时 300 秒 │ ├─ 4. 构造 Request设置两个回调 │ ├── response_handler → 检查 HTTP 状态码 │ └── content_receiver → 逐块处理 SSE 数据 │ ├─ 5. client.send(req) ──→ 请求发出 │ │ │ ├── 收到响应头 → response_handler │ │ └── status ! 200 → gotError true │ │ │ └── 收到每块 body → content_receiver │ │ │ ├── 检查 gotError → 有错则终止 │ ├── 追加到 buffer │ ├── while (找到 \n\n) │ │ ├── 提取 chunk │ │ ├── 空行/注释 → 跳过 │ │ ├── data: [DONE] → callback(, true), 结束 │ │ └── data: {json} → 解析 JSON │ │ └── choices[0].delta.content │ │ ├── fullResponse content │ │ └── callback(content, false) │ └── return true继续接收 │ ├─ 6. 检查 streamFinish │ └── 未收到 [DONE] → callback(, true) 兜底 │ └─ 7. return fullResponse累积的完整回复九、全量 vs 流式对比总结对比项sendMessage全量sendMessageStream流式请求体无stream字段stream: true读取超时60 秒300 秒请求头AuthorizationContent-Type多一个Accept: text/event-stream请求方式client.Post()client.send(req) 回调响应格式完整 JSON多个 SSE 事件\n\n分隔内容字段choices[0].message.contentchoices[0].delta.content结束标记无整个响应就是完整的data: [DONE]返回方式一次性返回完整文本callback 逐段回调 最终返回完整文本核心区别就一句话全量是等完再给流式是边收边给。十、总结sendMessageStream()的难点在于SSE 协议解析— 需要理解data:前缀、\n\n分隔符、[DONE]结束标记Buffer 缓冲机制— 网络传输的数据边界不确定必须缓冲拼接后按\n\n分割双重回调架构—response_handler检查状态码content_receiver逐块处理数据容错处理— 注释行过滤、JSON 解析失败不中断、缺少[DONE]的兜底回调后续将实现ChatGPT 和 Gemini 模型接入它们的整体结构与 DeepSeek 类似但在 API 端点、SSE 数据格式上有重要差异。