主题
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-effort | none、minimal、low、medium 或 high。 | - |
| spring.ai.openai.responses.reasoning-summary | auto、concise 或 detailed。 | - |
| spring.ai.openai.responses.verbosity | low、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-choice | auto、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-size | low、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 随其所属的推理一同传递:
| 输出条目 | 消息部分 |
|---|---|
reasoning | ReasoningPart,其 payload() 是一个 OpaquePayload("openai.responses", "encrypted_content", <blob>) |
message | TextPart |
function_call | ToolCallPart,其 ToolCall.id() 是 call_id |
带结果的 image_generation_call | MediaPart,也可通过 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.status | completed、incomplete、failed,…… |
openai.responses.incomplete_reason | max_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.status | completed、incomplete、failed,…… |
openai.responses.incomplete_reason | max_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 Models | GitHub Models 不支持 Responses API |
从 Chat Completions 迁移
- 设置
spring.ai.openai.chat.api=responses。 - 把选项属性从
spring.ai.openai.chat.*移到spring.ai.openai.responses.*。多数名字沿用;max-tokens变为max-output-tokens,而response-format/verbosity从你的视角不变。 - 去掉任何
n、stop 序列、惩罚、seed或logit_bias设置——它们在这里被忽略。 - 把对
id、role、finishReason、refusal、annotations或reasoningContent的任何读取,从助手消息的元数据移到response.getResult().getMetadata()。本模型在消息上不发布任何东西;参见元数据参考。 - 若你依赖跨回合的推理连续性,请对照无损重放中的表格检查你的聊天记忆仓储。目前只有
InMemoryChatMemoryRepository保留它。 - 去掉任何
store设置。该端点总发送store=false;若你依赖 OpenAI 保留 Chat Completions 输出,那在此不再发生。 - 把任何请求时的
OpenAiChatOptions替换为OpenAiResponsesChatOptions。OpenAiChatOptions构建器会像其他服务商的选项一样合并进本模型:model、temperature、top-p、max-tokens和工具沿用,但 OpenAI 特定的设置如reasoningEffort、responseFormat和toolChoice不沿用。 ChatClient代码、顾问、.tools()和.entity()无需改动。