Skip to content
Spring AI 文档版本 2.1.0-SNAPSHOT 源快照 48f737b7 更新于 2026-10-16

OpenAI Responses API

Spring AI 通过 OpenAiResponsesChatModel 支持 OpenAI 的 Responses API(/v1/responses),它是与 OpenAI 聊天 中所述 Chat Completions 并列的第二个 OpenAI ChatModel。

Responses API 是 OpenAI 新能力落脚的端点。 选择它的两个理由:

  • 推理与工具调用并用。 从 GPT-5.4 起,Chat Completions 不支持除 none 之外带推理力度的工具调用。Responses 支持。如果你在当前旗舰模型上构建智能体,这是一个正确性缺口而非缺失特性。
  • 推理摘要与服务端执行的工具。 网络搜索、文件搜索、代码解释器、远程 MCP 和图像生成都在 OpenAI 自身请求内运行,且推理摘要会被返回而非隐藏。

WARNING

OpenAiResponsesChatModel 是无状态的。 每次调用都会发送完整的 Prompt;绝不会要求 OpenAI 记住你的对话。 这里“Responses API”并不意味着“OpenAI 为你存下线程”——参见无状态性。

WARNING

尚无聊天记忆仓储持久化消息片段,因此只有 InMemoryChatMemoryRepository 能保留完整对话(含推理)。 记忆实现预计在 2.1.0-M2 在连续性丢失之处,对话仍可进行、答案仍正确;模型每轮从头推理。 在单个工具循环内连续性总能保持,因为聊天记忆位于循环之外。

前置条件

与 OpenAI 聊天 相同:通过 spring.ai.openai.api-key 设置的 API 密钥,以及 classpath 上的 spring-ai-starter-model-openai starter。

xml
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

选择端点

OpenAI 是 Spring AI 的默认聊天服务商,且 ChatClient 自动配置要求恰好一个 ChatModel bean,所以两个 OpenAI 端点互斥而非都配置。一个属性决定你得到哪一个:

properties
# chat-completions (default) | responses
spring.ai.openai.chat.api=responses

用 responses 时,自动配置会创建一个单独的 OpenAiResponsesChatModel bean 而非 OpenAiChatModel bean。 连接设置保持原位,在 spring.ai.openai 之下。

如果你确实想要两者都用——比如说用 Chat Completions 做廉价分类、用 Responses 做一个智能体——把该属性留在默认值,并自己声明第二个模型:

java
@Bean
OpenAiResponsesChatModel agentChatModel() {
	return OpenAiResponsesChatModel.builder()
		.options(OpenAiResponsesChatOptions.builder()
			.model("gpt-5-mini")
			.reasoningEffort("medium")
			.build())
		.build();
}

注意,第二个 ChatModel bean 会使 ChatClient 自动配置产生歧义,所以把其中一个标记为 @Primary,或显式构建你的 ChatClient 实例。

聊天属性

spring.ai.openai.responses 前缀配置该模型。

属性描述默认值
spring.ai.openai.responses.model要使用的模型。gpt-5-mini
spring.ai.openai.responses.max-output-tokens生成令牌的上限,含推理令牌。-
spring.ai.openai.responses.temperature采样温度。-
spring.ai.openai.responses.top-p核采样。-
spring.ai.openai.responses.reasoning-effortnone、minimal、low、medium 或 high。-
spring.ai.openai.responses.reasoning-summaryauto、concise 或 detailed。-
spring.ai.openai.responses.verbositylow、medium 或 high。-
spring.ai.openai.responses.strict工具模式是否作为 strict 发送。false
spring.ai.openai.responses.parallel-tool-calls模型是否可一次请求多个工具。-
spring.ai.openai.responses.max-tool-calls一次响应中工具调用的上限。-
spring.ai.openai.responses.tool-choiceauto、none、required,或一个命名某函数或托管工具类型的 JSON 对象。-
spring.ai.openai.responses.include额外的响应字段,例如 web_search_call.results。-
spring.ai.openai.responses.truncation上下文溢出时 auto 丢弃条目,disabled 失败。-
spring.ai.openai.responses.service-tier处理层。-
spring.ai.openai.responses.prompt-cache-key用于提示词缓存的缓存键。-
spring.ai.openai.responses.safety-identifier稳定的、非识别性的最终用户 id。-
spring.ai.openai.responses.metadata附加到响应的任意键/值对。-
spring.ai.openai.responses.extra-body额外的请求体属性。-
spring.ai.openai.responses.hosted-tools.web-search.enabled启用服务端网络搜索。false
spring.ai.openai.responses.hosted-tools.web-search.search-context-sizelow、medium 或 high。-
spring.ai.openai.responses.hosted-tools.web-search.allowed-domains把结果限制到这些域。-
spring.ai.openai.responses.hosted-tools.file-search.vector-store-ids要搜索的向量库;设置其一即启用该工具。-
spring.ai.openai.responses.hosted-tools.file-search.max-num-results最大结果数。-
spring.ai.openai.responses.hosted-tools.code-interpreter.enabled启用服务端 Python 执行。false
spring.ai.openai.responses.hosted-tools.image-generation.enabled启用服务端图像生成。false

