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与推理模型超时提示以该页为准;该页未给出断点续传字段,故本节不假设其存在。 - 三条处置路径与状态机为本书原创的工程框架;计费风险判断基于「重新生成会产生一次新的请求」这一通用事实,具体计费以控制台账单为准。
- 请求去重、幂等键等机制是否由平台提供,请以官方页与控制台当前显示为准;本节不虚构接口。
七、动手试
- 给
Stream加一个request_id字段,并写一段去重逻辑:当同一request_id的重试请求到达时,不再发起新生成。想一想:如果平台不提供幂等键,这个 ID 该由谁生成、怎么保证不重复? - 把
decide()改成「带预算感知」的版本:当本次重试的预估成本会让当日预算超限时,直接返回give_up,并说明为什么这比「先重试再告警」更稳。 - 设计一段用户提示:当只能保留不完整答案时,说明发生了什么、已经收到多少、用户可以选择继续还是重问。写出你会避免的两种表述——一种让人以为答案完整,一种让人以为内容丢失。