Grok WebSocket 模式怎么用:长连接续轮与断线重连
数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。
一句话说完:Grok 的 Responses API 除了走普通 HTTP,还可以挂在一条指向 /v1/responses 的长连接上跑,每一轮由客户端发一条 response.create 消息发起,后续轮次只带 previous_response_id 加本轮的新输入,服务端把上一轮的状态留在这条连接的内存里。这个设计真正值钱的地方不是「省了握手」,而是它让 store=false 和 ZDR 场景下也能做多轮链式续接——因为状态压根没落盘,就存在这条 socket 上。代价是连接一断,这份内存状态就没了,你必须自己写好重连分支,否则会直接撞上 previous_response_not_found。
它改的是哪一段
平时接 Responses API,一轮就是一个 HTTP 请求:建连接、发完整 body、收流式事件、连接关掉。下一轮再来一遍。就算你已经在用 previous_response_id 做链式续接,不需要重发整段历史,每轮的连接建立仍然是重复动作。
WebSocket 模式把这一层换掉:升级成 WebSocket 之后,这条连接一直开着,每一轮由客户端主动发一条 type: "response.create" 的消息发起。官方文档明确写了,这条消息的 body 形状和 Responses 的 create body 是一样的,只是去掉了纯传输层的字段——stream 和 background 不用写了,因为响应总是以事件流的形式回到这条 socket 上。
官方给这个模式圈定的场景也很具体:带大量顺序工具调用的 agent 型负载,比如编码 agent、编排循环,那种一轮一轮来回几十次的活儿。文档里说在内部基准上,这类负载的端到端延迟低于用同样 previous_response_id 链式续接的反复 HTTP 请求,幅度以官方文档当前版本为准。反过来说,如果你的调用是零散的单轮请求,长连接带来的复杂度并不划算。
开连接、发第一轮
官方给的 Python 示例走的是不带 SDK 的裸 WebSocket 客户端,鉴权信息放在 upgrade 请求的 header 里:
import json
import os
from websocket import create_connection
ws = create_connection(
"wss://api.x.ai/v1/responses",
header=[
f"Authorization: Bearer {os.environ['XAI_API_KEY']}",
],
)
ws.send(json.dumps({
"type": "response.create",
"model": "grok-4.6",
"store": False,
"input": [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Find fizz_buzz()"}],
}
],
"tools": [],
}))
这里有个容易被忽略的点:按官方示例,密钥是在 upgrade 握手请求的 header 里带的,不是每条 response.create 消息各带一次。示例里用环境变量取 key 是官方写法,具体到你自己的服务,密钥怎么下发、怎么轮换,可以参考API Key 安全管理的通用做法。
JavaScript 侧官方给的是 ws 包,形状一样,鉴权信息也在同一个位置——new WebSocket() 的第二个参数里放 headers.Authorization,然后 open 事件里发第一条 response.create,message 事件里 JSON.parse 收回来的事件。除了语言不同,两个示例的请求体字段完全对齐:type、model、store、input、tools。
用 generate: false 做预热
这个字段值得单独说。如果你已经知道下一轮要用哪些工具、哪段 instructions、什么 system message,可以先发一条带 generate: false 的 response.create。按官方文档的说法,服务端会把请求状态准备好但不跑模型,不产生输出;这次预热同样会给出一个 response ID,后面真正要生成的那一轮用 previous_response_id 挂上去,就能起步更快。
对编码 agent 这类工具定义又长又稳定的场景,这是个挺实用的口子:连接刚建立、用户还没提问的空档就可以先把工具集推上去。工具定义本身怎么写,见 Grok 的 function calling 与工具体系。
续轮:只发新东西
后续每一轮,官方要求你在 response.create 里带两样东西:
previous_response_id:这条链上一次响应的 ID;input:只放本轮的新条目,典型就是工具执行结果加上下一条用户消息。
文档里写得很直白:不要重发历史,服务端已经有了。所以一轮续接的 input 往往长这样——一个 function_call_output(带 call_id 和 output),后面跟一条新的 user message。
习惯了 Chat Completions 那种「每次把整个 messages 数组重新拼一遍」的写法,这里要改思维。你的客户端不再负责维护完整对话体,只负责维护「最后一个 response ID」这一个游标。代码会变简单,但也意味着这个游标丢了,整条链就断了。
状态到底存在哪:连接级缓存和 store 的两条分支
这一节是整个模式最需要想清楚的地方。
官方的说法是:previous_response_id 在 socket 上的行为和 HTTP 上一致,但 WebSocket 路径多了一个内存捷径——每条打开的连接会把它最近一次响应的状态放在一份 per-connection 缓存里。从这个响应继续,完全不碰存储。这正是它能和 store=false、和 ZDR 共存的原因。
于是当你引用的那个 previous_response_id 已经不在连接缓存里时,会分成两种结局:
store=true:服务端可能从持久化状态里把它重新加载出来,链还能继续,但那份内存捷径带来的收益就没了;store=false或开了 ZDR:没有可读的兜底存储,这一轮直接失败,返回previous_response_not_found。
顺带一个和安全策略相关的连接点。xAI 的安全 FAQ 里明确列过,开启 ZDR 之后有一批依赖存储的能力会不可用,其中就包括有状态的 Responses API(store_messages、previous_response_id)、Files API、Collections API、Batch API 和延迟补全,客户端必须自己保存对话历史。
把两处官方说法合起来看,事情就清楚了:连接级缓存这条路径压根不落盘,所以 WebSocket 模式那一节才会写它对 ZDR 和 store=false 都成立——「链式续接」在 ZDR 下仍然走得通,前提是这条链始终跑在同一条没断的连接上,并且总是从最近一次响应继续。安全 FAQ 在讲有状态 Responses API 那一行还补了一句提醒:涉及 agentic 工具调用状态时记得开 use_encrypted_content。做工具密集的 agent 循环,这个字段值得一并纳入你的 ZDR 检查清单。
还有一条容易漏的规则:一轮如果失败了(4xx 或 5xx),它的 previous_response_id 会被从连接缓存里逐出,这样重试就不会从一个已经坏掉的状态继续。这意味着你不能写「失败了原样重发一次」的傻重试——那次重发大概率会拿到 previous_response_not_found,得走重建上下文的分支。通用的重试与退避怎么设计,可以看429 与限流的通用处理。
一条连接同时只能跑一轮
官方在「连接限制与行为」里给了几条硬规则,都很具体:
- 事件类型和顺序与既有的 Responses 流式格式完全一致。也就是说你原来解析流式事件的代码可以直接搬过来,比如官方在文件问答示例里用的
response.output_text.delta; - 一条连接串行处理轮次。上一轮还在跑的时候再发一条
response.create,它会排队,不会并发多路复用; - 需要并行?开多条连接;
- 单条连接的存活时间有上限,到点服务端会主动关掉,你得重连。具体时长以官方文档当前版本为准。
第二条对做并发的人是硬约束:别指望一条 socket 当连接池用。要并行跑 N 条 agent 轨迹,就得有 N 条连接,而每条连接各自持有自己的那份最近响应状态,彼此不共享。
断了怎么接回去
官方把重连场景归了个类——网络抖动、发版重启、撞到连接存活上限,都算。开一条新连接之后按情况二选一:
- 如果你当初用的是
store=true,并且手上那个 response ID 还有效,那就在新 socket 上照常带previous_response_id和新输入继续; - 否则(比如
store=false,或者已经吃到了previous_response_not_found),把previous_response_id整个丢掉,重新起一条链,把下一轮需要的完整输入上下文一次性发过去。
第二条路径的隐含要求是:即使服务端替你存了状态,你自己也得留一份能重建上下文的东西。store=false 下这份历史只能在你的客户端里。很多人是在第一次线上断连时才发现自己没存,那时候已经晚了。写这类 agent 循环,建议从第一天就把「本地历史」和「服务端游标」两条线并行维护,正常情况下用游标,异常情况下用本地历史重建。
顺便说,长链条里重建上下文会把一大段 prompt 重新发一遍,这时候缓存机制的收益就显出来了,机制本身见 Grok 提示缓存是怎么工作的。
两个专属错误码
官方单独点名了两个 WebSocket 模式特有的错误,都值得写成显式分支。
previous_response_not_found:请求里的 previous_response_id 既不在连接缓存里、也没法从存储里恢复时返回,典型诱因是 ZDR、store=false,或者被前一次失败逐出了。返回体是 type: "error"、status: 400,error 里带 code、message 和 param: "previous_response_id"。看到它就走「重建完整上下文、开新链」的路径,别原地重试。
websocket_connection_limit_reached:在服务端关闭一条已达存活上限的连接之前发出来,error.type 是 invalid_request_error,message 里会告诉你连接已到上限、需要新建连接继续。它的语义不是「你错了」,而是「计划内的换连接通知」,所以处理姿势应该是无声重连,而不是报错给用户。
最容易栽的坑
按官方文档梳下来,这个模式的坑几乎都集中在一处:你以为服务端替你记住了对话,其实它只记住了一条连接上的最近一次。
- 别把
previous_response_id当成永久句柄。它在store=false/ZDR 下的寿命,等于这条连接的寿命,还得是最近一次; - 别写无脑重试。失败会逐出缓存条目,重发只会连着错两次;
- 别把连接当连接池。一条连接串行处理,并发靠多开;
- 重连逻辑不是可选项。存活上限是确定会发生的事件,不是异常。
如果你的负载不是「一轮接一轮的长 agent 循环」,说实话就别上这一层复杂度,普通 HTTP 加 previous_response_id 链式续接足够。它是给那种一次任务要来回几十次工具调用的场景准备的优化,跑三五轮的对话感知不到收益,却要多写一套重连和状态重建的代码。
至于其它托管路径能不能用这个模式——官方社区文档里讲 Google Cloud Vertex AI 时只说「模型可用性大体与 xAI API 一致,受 Google Cloud 区域可用性与配额约束」,并没有提到 WebSocket 模式,也就是说这一点官方文档里没有找到明确说明,要用的话建议直接向对应平台确认。