codecamp

Grok SSE 事件怎么消费

流式最常见的错误,是把「一个事件」当成「一次回答」。有人取第一个 chunk 的 delta.content 打印出来,看到只有半个词,认定流式坏了;也有人把 data: [DONE] 也丢进 JSON 解析,直接抛异常。还有一类更隐蔽:推理模型先吐 reasoning_content、后吐 content,代码只认后者,于是「开头那段思考去哪了」成了新问题。

这一节把事件流的消费拆成三步:切分、拼接、判停。

一、问题不是「能不能流」,而是「事件怎么切」

按 2026-11-04 官方 Streaming 页,流式输出用 Server-Sent Events(SSE),请求里设 "stream": true 即可开启。服务端把内容按增量推回来,每条事件是一行 data: {...},最后以 data: [DONE] 结束。

三个前提决定了消费代码怎么写:

  • 文本输出模型都支持流式,图像输出模型不支持。这是模型能力边界,不是参数问题。
  • 增量是碎片,不是句子。一条事件可能只有一两个字,拼接是消费方的责任。
  • 推理与正文是两条增量流。官方示例里,第一条事件的 delta 带的是 reasoning_content,之后才出现 content。

所以消费循环要同时处理「两条流」和「一个终止符」,缺一个都会出错。

二、最小可运行示例:SSE 消费循环

下面的脚本不联网、不需要真实密钥。它用一段 mock 事件流模拟服务端推送,把推理增量与正文增量分别拼起来,并读取最后一条的用量。

"""SSE 消费:切分事件、拼接增量、识别终止符。"""

import json

def iter_sse(lines):
    """从 SSE 文本行里取出 data 负载;遇到 [DONE] 结束。"""
    for line in lines:
        line = line.strip()
        if not line or line.startswith(":"):
            continue          # 空行与注释行不是数据
        if not line.startswith("data:"):
            continue
        payload = line[5:].strip()
        if payload == "[DONE]":
            return
        yield payload

def consume(lines):
    """把事件流拼成 (推理增量, 正文增量, 用量)。"""
    reasoning, content, usage = [], [], None
    for payload in iter_sse(lines):
        chunk = json.loads(payload)
        if chunk.get("usage"):
            usage = chunk["usage"]
        choices = chunk.get("choices") or []
        if not choices:
            continue          # 纯用量或错误事件:不打断整条流
        delta = choices[0].get("delta", {})
        if delta.get("reasoning_content"):
            reasoning.append(delta["reasoning_content"])
        if delta.get("content"):
            content.append(delta["content"])
    return "".join(reasoning), "".join(content), usage

MOCK = [
    'data: {"choices":[{"delta":{"reasoning_content":"先判断问题类型。"}}]}',
    'data: {"choices":[{"delta":{"content":"数据库"}}]}',
    'data: {"choices":[{"delta":{"content":"索引"}}]}',
    'data: {"usage":{"total_tokens":42}}',   # 纯用量事件:没有 choices,不能打断流
    'data: {"choices":[{"delta":{"content":"能加速查询。"}}],"usage":{"total_tokens":42}}',
    'data: [DONE]',
]

def offline_check() -> None:
    reasoning, content, usage = consume(MOCK)
    print("推理增量:" + reasoning)
    print("正文增量:" + content)
    print(f"用量:{usage}")

if __name__ == "__main__":
    offline_check()

真正上线时,把 lines 换成 HTTP 响应体的逐行迭代即可;consume() 的骨架不用改。注意 mock 里专门放了一条没有 choices 的纯用量事件,用来验证这类事件不会打断整条流——真实流量里这种事件一定会出现,而且位置取决于你用的客户端:xAI SDK 每个块都带累计费用,走 OpenAI SDK 或裸 REST 时则必须显式传 stream_options: {"include_usage": true},否则用量只在最后那条 choices 为空的块里出现一次,中间增量块完全没有。

三、事件结构的三处易错点

