codecamp

Grok 断线重连与重放

断线之后最危险的动作,是「自动重连」四个字。很多实现把它写成:捕获异常,重新发一次同样的请求,把新流接到旧位置。结果是用户看到答案从中间重来一遍,账单上多了一次完整生成,而日志里只留下一条「已重连」。更糟的是,如果重试发生在计费之后,团队会把多出来的成本归因到「模型变贵」。

这一节把断线处置拆成三条路,并明确它们各自的代价。

一、问题不是「要不要重连」,而是「重连等于什么」

三条路看着都能「恢复」,但语义完全不同:

  • 不重连:直接失败,把已收内容交给用户。代价是体验差,成本最低。
  • 重放:从断点续传剩余部分。语义上仍是同一次生成,不重复计费——前提是服务端支持断点续传。
  • 重新请求:重新生成整段答案。语义上是第二次生成,会再次计费,且答案可能与第一次不同。

所以「重连」不是一个动作,而是三个选择。选错的核心后果有两个:重复计费与答案不一致。

二、最小可运行示例:断线处置状态机

下面的脚本不联网、不需要真实密钥。它根据「已收内容、是否见到终止符、是否支持重放、已重试次数」给出动作与风险等级。

"""断线处置(mock):不重连、重放、重新请求三选一,并标出计费风险。"""

from dataclasses import dataclass

@dataclass
class Stream:
    received_chars: int = 0
    saw_done: bool = False
    retries: int = 0
    replay_verified: bool = False   # 已确认服务端支持续传,且校验过不重复

def decide(t: Stream) -> tuple[str, str]:
    """返回 (动作, 计费风险)。"""
    if t.saw_done:
        return "done", "无"
    if t.received_chars == 0:
        return "retry_request", "低(未产生输出)"
    if t.replay_verified:
        return "replay", "低(续传,不重新生成)"
    if t.retries >= 1:
        return "give_up", "中(保留部分内容,提示用户)"
    return "retry_request", "高(重新生成会重复计费且答案可能不同)"

def offline_check() -> None:
    cases = [
        ("正常完成", Stream(100, True, 0, False)),
        ("零输出断线", Stream(0, False, 0, False)),
        ("有内容且支持重放", Stream(50, False, 0, True)),
        ("有内容不支持重放", Stream(50, False, 0, False)),
        ("已重试过一次", Stream(50, False, 1, False)),
    ]
    for i, (name, t) in enumerate(cases, 1):
        action, risk = decide(t)
        print(f"{i} {name}:{action}|{risk}")

if __name__ == "__main__":
    offline_check()

真正上线时,replay_verified 不能靠猜——它需要两个前提同时成立:服务端确实提供续传能力,且你已经校验过重放不会重复拼接。官方 Streaming 页没有给出断点续传的字段说明,所以本节的默认值是 False:在确认并校验之前,一律按「只能重新生成」处理,这样风险敞口是明确的。

三、三条路的触发条件与代价

场景 建议动作 计费风险 用户可见后果
已收到终止符 不重连 无 无
零输出就断 重新请求 低 基本无感
有内容且确认可续传 重放 低 从断点继续
有内容且不可续传 最多重试一次 高 可能整段重来
已重试过仍失败 放弃并保留 中 看到不完整提示

三件要强调的事:

  • 重放不等于重新生成。重放的前提是服务端保存了这次生成的中间状态;没有这个前提,「重放」只是把重新请求包装了一下。把它当成同一件事,就会在计费上算错。
  • 重试要设上限。一次断线重试可能是网络抖动,两次三次就是系统性故障。上限不设,最坏情况是一条请求把预算烧穿(护栏见 5.3)。
  • 重试前先固化已收内容。把当前文本、已收 chunk 数、最后一条事件写入状态,再决定动作;否则重试成功后旧内容被覆盖,用户会以为「答案变短了」。

四、预期输出与异常分支

运行状态机,预期输出:

1 正常完成:done|无
2 零输出断线:retry_request|低(未产生输出)
3 有内容且支持重放:replay|低(续传,不重新生成)
4 有内容不支持重放:retry_request|高(重新生成会重复计费且答案可能不同)
5 已重试过一次:give_up|中(保留部分内容,提示用户)

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

现象 优先怀疑 第一步动作
账单出现成对请求 重试未去重 对齐请求 ID 与时间戳
答案前后不一致 重新生成覆盖 固化首次内容,标记来源
重试次数异常高 未设上限 加计数器并在入口拦截
续传后内容重复 「重放」实为重新生成 核对服务端是否真支持断点

最后一行尤其重要:如果「续传」之后用户看到前文重复出现,那说明服务端并没有真正续传,而是把整段重新生成后又拼了上去。这时问题不在拼接,在前提判断错了。

五、判据卡

判断项 该这么做 不该这么做
已完成 不再重连 见超时就重发
零输出 可安全重试 直接放弃
有内容可续传 重放并校验不重复 重新生成整段
有内容不可续传 最多一次,保留并提示 无限自动重试
计费核对 重试前记录请求数与成本 假定重试免费

这张卡的重点是先判断能不能重放,再决定重不重试。四类现象里「账单成对」和「答案前后不一致」常常同源:把重新生成当成了重放,表现一个是钱一个是内容,但修法都在动作选择那一步。

六、来源与核验

  • 官方 Streaming 页:https://docs.x.ai/developers/model-capabilities/text/streaming(核验日期 2026-11-04)。SSE 事件、stream: true 与推理模型超时提示以该页为准;该页未给出断点续传字段,故本节不假设其存在。
  • 三条处置路径与状态机为本书原创的工程框架;计费风险判断基于「重新生成会产生一次新的请求」这一通用事实,具体计费以控制台账单为准。
  • 请求去重、幂等键等机制是否由平台提供,请以官方页与控制台当前显示为准;本节不虚构接口。

七、动手试

  1. 给 Stream 加一个 request_id 字段,并写一段去重逻辑:当同一 request_id 的重试请求到达时,不再发起新生成。想一想:如果平台不提供幂等键,这个 ID 该由谁生成、怎么保证不重复?
  2. 把 decide() 改成「带预算感知」的版本:当本次重试的预估成本会让当日预算超限时,直接返回 give_up,并说明为什么这比「先重试再告警」更稳。
  3. 设计一段用户提示:当只能保留不完整答案时,说明发生了什么、已经收到多少、用户可以选择继续还是重问。写出你会避免的两种表述——一种让人以为答案完整,一种让人以为内容丢失。
Grok 流式前端逐字渲染
Grok JSON Schema 输出约束
温馨提示
下载编程狮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; }