连接属性(api-key、base-url、timeout、max-retries、proxy、custom-headers,以及 Microsoft Foundry 和 GitHub Models 开关)与其他 OpenAI 模型共享,位于 spring.ai.openai 之下,也可在 spring.ai.openai.responses 之下按模型覆盖。

这些也都可通过 OpenAiResponsesChatOptions 逐请求设置:

java
ChatResponse response = chatModel.call(new Prompt("Plan a two-day trip to Rome.",
		OpenAiResponsesChatOptions.builder()
			.model("gpt-5-mini")
			.reasoningEffort("medium")
			.reasoningSummary("auto")
			.maxOutputTokens(1024)
			.build()));

无状态性

Spring AI 目前只实现 Responses API 的无状态模式

因此 store: false 会在每个请求上发送,且不可配置。 两个理由,都是出于正确性而非偏好:

  • 没有什么可供 OpenAI 保留。对话存在于 Prompt 中,previous_response_id 和服务端会话都不受支持,因此一个被存储的响应只会是一份没人读的副本。
  • 正因如此推理重放才能工作。若 store: true,OpenAI 会把推理留在服务端供 previous_response_id 寻址、且不返回推理以供交回,于是 include: ["reasoning.encrypted_content"] 一无所获,连续性被静默丢失。

因此 OpenAI 不会代 Spring AI 保留响应;你所在组织的数据保留设置仍适用于请求本身。

助手回合的无损重放

一个推理模型把它的思维链作为一个不透明的加密 blob 返回,你无法读取它。 为了让模型的思路跨一次工具调用或后续回合得以保持,那个确切的 blob 必须在下一次请求上原样交回。 丢弃它不会报错——只是答案更差、令牌更多、工作重复。

因此整个助手回合不是一条消息,而是一份类型化条目的转录——一个 reasoning 条目,接着是它产生的函数调用,然后是一条文本消息。 那份转录就是助手消息的有序 List<MessagePart>,每个条目一个部分,于是交错顺序得以保留、加密 blob 随其所属的推理一同传递:

输出条目消息部分
reasoningReasoningPart,其 payload() 是一个 OpaquePayload("openai.responses", "encrypted_content", <blob>)
messageTextPart
function_callToolCallPart,其 ToolCall.id() 是 call_id
带结果的 image_generation_callMediaPart,也可通过 getMedia() 看到
其他一切,包括 SDK 尚未建模的条目类型UnknownPart,逐字持有该条目

在下一次请求上,每个部分按部分顺序被还原成它来自的那个条目。 条目的 id、条目的状态以及助手消息 phase 随部分的 attributes() 一同携带,因为没有它们就无法重建一个条目。

有两样东西被有意地不重放:先前助手回合上引用的 annotations,它们仍可在生成元数据上读到,但对模型毫无用处;以及生成的媒体,因为 API 没有对应的输入条目。

一个 reasoning 部分只有在其 payload 来自本服务商时才被重放,ReasoningPart.replayableTo("openai.responses") 会报告这一点。 Responses API 会拒绝不带其自身加密内容的推理,因此由 Anthropic 或 Bedrock 产生的 reasoning 部分、或其 payload 在传递途中丢失的部分,会被带 DEBUG 日志跳过而非发送后被拒。 该回合仍会进行;模型只是从头推理。

工具调用

应用侧工具的工作方式与在 OpenAiChatModel 中完全一致,通过 ToolCallingAdvisor。 模型自身不运行任何工具循环。

java
String answer = ChatClient.create(chatModel)
	.prompt()
	.advisors(ToolCallingAdvisor.builder().build())
	.tools(new WeatherTools())
	.user("What is the weather in Paris?")
	.call()
	.content();