位置 正确理解 常见误写
delta.reasoning_content 推理增量,先于正文出现 只读 delta.content,丢掉思考过程
delta.content 正文增量,需逐条拼接 把单个 chunk 当完整回答
data: [DONE] 终止信号,不是数据 丢进 json.loads 导致解析异常

三件要强调的事:

  • 两条流要分开存。推理内容通常不直接展示给终端用户,但它是多轮续接的关键;正文才是要渲染的部分。混在一个字符串里,后面既不好展示也不好裁剪。
  • 终止符要显式处理。[DONE] 之后不应再有数据;如果循环没退出,轻则多解析一次报错,重则把异常吞掉、看起来像「流没结束」。
  • 接口形态不同,事件结构也不同。官方 Streaming 页给出的是 Chat Completions 的事件示例(choices[].delta);Responses API 的流式事件是另一套结构,本节不臆测其字段名,具体以官方页当前示例为准。

四、预期输出与异常分支

运行消费循环,预期输出:

推理增量:先判断问题类型。
正文增量:数据库索引能加速查询。
用量:{'total_tokens': 42}

换成真实流时,若出现异常,按下面顺序定位:

现象 优先怀疑 第一步动作
正文只有半句 循环提前退出 打印收到的事件条数与是否见到 [DONE]
解析报错 把 [DONE] 当 JSON 在解析前先判终止符
思考内容丢失 只读 content 同时收集 reasoning_content
用量为空 只在最后一条取 改为「每条有新值就覆盖」

最后一行尤其重要:用量字段不是每条都保证有,取「最后一个非空值」比取「最后一条」稳。这个细节不显眼,但对账时会直接影响能不能把本地记录对上账单(见 5.1)。

五、判据卡

判断项 该这么做 不该这么做
事件切分 只认 data: 行,跳过空行与注释 整段响应一次性 json.loads
增量拼接 推理与正文分开累加 把单个 chunk 当答案
终止处理 见 [DONE] 立即结束 把终止符当数据解析
用量读取 取最后一个非空值 假定每条都带 usage
接口形态 Chat 与 Responses 分别对照官方示例 用一套字段套两个接口

这张卡的重点是先切对,再拼对。四类现象里「只有半句」和「解析报错」常常同源:终止符与循环边界没处理好,表现一个是内容不全一个是直接崩,但修法都在消费循环的头尾。

六、来源与核验

  • 官方 Streaming 页:https://docs.x.ai/developers/model-capabilities/text/streaming(核验日期 2026-11-04)。SSE、stream: true、事件示例(delta.reasoning_content / delta.content / [DONE])、文本模型支持而图像模型不支持的说明,均以该页为准。
  • 官方示例同时给出的多语言 SDK 写法(AISDK、OpenAI SDK、xAI SDK、cURL)说明事件结构一致;本节只取字段层面可核验的部分。
  • 官方成本追踪页:https://docs.x.ai/developers/cost-tracking(核验日期 2026-11-04)。流式下 stream_options.include_usage 的必要性、用量只落在最后一个空 choices 块、xAI SDK 每块携带累计值,均以该页为准(F56)。
  • Responses API 的流式事件结构不在该页示例范围内,本节不作断言;具体字段以官方页当前示例为准。

七、动手试

  1. 给 consume() 加一个「首 token 时间」的记录:在收到第一条带 content 的事件时打时间戳,并与请求发出时间相减。想一想:这个指标能帮你区分「模型慢」还是「网络慢」吗?
  2. 把 mock 事件流改成「推理增量与正文增量交替出现」,验证两条流的拼接是否仍然正确,并说明为什么不能假设「推理一定全部先到」。
  3. 写一个容错分支:当某条 data: 负载不是合法 JSON 时,跳过并计数,而不是让整个流崩掉。想一想:这个计数在什么情况下应该告警?
Grok 账单对账与异常定位
Grok 流式三类中断故障
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }