基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
简历
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-11-04 23:00

OpenAI 流式响应末尾的空值陷阱

dev.to · AI#OpenAI#流式响应#Python
Editor brief · 编辑速览

文章指出,从 OpenAI Python SDK 0.28 迁移至 1.x 时,直接拼接 delta.content 可能因空值报错,或把“None”写入回答。末尾块及工具调用块需要区别处理,启用 usage 返回后也应检查收尾逻辑。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

OpenAI Python SDK 从 0.28 升到 1.x,普通请求的报错通常很好定位,流式请求却可能等用户读完答案才失败。作者指出了两个容易漏掉的边界:delta.content 可能是 None,开启用量统计后,末尾还可能收到 choices 为空的块。

这篇的价值在于提醒我们:能持续输出文字,不代表正确处理了整个流。修复只需要几行,但测试必须覆盖收尾过程,否则演示正常、线上偶发报错的情况仍会出现。

为什么改成属性访问还不够

从 0.28 迁移到 1.x,最显眼的变化是调用入口从 openai.ChatCompletion.create 变成 client.chat.completions.create,响应读取也从 response["choices"][0]["message"]["content"] 变成 response.choices[0].message.content。

流式代码看起来也能照着改。旧代码常用 chunk["choices"][0]["delta"].get("content", ""),于是迁移后直接拼接 chunk.choices[0].delta.content。

问题是,属性存在,不代表值一定是字符串。

作者指出,携带 finish_reason 的结束块,以及携带工具调用而非文本的块,delta.content 可能是 None。直接拼接会触发 TypeError;如果用 f-string 或 str() 转换,异常可能消失,答案末尾却多出一个「None」。

这里还要补一个边界:Python 字典的 .get("content", "") 只会在键不存在时返回默认值。如果键存在、值就是 None,旧写法也不会自动兜底。因此,不能把这次问题简单理解成「字典安全,类型对象不安全」。

流式块是协议事件,不能默认每一块都是正文。

开启用量统计,为什么答案完整了还会报错

第二个坑由 stream_options={"include_usage": True} 触发。作者给出的调用方式如下,它用于让流返回用量统计:

stream = client.chat.completions.create(
    model=MODEL, messages=messages, stream=True,
    stream_options={"include_usage": True},
)

要留意 stream_options 这一行:按原文描述,开启后,最后的用量块带有 usage,但 choices 是空列表。

此时,循环如果直接读取 chunk.choices[0],就会触发 IndexError。前面的正文已经全部显示,服务端却把请求记成失败,排查时很容易误以为是收尾阶段的随机故障。

作者用下面这段循环同时处理空列表和空内容:

text = ""
for chunk in stream:
    if not chunk.choices:          # 跳过仅包含用量的最终块
        continue
    text += chunk.choices[0].delta.content or ""

关键是两道检查的顺序:先确认 choices 非空,再访问索引;随后用 or "" 处理 None。

不过,这只是一个文本汇总循环。它跳过了用量块,也没有处理工具调用。如果业务需要记录 token 用量或执行工具,还要在对应分支中读取和处理,不能把「没有文本」当成「没有用途」。

为什么只模拟一个块的测试会漏掉问题

作者认为,很多测试只手写一个包含正文的 mock 块,根本没有覆盖真实流的开头和结尾。这样的测试能验证字符串拼接,却无法验证完整的流处理。

他们主张读取真实流,或读取录制下来的完整流,一直消费到结束,再检查拼接结果。对这里的两个故障,至少要覆盖正文块、content=None 的块,以及开启用量统计后的空 choices 块。

另一个实际问题是代码复制。聊天接口、后台摘要任务、CLI 脚本里,可能各有一套循环。作者建议在整个仓库搜索 .delta.content 和 choices[0],修复所有相关位置,并在修改前后运行测试。

文末提到的 Versyka,宣称会自动完成这套搜索、改写和测试流程。原文没有给出运行记录、覆盖率或对照数据,不能据此判断它的迁移效果。

我的判断

这篇对边界条件的提醒很实用:用户已经看到完整答案,仍然不能证明请求成功结束。前端显示完成、后端消费结束、用量记录完成,可能发生在不同时间。

但原文没有交代具体的 1.x 小版本、模型和完整复现环境,也没有提供团队故障统计。它更适合作为代码审查线索,不能扩展成所有 OpenAI 流式接口的通用结论。

相比「只信真实流测试」,我更倾向于分层验证:用固定样本稳定覆盖空内容和空列表,再用真实流检查 SDK 集成。录制流便于重复验证,真实调用能发现协议变化,两者各有用途。直接读取最新真实流,也不能保证每次都遇到所有边界事件。

能拿来做什么

  • 维护 Python AI 服务时,全仓搜索 .delta.content 和 choices[0],逐处确认是否检查了空列表、是否允许内容为 None,同时检查摘要任务和 CLI。
  • 给流式汇总补上完整事件序列的测试,既断言最终文本正确,也确认消费到最后一个块时不会抛异常。
  • 为前端提供流式接口时,检查后端是否把正文结束和用量处理混在一起,避免文字已显示完整,收尾异常却又把页面切成失败状态。
  • 使用 AI 编程工具辅助 SDK 迁移时,把空内容、用量块和工具调用列入验收要求,并检查生成代码是否把需要保存的 usage 直接跳过。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
Kimi K3.1 Pro 据报开放模型权重
下一篇
AI 编程验收:独立复跑才能确认通过

资讯速览

dev.to · AI · 2026-11-04 23:00

AI 评分 8.0 / 10

相关标签

#OpenAI#流式响应#Python

继续阅读

  • 研究探索让大模型直接修改其他模型权重
  • Kimi K3.1 Pro 据报开放模型权重
  • AI 编程验收:独立复跑才能确认通过
  • 语音Agent选型先算链路与延迟
  • 让 AI 用独立证据核验任务完成状态