有两个行为是该端点特有的:

  • 工具模式除非你设置 strict 否则作为非 strict 发送。 Responses API 在该标志省略时会尝试 strict 模式,而 Spring AI 生成的模式常常不满足它,因此显式发送它能让一个工具在两个 OpenAI 模型间表现一致。
  • 服务端执行的工具绝不会作为工具调用出现。 它们改在生成元数据的 openai.responses.hosted_tool_calls 键下被报告,因此 ToolCallingAdvisor 从不会去寻找一个名为 web_search 的本地回调。

服务端执行的工具

它们在 OpenAI 的请求内运行;对你的应用来说没有要执行的东西。

java
var options = OpenAiResponsesChatOptions.builder()
	.model("gpt-5-mini")
	.hostedTools(
			new HostedTool.WebSearch("high", List.of("spring.io")),
			HostedTool.FileSearch.of("vs_abc123"),
			HostedTool.CodeInterpreter.of(),
			HostedTool.Mcp.of("my-server", "https://mcp.example.com"),
			HostedTool.ImageGeneration.of())
	.build();

HostedTool.Raw 是那些 OpenAI 先于 Spring AI 为其命名的工具的逃生舱:

java
new HostedTool.Raw(Map.of("type", "local_shell"))

来自 web 和 file 搜索的引用会落在生成元数据的 annotations 键下。 生成的图像作为助手消息上的 Media 到达,并以该条目报告的 output_format 标注。

MCP 审批往返尚未实现,因此 HostedTool.Mcp 在构造时会拒绝 requireApproval = "always":一个 mcp_approval_request 会在工具未运行的情况下结束回合。请使用 never。

推理

java
var options = OpenAiResponsesChatOptions.builder()
	.model("gpt-5-mini")
	.reasoningEffort("medium")
	.reasoningSummary("auto")
	.build();

ChatResponse response = chatModel.call(new Prompt("...", options));
String summary = response.getResult().getMetadata().get(OpenAiResponsesMetadata.REASONING_CONTENT);

OpenAI 从不返回原始推理文本——只有摘要和那个加密 blob。 推理令牌数可通过原生用量对象 response.getMetadata().getUsage().getNativeUsage() 获得。

Spring AI 总是向请求添加 include: ["reasoning.encrypted_content"]。 这是该模型上仅有的两个框架级默认之一,且它是为正确性而存在:没有那个 blob 回来,重放一个 reasoning 回合就不可能,而非仅仅降级。 另一个是上面所述的非 strict 工具模式。

结构化输出

ChatClient.entity() 原样工作。 在其下方,response_format 变为 text.format。

java
ActorFilms films = ChatClient.create(chatModel)
	.prompt("Generate the filmography for a random actor.")
	.call()
	.entity(ActorFilms.class);

多模态输入

支持图像(按 URL 或字节)和 PDF。

java
var media = Media.builder()
	.mimeType(MimeTypeUtils.IMAGE_PNG)
	.data(new ClassPathResource("/chart.png"))
	.build();

String description = ChatClient.create(chatModel)
	.prompt()
	.user(user -> user.text("Explain this chart.").media(media))
	.call()
	.content();

该端点不支持音频输入与输出,会带一条清晰的消息拒绝;音频请使用 OpenAiChatModel。

对回合转录的类型化访问

对于你想要转录而非扁平化消息的情况,读取各部分:

java
ChatResponse response = chatModel.call(new Prompt("..."));
AssistantMessage message = response.getResult().getOutput();

for (MessagePart part : message.getParts()) {
	if (part instanceof ReasoningPart reasoning) {
		System.out.println("thought: " + reasoning.summary());
	}
	else if (part instanceof ToolCallPart toolCall) {
		System.out.println("calls: " + toolCall.toolCall().name());
	}
	else if (part instanceof UnknownPart hostedTool) {
		System.out.println("ran server-side: " + hostedTool.kind());
	}
}

getReasoning()、getToolCalls()、getMedia() 和 getText() 是同一列表之上的视图,因此熟悉的访问器继续工作。 没有 OpenAI 专有的 AssistantMessage 子类:这些部分是服务商中立的,这正是让重放在流式聚合或一次聊天记忆往返之后仍能工作的原因。

元数据参考

助手消息不携带任何元数据。 一个回合除其各部分之外所报告的一切,描述的都是某一次响应而非该消息,所以它位于生成和响应元数据之上;消息元数据会被某些聊天记忆仓储持久化、被其他丢弃,这使它成为放置任何要紧之物的错误之处。 这不同于 OpenAiChatModel,后者在消息上发布 id、role、finishReason、refusal、annotations 和 reasoningContent。 下面的键在适用处沿用那些名字,因此切换 bean 只改变你在何处读取它们。

