Responses API 支持 WebSocket 模式,适用于长时间运行且频繁调用工具的工作流。除了降低延迟,stream_id 还支持 WebSocket 多路复用:通过一个连接到 /v1/responses 的持久连接,即可并行运行多个对话,并将现有对话派生到新流中。每轮只需发送新的输入项和 previous_response_id 即可续接。
WebSocket 模式同时兼容零数据保留(ZDR)和 store=false。
为什么使用 WebSocket 模式
当工作流需要在模型和工具之间进行大量往返交互时,WebSocket 模式尤为适用,例如智能体编程或反复调用工具的编排循环。
由于连接保持打开,且每轮只发送增量输入,WebSocket 模式可以减少每轮续接的开销,降低长链路的端到端延迟。在包含 20 次及以上工具调用的运行中,我们观察到端到端执行速度最高提升了约 40%。
连接并创建响应
请安装 WebSocket 依赖项:Python 使用 pip install "openai[realtime]>=3.8.0",JavaScript 使用 npm install openai@^7.10.0 ws,Ruby 使用 gem install openai async-websocket。
在 WebSocket 模式下,从客户端发送 response.create 事件即可开始一轮交互。其载荷与常规的 Responses 创建请求体一致,但不使用 stream 和 background 等与传输方式相关的字段。
import OpenAI from "openai";
import { ResponsesWS } from "openai/resources/responses/ws";
const client = new OpenAI();
const ws = new ResponsesWS(client);
try {
ws.send({
type: "response.create",
stream_id: "main",
model: "gpt-6-astra",
store: false,
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "Find fizz_buzz()" }],
},
],
tools: [],
});
let completed = false;
for await (const event of ws) {
if (event.type === "error") throw event.error;
if (event.type !== "message") continue;
const message = event.message;
if (message.type === "response.output_text.delta") {
process.stdout.write(message.delta);
} else if (message.type === "response.completed") {
completed = true;
break;
} else if (
message.type === "response.failed" ||
message.type === "response.incomplete"
) {
throw new Error(JSON.stringify(message));
}
}
if (!completed)
throw new Error("Connection closed before the response finished.");
} finally {
ws.close();
}客户端可以选择发送带有 generate: false 的 response.create 来预热请求状态。如果您已经知道下一轮计划发送的工具、指令和/或自定义消息,这种方式会很有用。generate: false 不会返回模型输出,而是预先准备请求状态,让下一轮生成更快开始。预热请求会返回一个响应 ID,您可以通过 previous_response_id 从该响应续接,包括在响应链的后续轮次中使用这种方式。下一节将介绍如何使用 previous_response_id 和增量输入续接会话。
使用增量输入续接
要在响应仍在运行时添加用户指令,请使用轮次中途引导。引导会保留已完成的工作,并在续接时纳入新指令。对于常规的轮次间续接和工具结果,请使用下面的 response.create 模式。
要继续一次运行,请再发送一个 response.create,其中:
- 将
previous_response_id设置为上一条响应的 ID。 input仅包含新项,例如工具输出和下一条用户消息。
import OpenAI from "openai";
import { ResponsesWS } from "openai/resources/responses/ws";
const client = new OpenAI();
const model = "gpt-6-astra";
const tools = [
{
type: "function",
name: "get_test_results",
description: "Return a local demo test result.",
parameters: { type: "object", properties: {}, additionalProperties: false },
strict: true,
},
];
async function waitForResponse(ws) {
for await (const event of ws) {
if (event.type === "error") throw event.error;
if (event.type !== "message") continue;
const message = event.message;
if (message.type === "response.output_text.delta") {
process.stdout.write(message.delta);
} else if (message.type === "response.completed") {
return message.response;
} else if (
message.type === "response.failed" ||
message.type === "response.incomplete"
) {
throw new Error(JSON.stringify(message));
}
}
throw new Error("Connection closed before the response finished.");
}
const ws = new ResponsesWS(client);
try {
ws.send({
type: "response.create",
stream_id: "main",
model,
store: false,
input: "Find the failing test and suggest a fix.",
tools,
tool_choice: { type: "function", name: "get_test_results" },
parallel_tool_calls: false,
});
const first = await waitForResponse(ws);
const call = first.output.find((item) => item.type === "function_call");
if (!call || call.name !== "get_test_results") {
throw new Error("Expected a get_test_results function call.");
}
const result = {
test: "test_fizz_buzz",
failure: 'Expected "FizzBuzz" for 15, got "Fizz".',
};
// Continue on the same socket with the actual response and tool-call IDs.
ws.send({
type: "response.create",
stream_id: "main",
model,
store: false,
previous_response_id: first.id,
input: [
{
type: "function_call_output",
call_id: call.call_id,
output: JSON.stringify(result),
},
{ role: "user", content: "Now optimize it." },
],
tools,
tool_choice: "none",
});
await waitForResponse(ws);
} finally {
ws.close();
}续接的工作原理
WebSocket 模式采用与 HTTP 模式相同的 previous_response_id 链式关联语义,同时在活动套接字上提供延迟更低的续接路径。
在活动的 WebSocket 连接上,服务会将近期的先前响应状态保存在该连接专属的内存缓存中。使用 stream_id 时,每个通道都会保留其最新的缓存响应。从该通道的最新响应续接时,服务可以复用连接本地状态,因此速度很快。由于服务仅在内存中保留先前响应状态,不会将其写入磁盘,因此您可以以兼容 store=false 和零数据保留(ZDR)的方式使用 WebSocket 模式。
如果内存缓存中没有某个 previous_response_id,具体行为取决于您是否存储响应:
- 使用
store=true时,如果存在持久化状态,服务可能会从中恢复较早响应 ID 对应的状态。续接仍可进行,但不再享有内存缓存带来的延迟优势。 - 使用
store=false时(包括 ZDR),没有持久化状态可供回退。如果该 ID 未被缓存,请求会返回previous_response_not_found。
如果同一通道内的续接返回 4xx 或 5xx,服务会从连接本地缓存中移除所引用的 previous_response_id。跨通道派生返回错误时,则会保留共享的父响应,以便源通道继续运行。
压缩与创建新响应
如果您使用压缩,有两种不同的续接模式:
服务端压缩(context_management)
启用服务端压缩(context_management 配合 compact_threshold)后,压缩会在正常的 /responses 生成过程中进行。在 WebSocket 模式下,续接方式与平时相同:发送下一个 response.create,携带最新的 previous_response_id,并且仅包含新的输入项。
独立的 /responses/compact
独立的 /responses/compact 端点返回一个新的压缩后输入窗口,而不是响应 ID。压缩完成后,在您的 WebSocket 连接上创建新响应,将压缩后的窗口用作 input,并附上接下来的用户项或工具项。
省略 previous_response_id 或将其设置为 null,即可开始一条新链。请原样传入压缩后的输出,不要删减返回的窗口。
import { toResponseInputItems } from "openai/lib/responses/ResponseInputItems";
// Compact your current window with an HTTP request.
const compacted = await client.responses.compact({
model: "gpt-6-astra",
input: longInputItems,
});
const nextInput = toResponseInputItems(compacted.output);
nextInput.push({
type: "message",
role: "user",
content: [{ type: "input_text", text: "Continue from here." }],
});
// Start a new response on the WebSocket using the compacted window.
const ws = new ResponsesWS(client);
try {
ws.send({
type: "response.create",
stream_id: "main",
model: "gpt-6-astra",
store: false,
input: nextInput,
tools: [],
});
let completed = false;
for await (const event of ws) {
if (event.type === "error") throw event.error;
if (event.type !== "message") continue;
const message = event.message;
if (message.type === "response.output_text.delta") {
process.stdout.write(message.delta);
} else if (message.type === "response.completed") {
completed = true;
break;
} else if (
message.type === "response.failed" ||
message.type === "response.incomplete"
) {
throw new Error(JSON.stringify(message));
}
}
if (!completed)
throw new Error("Connection closed before the response finished.");
} finally {
ws.close();
}并行运行对话
您可以使用 stream_id 参数在同一连接上维持多个并行对话。连续发送独立的 response.create 事件,并为它们指定不同的 stream_id 值。服务端可以在一个连接上并发运行它们。各个对话的事件可能交错到达,因此请使用一个读取循环,并根据 stream_id 路由每个事件。
stream_id 用于标识一个 WebSocket 连接上的有序通道。请区分 stream_id 和 previous_response_id 的作用:
stream_id控制事件的去向,以及哪些请求按先进先出顺序运行。previous_response_id控制对话的继承关系。
这种分离支持两种实用模式。
one WebSocket connection
├─ stream_id="planner" draft a deployment plan
└─ stream_id="research" list deployment risks
具有相同 stream_id 的请求始终按先进先出顺序运行,且不会重叠执行。具有不同 stream_id 值的请求可以并发运行。
每个连接的限制
- 一个连接的命名通道和默认通道合计最多可有 16 个正在处理中的活动响应。连接可以接受更多
response.create事件,并将其排队,直到有活动响应完成。 - 一个连接最多接受 32 个不同的命名
stream_id值。隐式默认通道不计入此命名流数量限制。达到上限后,请复用现有的stream_id或打开新连接。
将对话派生到新流
要从已完成的响应创建分支,请将该响应的 ID 作为 previous_response_id 发送,同时指定一个新的 stream_id。只要该响应仍然可用,新流就会继承其上下文,而原始流可以继续运行。派生开始后,两个分支使用不同的流 ID,因此可以并发运行。
使用 store=false 时(包括 ZDR),跨通道派生依赖于父响应仍保留在连接本地缓存中。如果派生请求正在排队,而源通道继续推进或发生错误,父响应可能在派生开始前被移除,此时派生请求会返回 previous_response_not_found。请等待派生通道发出 response.in_progress 后再推进源通道,或者将 previous_response_id 设置为 null,重新发送完整输入上下文以重试。
main: resp_1 ──▶ resp_2 ──▶ resp_3
╲
critic: resp_4 ──▶ resp_5
复用 stream_id 但不提供 previous_response_id 会启动一个新响应,而不会续接对话。
关键调用如下:
# One socket, two independent conversations.
send_create(connection, "planner", "Draft a deployment plan.")
send_create(connection, "research", "List deployment risks.")
# Fork the planner response, then continue the original branch in parallel.
send_create(
connection,
"critic",
"Find gaps in this plan.",
previous_response_id=planner_response_id,
)
wait_for_in_progress(connection, "critic")
send_create(
connection,
"planner",
"Add rollback steps.",
previous_response_id=planner_response_id,
)
完整示例
import OpenAI from "openai";
import { ResponsesWS } from "openai/resources/responses/ws";
const client = new OpenAI();
const latestResponseIdByLane = new Map();
function sendCreate(
ws,
streamId,
text,
previousResponseId = latestResponseIdByLane.get(streamId)
) {
ws.send({
type: "response.create",
stream_id: streamId,
model: "gpt-6-astra",
store: false,
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text }],
},
],
previous_response_id: previousResponseId,
});
}
async function readMessage(events) {
while (true) {
const { value: event, done } = await events.next();
if (done)
throw new Error("Connection closed before all responses finished.");
if (event.type === "error") throw event.error;
if (event.type !== "message") continue;
const message = event.message;
if (
message.type === "response.failed" ||
message.type === "response.incomplete"
) {
throw new Error(
`Lane ${message.stream_id} failed: ${JSON.stringify(message)}`
);
}
return message;
}
}
async function drainUntilComplete(events, expectedStreamIds) {
const remaining = new Set(expectedStreamIds);
while (remaining.size > 0) {
const message = await readMessage(events);
const streamId = message.stream_id;
if (!streamId || !remaining.has(streamId)) continue;
if (message.type === "response.completed") {
latestResponseIdByLane.set(streamId, message.response.id);
remaining.delete(streamId);
}
}
}
async function waitForInProgress(events, streamId) {
while (true) {
const message = await readMessage(events);
if (
message.type === "response.in_progress" &&
message.stream_id === streamId
)
return;
}
}
const ws = new ResponsesWS(client);
// Keep one iterator so events stay queued while moving between phases.
const events = ws.stream();
try {
// Run two independent conversations in parallel.
sendCreate(
ws,
"planner",
"Draft a deployment plan for a stateless API service."
);
sendCreate(
ws,
"research",
"List common deployment risks for a stateless API service."
);
await drainUntilComplete(events, new Set(["planner", "research"]));
// Fork the planner conversation and continue its original branch in parallel.
const plannerResponseId = latestResponseIdByLane.get("planner");
sendCreate(
ws,
"critic",
"Find gaps in this deployment plan.",
plannerResponseId
);
// Let the fork load its parent before advancing the original lane's cache.
await waitForInProgress(events, "critic");
sendCreate(
ws,
"planner",
"Add rollback and monitoring steps to the plan.",
plannerResponseId
);
await drainUntilComplete(events, new Set(["critic", "planner"]));
} finally {
await events.return?.();
ws.close();
}stream_id 的长度必须为 1–256 个字符,且只能包含字母、数字、下划线(_)、连字符(-)和句点(.)。只能在 WebSocket response.create 事件中使用它,不要将其包含在 HTTP POST /v1/responses 中。
对于命名流,服务端事件会包含对应的 stream_id,终止事件和请求范围内的错误也不例外。
如果省略 stream_id,请求会使用隐式默认通道,其事件不包含 stream_id。除此之外,默认通道遵循与命名流相同的排序和并发规则。空字符串不是有效的 stream_id;要选择默认通道,请省略该字段。
连接行为与限制
- 每个响应内的事件遵循现有的 Responses 流式事件模型。不同通道的事件可能交错到达。
- 具有相同
stream_id的请求按先进先出顺序运行,且不会重叠执行。不同通道上的请求可以并发运行。 - 连接最长持续 60 分钟。达到时限后,请重新连接。
重新连接与恢复
当连接关闭(或达到 60 分钟的时限)时,所有通道在该连接上的本地缓存都会消失。请建立新的 WebSocket 连接,并使用以下方式之一恢复各个通道:
- 如果您已存储先前的响应(
store=true)并且拥有有效的响应 ID,请使用previous_response_id和新的输入项继续该通道的对话。 - 如果您无法继续某个通道的对话(例如,使用
store=false/ZDR 或遇到previous_response_not_found),请将previous_response_id设为null(或省略该参数)以创建新响应,并发送该通道下一轮所需的完整输入上下文。 - 如果您已使用
/responses/compact压缩上下文,请将返回的压缩后窗口作为新响应的基础input,然后追加最新的用户项和工具项。
需要处理的错误
当服务器能够将错误关联到某个命名通道时,错误事件会包含 stream_id。发生请求级错误后,其他通道可以继续运行。
previous_response_not_found
{
"type": "error",
"status": 400,
"stream_id": "main",
"error": {
"type": "invalid_request_error",
"code": "previous_response_not_found",
"message": "Previous response with id 'resp_abc' not found.",
"param": "previous_response_id"
}
}
invalid_stream_id
{
"type": "error",
"status": 400,
"error": {
"type": "invalid_request_error",
"code": "invalid_stream_id",
"message": "The 'stream_id' field must be a non-empty string with at most 256 characters and may only contain letters, numbers, underscores, hyphens, and periods.",
"param": "stream_id"
}
}
websocket_stream_limit_reached
{
"type": "error",
"status": 400,
"stream_id": "agent_33",
"error": {
"type": "invalid_request_error",
"code": "websocket_stream_limit_reached",
"message": "This WebSocket connection has reached its maximum number of distinct stream IDs (32). Reuse an existing stream_id or open a new WebSocket connection.",
"param": "stream_id"
}
}
websocket_connection_limit_reached
{
"type": "error",
"error": {
"type": "invalid_request_error",
"code": "websocket_connection_limit_reached",
"message": "Responses websocket connection limit reached (60 minutes). Create a new websocket connection to continue."
},
"status": 400
}