OpenAI 轮次中引导
轮次中引导(mid-turn steering)允许用户在响应尚未结束时补充要求或改变方向,无需等待整次回答完成。
Responses的WebSocket连接上,GPT-6家族支持此功能,GPT-5.6及更早模型不支持。引导不会改写已发送给应用的输出、撤销既有动作或取消已经开始的工具。
Java示例要求Java 17或更新版本。连接方式见WebSocket模式,事件精确定义见Responses WebSocket事件参考。
发送引导消息
先用response.create开始响应,收到response.created后,在同一连接发送response.steer,以该响应ID作为previous_response_id。
来源示例 1(json):
{
"type": "response.steer",
"previous_response_id": "resp_1",
"input": "Keep the scope small enough for one developer to finish in two weeks."
}
事件只接受type、previous_response_id和input。input可为字符串,或内容类型受支持的非空用户消息数组。
API用response.steer.accepted确认排队:
来源示例 2(json):
{
"type": "response.steer.accepted",
"sequence_number": 4,
"steer": {
"id": "steer_0123456789abcdef0123456789abcdef",
"previous_response_id": "resp_1"
}
}
接受只表示输入已排队,不表示模型已经执行。除非还需应用提供工具结果或审批,API会自动创建带更新的后续响应。
自动续接前,服务端先完成当前输出项及已运行的托管工具。继续读取事件,不要另发response.create。被引导打断的原响应会以response.incomplete结束,原因steered;若原响应先正常完成,保留completed状态,仍可能随后出现引导续接。
自动续接继承原请求设置,词元和工具调用上限分别作用于每个响应。
运行完整示例
.NET SDK没有Responses WebSocket客户端,因此这里没有C# SDK版本。以下在项目计划生成过程中补充要求:
来源示例 3(javascript):
// [中文] 在第一次response.created后发送steer,把项目计划限定为一名开发者两周完成;追踪原响应及续接ID,不另发重复更新。
// Set OPENAI_API_KEY before running this example.
// Install the SDK and WebSocket transport: npm install openai ws
import OpenAI from "openai";
import { ResponsesWS } from "openai/resources/responses/ws";
const client = new OpenAI();
const ws = new ResponsesWS(client, {
handshakeTimeout: 10_000,
});
let initialResponseId = "";
let successorResponseId = "";
let timeout;
try {
const output = await new Promise((resolve, reject) => {
timeout = setTimeout(() => {
reject(new Error("Timed out waiting for the steered response."));
ws.close();
}, 120_000);
ws.once("error", reject);
ws.once("close", () => {
reject(
new Error("Connection closed before the steered response finished.")
);
});
ws.on("event", (event) => {
try {
if (event.type === "response.created") {
if (!initialResponseId) {
initialResponseId = event.response.id;
// Simulate a user adding instructions while the response runs.
ws.send({
type: "response.steer",
previous_response_id: initialResponseId,
input:
"Keep the scope small enough for one developer to finish in two weeks.",
});
} else {
successorResponseId = event.response.id;
}
} else if (
["response.steer.failed", "response.failed", "error"].includes(
event.type
)
) {
reject(new Error(JSON.stringify(event)));
} else if (
event.type === "response.incomplete" &&
(event.response.id !== initialResponseId ||
event.response.incomplete_details?.reason !== "steered")
) {
reject(new Error(JSON.stringify(event)));
} else if (
event.type === "response.completed" &&
event.response.id === successorResponseId
) {
let text = "";
for (const item of event.response.output) {
if (item.type !== "message") continue;
for (const part of item.content) {
if (part.type === "output_text") text += part.text;
}
}
resolve(text);
}
// Acceptance only queues the input. Keep reading past the first response.
} catch (error) {
reject(error);
}
});
ws.send({
type: "response.create",
store: false,
model: "gpt-6-astra",
reasoning: { effort: "medium" },
input: "Draft a project plan for building a task-tracking app.",
});
});
console.log(output);
} finally {
clearTimeout(timeout);
ws.close();
}
来源示例 4(python):
## [中文] 在第一次response.created后发送steer,把项目计划限定为一名开发者两周完成;追踪原响应及续接ID,不另发重复更新。 asyncio.wait_for将整个流程限制为120秒。
import asyncio
from openai import AsyncOpenAI
async def main():
client = AsyncOpenAI()
initial_response_id = None
successor_response_id = None
async with client.responses.connect() as connection:
await connection.response.create(
model="gpt-6-astra",
store=False,
reasoning={"effort": "medium"},
input="Draft a project plan for building a task-tracking app.",
)
async for event in connection:
if event.type == "response.created":
if initial_response_id is None:
initial_response_id = event.response.id
# Simulate a user adding instructions while the response runs.
await connection.response.steer(
previous_response_id=initial_response_id,
input="Keep the scope small enough for one developer to finish in two weeks.",
)
else:
successor_response_id = event.response.id
elif event.type in {"response.steer.failed", "response.failed", "error"}:
raise RuntimeError(event.to_json())
elif event.type == "response.incomplete":
response = event.response
if (
response.id != initial_response_id
or response.incomplete_details is None
or response.incomplete_details.reason != "steered"
):
raise RuntimeError(event.to_json())
elif (
event.type == "response.completed"
and event.response.id == successor_response_id
):
print(event.response.output_text)
return
# Acceptance only queues the input. Keep reading past the first response.
raise RuntimeError("Connection closed before the steered response finished.")
async def run():
await asyncio.wait_for(main(), timeout=120)
asyncio.run(run())
来源示例 5(go):
// [中文] 在第一次response.created后发送steer,把项目计划限定为一名开发者两周完成;追踪原响应及续接ID,不另发重复更新。
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
client := openai.NewClient()
conn, err := client.Responses.Connect(ctx, responses.ResponseConnectionOptions{})
if err != nil {
log.Fatal(err)
}
defer conn.Close()
if err := conn.Create(ctx, responses.ResponsesClientEventResponseCreateParam{
Model: "gpt-6-astra",
Reasoning: shared.ReasoningParam{
Effort: shared.ReasoningEffortMedium,
},
Store: openai.Bool(false),
Input: responses.ResponsesClientEventResponseCreateInputUnionParam{
OfString: openai.String("Draft a project plan for building a task-tracking app."),
},
}); err != nil {
log.Fatal(err)
}
initialID, successorID := "", ""
for {
event, err := conn.Recv(ctx)
if err != nil {
log.Fatal(err)
}
switch event.Type {
case "response.created":
id := event.OfResponsesServerEventResponseWsCreated.Response.ID
if initialID == "" {
initialID = id
err := conn.Send(ctx, responses.ResponsesClientEventUnionParam{
OfResponseSteer: &responses.ResponseSteerEventParam{
PreviousResponseID: id,
Input: responses.ResponseSteerInputUnionParam{
OfString: openai.String("Keep the scope small enough for one developer to finish in two weeks."),
},
},
})
if err != nil {
log.Fatal(err)
}
} else {
successorID = id
}
case "response.steer.failed", "response.failed", "error":
log.Fatal(event.RawJSON())
case "response.incomplete":
r := event.OfResponsesServerEventResponseWsIncomplete.Response
if r.ID != initialID || r.IncompleteDetails.Reason != "steered" {
log.Fatal(event.RawJSON())
}
case "response.completed":
r := event.OfResponsesServerEventResponseWsCompleted.Response
if successorID != "" && r.ID == successorID {
fmt.Println(r.OutputText())
return
}
}
// Steering acceptance only queues input. Wait for the successor to complete.
}
来源示例 6(java):
// [中文] 在第一次response.created后发送steer,把项目计划限定为一名开发者两周完成;追踪原响应及续接ID,不另发重复更新。 来源为Java 17+嵌入片段,须置于适当的类/方法环境,不能当作完整独立Java文件。
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.responses.*;
import java.util.*;
var client = OpenAIOkHttpClient.fromEnv();
long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(120);
try (var conn =
client.async().responses().connect().get(10, java.util.concurrent.TimeUnit.SECONDS)) {
conn.send(
ResponsesClientEvent.ofResponseCreate(
ResponsesClientEvent.ResponseCreate.builder()
.model("gpt-6-astra")
.reasoning(
com.openai.models.Reasoning.builder()
.effort(com.openai.models.ReasoningEffort.MEDIUM)
.build())
.store(false)
.input("Draft a project plan for building a task-tracking app.")
.build()));
String initialId = null, successorId = null;
while (true) {
var event =
conn.receive()
.get(
Math.max(1, deadline - System.nanoTime()),
java.util.concurrent.TimeUnit.NANOSECONDS);
if (event.responseCreated().isPresent()) {
var id = event.responseCreated().orElseThrow().response().id();
if (initialId == null) {
initialId = id;
conn.send(
ResponsesClientEvent.ofResponseSteer(
ResponseSteerEvent.builder()
.previousResponseId(id)
.input(
"Keep the scope small enough for one developer to finish in two weeks.")
.build()));
} else {
successorId = id;
}
} else if (event.responseSteerFailed().isPresent()
|| event.responseFailed().isPresent()
|| event.error().isPresent()) {
throw new IllegalStateException(event.toString());
} else if (event.responseIncomplete().isPresent()) {
var response = event.responseIncomplete().orElseThrow().response();
if (!response.id().equals(initialId)
|| !response
.incompleteDetails()
.flatMap(Response.IncompleteDetails::reason)
.map(r -> r.toString().equals("steered"))
.orElse(false)) throw new IllegalStateException(event.toString());
} else if (event.responseCompleted().isPresent()
&& event.responseCompleted().orElseThrow().response().id().equals(successorId)) {
event.responseCompleted().orElseThrow().response().output().stream()
.flatMap(item -> item.message().stream())
.flatMap(message -> message.content().stream())
.flatMap(content -> content.outputText().stream())
.forEach(text -> System.out.println(text.text()));
break;
}
// Acceptance only queues input; follow the successor through completion.
}
} finally {
client.close();
}
来源示例 7(ruby):
## [中文] 在第一次response.created后发送steer,把项目计划限定为一名开发者两周完成;追踪原响应及续接ID,不另发重复更新。
require "async"
require "openai"
client = OpenAI::Client.new
Sync do |task|
task.with_timeout(120) do
client.responses.connect(request_options: { timeout: 10 }) do |connection|
connection.response.create(
model: "gpt-6-astra", store: false, reasoning: { effort: "medium" },
input: "Draft a project plan for building a task-tracking app."
)
state = {}
while (event = connection.receive)
case event
when OpenAI::Responses::ResponseCreatedEvent
response = event.response
if !state[:initial_id]
state[:initial_id] = response.id
connection.send_event(
type: "response.steer", previous_response_id: state[:initial_id],
input: "Keep the scope small enough for one developer to finish in two weeks."
)
else
state[:successor_id] = response.id
end
when OpenAI::Responses::ResponseSteerFailedEvent, OpenAI::Responses::ResponseFailedEvent, OpenAI::Responses::ResponsesServerEvent::ResponseWsError
raise "Steering failed: #{event.to_json}"
when OpenAI::Responses::ResponseIncompleteEvent
response = event.response
unless response.id == state[:initial_id] && response.incomplete_details&.reason.to_s == "steered"
raise "Response incomplete: #{event.to_json}"
end
when OpenAI::Responses::ResponseCompletedEvent
response = event.response
next unless state[:successor_id] && response.id == state[:successor_id]
puts(response.output_text)
state[:completed] = true
break
end
end
raise "Connection closed before the steered response finished" unless state[:completed]
end
end
end
来源示例在第一次response.created后发送更新;实际应用应在用户提交更新时发送。收到续接响应的创建事件后,再次引导要使用新的响应ID。当前Python例以asyncio.wait_for设置120秒等待,Java例补齐客户端初始化。
返回工具结果或审批
需要客户端工具结果或审批时,引导继续排队;应用应在同一连接完成常规工具或审批流程。例如原响应调用get_project_status:
来源示例 8(json):
{
"type": "response.completed",
"response": {
"id": "resp_1",
"status": "completed",
"output": [
{
"type": "function_call",
"call_id": "call_project",
"name": "get_project_status",
"arguments": "{\"project\":\"task-tracker\"}"
}
]
}
}
原响应结束后,若已接受的引导仍需输入,会发出response.steer.pending,其required_input指出所需结果或审批。
来源示例 9(json):
{
"type": "response.steer.pending",
"sequence_number": 12,
"steer": {
"id": "steer_0123456789abcdef0123456789abcdef",
"previous_response_id": "resp_1"
},
"reason": "waiting_for_required_input",
"required_input": [
{
"type": "function_call_output",
"call_id": "call_project",
"name": "get_project_status"
}
]
}
在同一连接用response.create返回,previous_response_id设为resp_1。不要重复已接受的引导。显式创建请求使用自己提供的工具、指令及其他设置。
以下JSONC注释说明服务端在哪加入已排队更新;JSONC允许注释,不应当作严格JSON直接解析:
来源示例 10(jsonc):
格式说明:本块为带注释JSONC,不是严格JSON;注释用于说明流程。
// [中文] 这是JSONC:服务端会在工具结果前加入已接受的“两周内一人完成”要求;工具结果是设计已完成、实现待开始。注释不是请求数据。
{
"type": "response.create",
"model": "gpt-6-astra",
"previous_response_id": "resp_1",
"input": [
// The server implicitly prepends your accepted steer here:
// "Keep the scope small enough for one developer to finish in two weeks."
{
"type": "function_call_output",
"call_id": "call_project",
"output": "Design is complete. Development has not started.",
},
{
"role": "user",
"content": "Show me the updated plan before starting any work.",
},
],
}
无需等pending事件才提交工具结果;若服务端已收到匹配的创建请求,可以直接继续,不先发送该通知。
处理失败与断连
response.steer.failed表示此次输入未通过引导应用,后续也不会自动应用;steer中返回原始input和previous_response_id,error说明原因。以steer.id跟踪接受的提交,后续失败沿用相同ID。
常见错误:
invalid_input:仅使用支持的事件字段及用户消息。steering_not_supported:模型、参数或两者不支持。response_not_found:目标须在同一WebSocket连接仍可用。too_many_pending_steers:先用创建请求返回所需工具结果或审批,否则等待自动续接再提交更多;不要重复已接受输入。
排队的引导只存在当前连接,不随原响应存储。记录自己发送的引导,重放前与响应事件及历史对照,不能假定断连后仍保留。参见重连恢复。

TRAE-AI编程


更多建议: