自己解析 DeepSeek 流式接口时,响应里出现“: keep-alive”,随后 json.loads 报错,不一定是模型输出了坏 JSON。本文针对 Python 客户端逐行解析服务器流的场景,用离线事件样本区分注释、事件数据和结束标记。依据 2026 年 10 月 14 日官方资料整理,未在读者网络实测;先修传输解析,再观察真实调用。
1. 保活消息不是模型正文
SSE 是 Server-Sent Events,即服务器发送事件的文本流格式。HTTP 响应开始后,服务端可以继续发事件;你收到的一行不必是一份完整 JSON,也不一定是模型生成的文字。
DeepSeek 当前官方说明,等待模型响应期间可能发送流式保活注释,非流式请求则可能出现空行。DeepSeek 保活机制 因而“每读到一行就直接 JSON 解码”的实现缺少协议层判断。
最短修复是把 SSE 分帧放在 JSON 解码之前:注释行跳过,data 字段收集,空行触发一帧结束;拿到事件数据后,再检查是否为终止标记或 JSON。不要把整个响应拼成一段聊天正文,也不要把所有解析异常都悄悄吞掉。
WHATWG 规范把冒号开头的行定义为注释,多个 data 行属于同一事件,事件由空行分隔。SSE 事件流规范 网络层一次返回的字节块也不等于一行,读取工具需要先正确解码和拆行。
2. 先用离线样本修好解析层
以下代码不调用网络,不需要 API Key,可在本地 Python 3 运行。输入是一组已解码且移除换行符的行,样本中的 choices 只是教学事件内容,不代表本文已经收到真实模型响应。
importjsondefdata_frames(lines):fields=[]forlineinlines:ifline=="":iffields:yield"\n".join(fields)fields=[]continueifline.startswith(":"):continuename,separator,value=line.partition(":")ifseparatorandvalue.startswith(" "):value=value[1:]ifname=="data":fields.append(value)sample=[": keep-alive","",'data: {"choices":[{"delta":{"content":"演示文本"}}]}',"","data: [DONE]","",]fordataindata_frames(sample):ifdata=="[DONE]":print("流结束")breakevent=json.loads(data)print("收到JSON事件:",event)预期跳过保活注释、解码一帧 JSON、识别结束标记。函数没有实现浏览器 EventSource 的完整重连与事件编号行为;它是一个专用于展示 data 分帧的最小教学实现。生产环境优先使用成熟客户端,不从这段代码推导完整协议兼容承诺。
3. 读完连接和任务完成不是一回事
连接 EOF,即数据读取结束,不一定表示模型正常生成完毕。网络中断也会产生 EOF。教学解析器只在空行处交付完整 data 帧,最后没有结束空行的数据不应为了凑一个回答被强行拼接为成功事件。
应用还需要按照供应商实际响应判断结束状态。本文样本使用常见的 [DONE] 标记展示终止处理,但真实调用仍要根据当前接口说明与客户端返回值验收。没有正常完成证据时,把结果标成中断,比显示“全部完成”更准确。
| 接收到的内容 | 解析层应做什么 | UI 可以显示什么 |
|---|---|---|
| 冒号开头注释 | 不送入 JSON 解码 | 请求仍在等待 |
| data 字段组成的完整帧 | 判断标记或解码对象 | 新的事件内容 |
| JSON 解码失败 | 留存脱敏诊断,停止或报错 | 响应格式异常 |
| 连接提前结束 | 不默认标成功 | 已中断,可保留部分内容 |
不要把“收到保活”当成“模型已经开始回答”。它说明连接上有数据活动,但不能证明当前已有正文。用户界面可以显示等待状态,避免制造一个与事实不符的进度百分比。
4. 仍然报错时,看原始事件类型
如果在第一帧就得到 HTML 或普通错误文本,先检查 HTTP 状态与 Content-Type,而不是继续套用 SSE 解码。请求可能没有进入正常事件流,代理错误页和认证错误响应都不是聊天 JSON 事件。
如果 data 字段本身是合法 JSON,却没有你期待的 choices,可能是错误事件或另一种结构。不要无条件访问 choices 的第一项;先识别类型,再按该类型处理。使用 SDK 时,优先遵循 SDK 已解析对象的结构,不要把对象再当成原始行重复解析。
如果中间才出现不完整 JSON,检查你是否把每个网络字节块当成完整事件。一个块可能包含半行,也可能包含多帧。缓冲区必须跨读取保存未结束的部分;本文示例从“已经拆好的行”开始,不能直接喂入随意切分的字节块。
如果保活一直有而正文迟迟没有,检查等待与超时设置,同时给任务设置你能接受的总等待边界。不要无限等待,也不要短时间并发提交同一个请求。官方等待机制有明确边界,但本机、代理和网关还可能更早结束连接,实际诊断应分别记录这些层。
5. 用协议样本与真实短请求双重验收
为离线解析准备四类样本:单条注释后接数据、多条注释没有数据、一个事件包含多个 data 行、数据最后被截断。前三类用于核对分帧规则,第四类用于确认不会伪造成功结果。多行 data 拼接后不保证一定是 JSON,仍由后续内容解码判断。
离线规则稳定后,再做一次短的真实调用,记录 HTTP 状态、内容类型、完整事件数量和正常结束证据。密钥放在本地环境,不打印请求认证头。调试日志最好记录帧长度和事件类型,而不是完整用户输入及模型输出。
需要辨认换行问题时,保存自行构造的原始样本,确认行结束符已被读取层处理。解析器里的空字符串代表空行,不代表某个时间点没有收到字节;这两者不能混用。若自行实现缓冲,应分别设置未完成帧的大小上限和任务等待时限,超过边界时明确报错,防止异常连接持续积累内存。读者应按客户端库的读取接口接入,不把示例函数直接当作网络下载函数。
若你原来使用官方兼容 SDK,只有自己写了代理或手工读取响应时才需要检查这一层。不要为了学习分帧而把已经稳定的客户端换成一套未经验证的解析器。先在测试路径对照,再决定是否改正式链路。
修复的结束条件是保活不再触发 JSON 错误、正常事件仍能处理、断流不会被当作完成。它解决的是传输格式识别,与模型是否输出结构化业务 JSON、如何展示思考内容是不同的问题,应分别验收。
参考资料
- Rate Limit & Isolation,DeepSeek,页面更新时间未显示。
- Server-sent events,WHATWG HTML 标准,核验时更新于 2026-10-13。访问于 2026-10-14。