流式输出实战:SSE 解析、chunk 拼装与断流重试

流式输出实战:SSE 解析、chunk 拼装与断流重试

结论先放这儿

事项 要点
SSE 格式 data: 前缀逐行读,[DONE] 收尾,冒号开头是心跳注释
usage 统计 请求参数带 include_usage,最后一个 chunk 里给
断流重试 首字节前可安全重试,输出中途断了重试会重复计费
前端渲染 按 50-100ms 节流重绘,别每个 token 都刷

流式是体验工程的第一优先级,首 token 延迟比总时长更影响体感。这篇把流式接入的工程细节补齐,与语音篇的流水线、扛量篇的限流配着看。

一、SSE 长什么样

流式接口返回的是一条持续打开的 HTTP 连接,服务端逐块推数据,每块都是一小段 JSON。OpenAI 兼容接口的每一块长这样:

复制代码
data: {"choices":[{"delta":{"content":"你"}}]}

data: {"choices":[{"delta":{"content":"好"}}]}

data: [DONE]

三件事要注意。每块以 data: 开头,解析时按行切、去掉前缀再 json.loads。结尾的 [DONE] 是文本不是 JSON,要单独判断。还有一类以冒号开头的行(: keep-alive),是 SSE 规范里的心跳注释,直接跳过。

二、chunk 拼装与 usage

每块 delta 里只有增量文本,要自己拼。拼接的同时统计 token 数:

python 复制代码
import json, httpx

def stream_chat(messages):
    content = []
    usage = None
    with httpx.stream('POST', BASE_URL + '/chat/completions',
                      headers=HEADERS, json={
                          'model': MODEL, 'messages': messages,
                          'stream': True,
                          'stream_options': {'include_usage': True},
                      }, timeout=120) as r:
        for line in r.iter_lines():
            if not line.startswith('data:'):
                continue
            payload = line[5:].strip()
            if payload == '[DONE]':
                break
            chunk = json.loads(payload)
            if chunk.get('usage'):
                usage = chunk['usage']       # 最后一块才有
            for choice in chunk.get('choices', []):
                delta = choice.get('delta', {}).get('content')
                if delta:
                    content.append(delta)
                    yield delta
    print('usage:', usage)

两个细节容易踩。usage 挂在最后一个 chunk 里,且要显式传 include_usage 才有,不传就永远拿不到对账数据。choices 可能为空列表(只有 usage 的那块),遍历前判一下。

三、断流重试的重试边界

流式的重试比普通请求危险。连接还没建立、首字节还没到的时候重试是安全的,什么都没发生。输出到一半断了,重试等于从头再生成一次,用户看到内容闪重,账单计两次。

所以重试逻辑按阶段分。首字节超时,放心重试。输出中途断,有两种处理,要完整答案就带着已拼内容重新请求(让模型从已有内容续写),要体感就只提示用户重发。对账以 usage 为准,重试导致的双份计费要在日志里能对出来。

python 复制代码
def stream_with_retry(messages, retries=2):
    for i in range(retries + 1):
        try:
            first = None
            gen = stream_chat(messages)
            for delta in gen:
                if first is None:
                    first = delta          # 首字节到了,之后断流不再整体重试
                yield delta
            return
        except httpx.ReadTimeout:
            if first is not None:
                raise                      # 输出中途断,交给上层决定
            if i == retries:
                raise

四、前端渲染别每个 token 都刷

拿到 delta 直接往 DOM 里塞,Markdown 渲染会闪,半个代码块会错位。两个惯用做法。按时间节流,50-100ms 批量刷一次。按句渲染,攒到标点再重绘,顺带给 TTS 之类的下游切句用。

写在最后

流式接入三件事,SSE 逐行解析(心跳注释和 [DONE] 单独处理),增量拼接加 usage 对账,重试按阶段分层(首字节前安全,输出中途要小心)。配上节流渲染,体感和账单都稳。

本文方法与代码按自己的项目实践整理,与语音篇的流水线、扛量篇的对账配着用。

相关推荐
天天被压力8 小时前
【A股分钟级高频数据完全手册 #04】指数分钟级行情数据接口:沪深指数Pro版最新分时取数
java·人工智能·python
日日大王学编程9 小时前
2026主流AI Agent产品分类、核心差异与技术特性全解析
人工智能·后端·python
天天被压力9 小时前
【A股分钟级高频数据完全手册 #03】股票历史1分钟K线数据接口怎么用:Pro版批量拉取与本地缓存
java·人工智能·python
小lu飞18 小时前
2026小红书小工具怎么制作:纯前端 ZIP 打包、沙箱限制与笔记挂载流程
前端·笔记·ai·ai编程
Ramble_Naylor1 天前
Vercel 全栈应用落地与场景化实战指南
ai
武天旭1 天前
AI安全--AI大模型的毒性偏见与安全对齐实践
人工智能·python·深度学习
视觉小萌新1 天前
AI Class 3 · Chapter 2-3:函数、异常、文件读写+JSON / JSONL 读写(小白版)
人工智能·python·json
小七-七牛开发者1 天前
Agent 支线知识:Personal Agent 如何跨应用完成任务?
ai·agent·工具调用·aiagent·mcp
小小张说故事1 天前
Python 的 obj.x 到底按什么顺序找?拆开数据描述符、实例字典和 __getattr__ 那条优先级链
python
vivo互联网技术1 天前
面向技术产品经理的 Agent Harness 设计:从任务编排到可验证交付
ai·产品经理·agent设计·harness engineering·权限策略