在生成元数据上,用 response.getResult().getMetadata() 读取:

键含义
getFinishReason()STOP、TOOL_CALLS、LENGTH 或 CONTENT_FILTER,通过类型化访问器而非某个键
openai.responses.statuscompleted、incomplete、failed,……
openai.responses.incomplete_reasonmax_output_tokens 或 content_filter
openai.responses.hosted_tool_calls服务端执行的工具调用,作为 {type, id, status} 条目
refusal模型的拒绝文本,或一个空字符串
annotations附加到生成文本的引用
reasoningContent推理摘要,每个 reasoning 条目一行

reasoningContent 是一个扁平化视图,在 OpenAiChatModel 和 DeepSeek 使用的键下发布。 权威形式是那些 ReasoningPart,它们把每个 reasoning 条目彼此分开、就地保留在它据以论证的那些工具调用之中,并携带重放它所需的加密内容。

openai.responses.hosted_tool_calls 概述单次请求内的活动,所以下一回合没有要重放的东西;每个条目的权威副本是它所映射到的那个 UnknownPart,逐字持有该条目。

annotations 键仅按名与 OpenAiChatModel 相符。 本模型发布 List<Map<String, Object>>;OpenAiChatModel 发布 OpenAI SDK 自己的 annotation 对象。 当你把一个 bean 换成另一个时,对条目做强制类型转换的代码需要调整。

在响应元数据上,用 response.getMetadata() 读取:

键含义
getId()响应的 resp_... id,通过类型化访问器
openai.responses.created_at响应创建时间,以自纪元起的秒数计
openai.responses.statuscompleted、incomplete、failed,……
openai.responses.incomplete_reasonmax_output_tokens 或 content_filter

请注意,response.getMetadata() 在流式聚合期间由一组固定字段重建,所以 openai.responses.* 键存在于每个流式块上、却不在聚合后的响应上。 getId()、getModel() 和 getUsage() 能在聚合后存活。

结束原因: STOP、TOOL_CALLS、LENGTH(因 max_output_tokens 而不完整)和 CONTENT_FILTER。 一个 failed 响应会抛出 OpenAiResponsesException 而非返回一个空答案。

限制

领域行为
n、stop 序列、频率与存在惩罚、seed、logit_bias、topK无 Responses 对应项。每个选项警告一次并被忽略。
音频输入与输出该端点不支持。带一条清晰的消息拒绝。
服务端会话状态未实现;参见无状态性。
store总作为 false 发送且不可配置,因为一个被存储的响应不返回可供重放的推理。参见无状态性。
后台执行、上下文压缩、MCP 审批、WebSocket 模式未实现。
跨回合的推理连续性与 store 支撑的聊天记忆丢失,因为尚无仓储持久化消息片段。用 InMemoryChatMemoryRepository 及在工具循环内可保留。参见无损重放。
OpenAI 兼容后端(vLLM、Ollama、LiteLLM、网关)常实现 /v1/chat/completions 但不实现 /v1/responses。Chat Completions 因此仍是默认。
GitHub ModelsGitHub Models 不支持 Responses API

从 Chat Completions 迁移

  1. 设置 spring.ai.openai.chat.api=responses。
  2. 把选项属性从 spring.ai.openai.chat.* 移到 spring.ai.openai.responses.*。多数名字沿用;max-tokens 变为 max-output-tokens,而 response-format/verbosity 从你的视角不变。
  3. 去掉任何 n、stop 序列、惩罚、seed 或 logit_bias 设置——它们在这里被忽略。
  4. 把对 id、role、finishReason、refusal、annotations 或 reasoningContent 的任何读取,从助手消息的元数据移到 response.getResult().getMetadata()。本模型在消息上不发布任何东西;参见元数据参考。
  5. 若你依赖跨回合的推理连续性,请对照无损重放中的表格检查你的聊天记忆仓储。目前只有 InMemoryChatMemoryRepository 保留它。
  6. 去掉任何 store 设置。该端点总发送 store=false;若你依赖 OpenAI 保留 Chat Completions 输出,那在此不再发生。
  7. 把任何请求时的 OpenAiChatOptions 替换为 OpenAiResponsesChatOptions。OpenAiChatOptions 构建器会像其他服务商的选项一样合并进本模型:model、temperature、top-p、max-tokens 和工具沿用,但 OpenAI 特定的设置如 reasoningEffort、responseFormat 和 toolChoice 不沿用。
  8. ChatClient 代码、顾问、.tools() 和 .entity() 无需改动。

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。