OpenAI 轮次中引导

2026-11-06 18:37 更新

轮次中引导(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:先用创建请求返回所需工具结果或审批,否则等待自动续接再提交更多;不要重复已接受输入。

排队的引导只存在当前连接,不随原响应存储。记录自己发送的引导,重放前与响应事件及历史对照,不能假定断连后仍保留。参见重连恢复。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号