Python消费Responses SSE事件:增量文本、超时与取消

用户问"公司报销流程怎么走",模型可能十几秒后才交出完整段落。命令行像卡住了一样,用户又按一次回车,后台便多出一次请求。今天用 Python 做一个最小答疑程序:文字到达就显示,等太久会结束,按 Ctrl+C 可以取消。学到的是网络流的消费方法,换模型、换前端仍然用得上。

为什么不是把普通请求的结果切成小段

普通请求的服务器响应在生成结束后才返回。流式请求让服务器使用 SSE(Server-Sent Events,服务器发送事件)逐个传递事件。response.output_text.delta 携带新文本,response.completed 表示正常结束,response.failed 指向服务端失败。收到事件不等于答案已经可靠完成:如果网络断开,屏幕上可能只有半句话。因此示例明确区分"显示过内容"和"已完成"。

这篇文章以近期 Python SDK 对 Responses 流快照处理的更新为引子,刻意使用标准库直接读 REST(表述性状态传输)接口。这样可看清最底层的事件边界;应用生产环境时,官方 SDK 通常能减少协议细节工作。模型用 gpt-5,因为官方快速入门有该模型的 Responses 调用示例;实际账户可用性仍需在运行前确认。场景只给一段公共示例政策,不能把真实内部政策硬编码到程序里。

flowchart LR A[用户输入问题] --> B[POST Responses stream=true] B --> C{读取SSE事件} C -->|output_text.delta| D[立即显示文本] C -->|completed| E[标记完整] C -->|failed或断线| F[提示答案不完整] D --> C

环境准备与完整代码

需要 Python 3.10 或以上;代码只用标准库,无第三方依赖。先在自己的终端设置 OPENAI_API_KEY。macOS/Linux 可运行 export OPENAI_API_KEY='你的密钥';Windows PowerShell 使用 $env:OPENAI_API_KEY='你的密钥'。这两个命令是本机操作说明,不要把真实密钥提交到仓库。将下段保存为 stream_qa.py,运行 python3 stream_qa.py。示例默认网络连接超时 10 秒,整体读取超时 45 秒。

python 复制代码
import json
import os
import socket
import sys
import urllib.error
import urllib.request

URL = "https://api.openai.com/v1/responses"
MODEL = "gpt-5"


def ask(question: str) -> None:
    key = os.environ.get("OPENAI_API_KEY")
    if not key:
        raise RuntimeError("请先设置 OPENAI_API_KEY")
    if not question.strip():
        raise ValueError("问题不能为空")

    payload = {
        "model": MODEL,
        "stream": True,
        "input": [
            {"role": "developer", "content":
             "只根据这段示例政策回答:报销须提交发票,并由直属主管审批。"
             "没有依据时明确说不知道。"},
            {"role": "user", "content": question},
        ],
    }
    request = urllib.request.Request(
        URL,
        data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
        headers={"Authorization": f"Bearer {key}",
                 "Content-Type": "application/json"},
        method="POST",
    )
    completed = False
    shown = False
    try:
        with urllib.request.urlopen(request, timeout=45) as response:
            for raw in response:
                line = raw.decode("utf-8", errors="replace").strip()
                if not line.startswith("data: "):
                    continue
                if line == "data: [DONE]":
                    break
                event = json.loads(line[6:])
                kind = event.get("type")
                if kind == "response.output_text.delta":
                    print(event.get("delta", ""), end="", flush=True)
                    shown = True
                elif kind == "response.completed":
                    completed = True
                elif kind in ("response.failed", "error"):
                    raise RuntimeError(f"服务端事件失败:{kind}")
    except urllib.error.HTTPError as exc:
        detail = exc.read(400).decode("utf-8", errors="replace")
        raise RuntimeError(f"HTTP {exc.code}: {detail}") from exc
    except (urllib.error.URLError, socket.timeout, TimeoutError) as exc:
        raise RuntimeError(f"网络或读取超时:{exc}") from exc
    finally:
        if shown:
            print()
    if not completed:
        raise RuntimeError("连接结束但没有完成事件;上方内容可能不完整")


if __name__ == "__main__":
    try:
        ask(input("请输入报销问题:"))
    except KeyboardInterrupt:
        print("\n已取消;请勿将已显示的片段当成完整答案", file=sys.stderr)
        sys.exit(130)
    except (RuntimeError, ValueError) as exc:
        print(f"失败:{exc}", file=sys.stderr)
        sys.exit(1)

逐段看关键逻辑

请求体里 stream: true 决定传输形态;用户问题与示例政策分为两个角色,便于后来把政策替换为检索结果。for raw in response 按行读取,空行和 event: 行被跳过,只有 data: 行进入 JSON(JavaScript Object Notation,结构化数据格式)解析。这是一个适合入门的 SSE 消费器;如果代理把超长事件拆成多行 data:,需要改成按空行聚合一整个事件。

每个 delta 都直接打印,flush=True 使终端立即刷新。completed 标志是完整性门槛:看见了字却没有结束事件,程序仍报错。按 Ctrl+C 时关闭上下文中的连接,并明确提醒用户片段不完整。这里的 timeout=45 是套接字等待超时,不是严谨的整个请求总时限;生产环境还要加应用层截止时间,以及请求标识和取消日志。

成功时会先出现逐字或逐块增长的答复,例如"按照示例政策,提交发票并请直属主管审批"。输出内容由模型生成,不保证逐字相同。没有密钥时应立即报"请先设置";若模型不可用,可能收到 404 或权限类错误。示例未在本次任务中实际调用线上API。

常见错误与边界

第一,401 常是密钥未设置或失效;先确认环境变量存在,别把密钥打印到日志。第二,429 表示限流或额度问题;减少并发或按响应头退避,不能无上限重试。第三,终端显示半句后结束:检查是否收到完成事件,网络错误时应在界面标记"未完成"。第四,如果回答引用了示例之外的政策,说明输入约束不足,业务系统还需检索证据和人工校验。

适用场景是长回答、互动问答和需要尽早展示进度的前端。不适用的是必须一次性交付完整且可验证 JSON 的机器间事务;这种场景应先拿到完整结果再校验。工程化时可将事件消费层与展示层分开,记录首字延迟、完成率、取消率,并对同一问题设置请求去重键。缓存也要区分"完整答案"和"中途片段",避免断线内容污染知识库。

把例子接到网页时,服务器应继续作为唯一持有密钥的一端,浏览器只连接自家后端。后端还要给每位用户设置并发上限。流式输出会占住连接,用户反复刷新页面可能耗掉连接池;这和模型推理费用是两种资源。先测并发请求的首字时间与完整结束率,再决定规模。

"停止显示"和"停止计费"也并非同一件事。按下取消后本地连接关闭,服务端是否已处理部分请求,取决于取消传播时机。日志最好保留自己的请求 ID、开始与取消时间、最后一个事件类型,便于排查重复请求;用户问题只在确有业务需要时留存。若用流式结果驱动工具,切勿把未闭合的 JSON 片段直接交给执行器。展示文本可以不完整,数据库写入却必须等参数完整、解析成功并通过权限校验。

5 分钟实践:把示例政策改为"报销须在 30 天内提交",问一个文档没有回答的问题,观察模型能否说不知道;然后按 Ctrl+C,确认程序不会输出"完成"标记。你的答疑产品更需要首字速度,还是完整答案的可靠性?

还有一处产品细节:用户看到文字逐步出现,容易以为模型已经"确定"了答案。实际上,流事件表达的是生成过程,不是审核过程。界面可以在进行中显示"草稿生成中",收到完整事件后再标记"回答结束";若后续还要做引用核对,应另设"证据已核验"状态。三个状态分开,能减少用户把半成品截图当正式结论的情况。对客服业务尤其如此,涉及报销期限、审批人的句子最好在后端查到现行政策版本后再展示。

处理中文时,终端输出看似按字出现,但事件并不保证一次一个汉字。网络缓冲、代理和模型生成节奏都会改变块大小,因此不要以块数估算 Token 或费用。也不要在每个小块后立即写数据库:高频写入会拖慢请求,还可能造成大量碎片记录。一个常见办法是把片段暂存在内存,每隔数百毫秒刷新一次界面,只在完整事件之后保存正式答案;断线片段可以单独存为排障资料并设置短保留期。

若公司网络通过代理访问 API,要把代理关闭连接、重试和超时行为纳入测试。连接在第 44 秒静默断开,与服务器返回明确的失败事件,在用户界面上都应显示"本次答案不完整",但排查路径不同。第一种应检查网络和代理日志,第二种看接口错误字段。把这两类失败混成"AI不会回答",会使团队不断改提示词,却解决不了真正的连接问题。

关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。


本文首发于 java4u.cn,转载请注明出处。

相关推荐
AI日报派送佬44 分钟前
2026年10月4日AI行业日报|AI智能体自主能力升级,模型落地与安全规范双向迭代
人工智能·智能体·ai安全·开源ai·ai日报·人工智能前沿·ai科研
蒲公英eric1 小时前
人口增长问题:从 Malthus 到 Logistic 的建模之旅
人工智能·python·数学建模·人口增长问题
知几蜗牛1 小时前
Python调用Audio Speech REST API:文本分段、超时与原子落盘
人工智能
打工仔折腾 AI1 小时前
把 AI Agent 托管到家里电脑:UU远程端口映射与CLI实测记录
人工智能·后端·python·langchain·电脑·ai agent 实战
枯木◊靠推文躺平版1 小时前
AI音乐成品优化工具怎么选:从 demo 听感到可发布状态
人工智能
YH55269841 小时前
怎么看 OpenAI 的 Pro 订阅取消 5x 和 20x 的描述?
人工智能·chatgpt
海盗12341 小时前
微软技术日报 2026-10-04:26H2 三个已知问题确认,Blazor 补上智能体 UI
人工智能·microsoft·ui·机器人·aigc
jimmyleeee1 小时前
大模型安全之四十:纵深防御----AI安全没有银弹
人工智能·安全
中年阿甘1 小时前
对人工智能方法的一点看法
人工智能