主题
服务商原生的结构化输出
默认情况下,.entity(...) 把 JSON 模式作为文本指令追加到提示词中。 那是一种响应侧的方法:模型被要求遵循,响应随后被解析。
互补的方法是一种请求侧约束:在 API 层面告知模型的服务商,响应必须符合某个模式。 大多数现代服务商都支持这一点(OpenAI 的 Structured Outputs、Anthropic 的结构化输出扩展、Gemini 的 responseSchema、Mistral 的 response_format)。
Spring AI 通过 EntityParamSpec 消费者上的一个开关可移植地暴露它:
java
ActorsFilms films = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorsFilms.class, spec -> spec.useProviderStructuredOutput());在线路层面会发生什么变化:
- 系统提示词不再携带 JSON 格式指令(更简洁、更少令牌)。
- 模式作为 API 层级的字段发送给服务商。
- 服务商的运行时强制符合性——无效的响应根本无法被产出。
这带来:
- 更高可靠性:模型保证输出符合模式。
- 更简洁的提示词:无需追加格式指令。
- 更好性能:模型可以在内部为结构化输出做优化。
Spring AI 如何检测支持
Spring AI 通过检查模型的聊天选项是否实现 StructuredOutputChatOptions 接口来检测原生支持。 若未实现,该标志会被静默忽略,调用回退到基于提示词的默认方式。
受支持的模型
截至 Spring AI 2.0,以下服务商支持原生的结构化输出。 无论装配的是哪一个,同样的 .useProviderStructuredOutput() 调用都能工作:
- OpenAI:GPT-4o 及之后的支持 JSON 模式的模型。
- Anthropic:Claude 3.5 Sonnet 及之后的模型。
- Google GenAI:Gemini 1.5 Pro 及之后的模型。
- Mistral AI:Mistral Small 及之后的支持 JSON 模式的模型。
- Ollama:支持 JSON 模式的模型(因模型而异;见已知限制)。
为什么默认关闭
兼容性。 较旧或不支持的模型会拒绝请求,而基于提示词的默认方式处处可用。
DANGER
原生的结构化输出默认不启用,因为支持情况在不同模型和服务商之间差异很大。
仅当你需要它所提供的更强的 API 级模式强制时才启用它,并且务必用你的具体模型版本进行测试。
已知限制
即便在宣称支持该特性的服务商上,原生结构化输出的支持也常常是部分的——所接受的 JSON 模式范围各不相同。 $ref、深度嵌套的数组、allOf/anyOf/oneOf、正则模式以及递归类型,都是常见的限制。 这可能导致形状漂移,而这正是 validateSchema() 擅长捕获的。
Ollama:因模型而异的不稳定性
并非所有 Ollama 模型都能可靠地遵循结构化输出的模式约束。 特别是带有内置推理或“思考”模式的模型(例如 qwen3:8b、qwen3.5:9b 以及其他较新的 Qwen 变体),可能会把内部推理轨迹作为纯文本返回,而非结构化 JSON,从而在 BeanOutputConverter 中引发诸如以下的反序列化错误:
StreamReadException: Unrecognized token 'The': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')如果你在 Ollama 上遇到此问题,请尝试换一个模型(例如 llama3.1:latest),或回退到默认的基于提示词的方法。 你也可以把 useProviderStructuredOutput() 与 validateSchema() 结合起来,从而让格式错误的响应被自动重试:
java
ActorsFilms films = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorsFilms.class, spec -> spec
.useProviderStructuredOutput()
.validateSchema());OpenAI:不支持顶层数组
OpenAI 的 Structured Outputs API 不接受顶层 JSON 数组作为响应模式(参见 OpenAI 社区讨论)。 在启用原生结构化输出的情况下请求一个 List<T> 会导致 API 错误。
java
// Does NOT work with OpenAI native structured output:
List<ActorsFilms> films = chatClient.prompt()
.user("Generate filmographies for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorsFilms>>() {},
spec -> spec.useProviderStructuredOutput()); // fails with OpenAI请改用以下替代方案之一:
java
// Option 1: wrap the list in a container record
record FilmographyList(List<ActorsFilms> films) {}
FilmographyList result = chatClient.prompt()
.user("Generate filmographies for Tom Hanks and Bill Murray.")
.call()
.entity(FilmographyList.class, spec -> spec.useProviderStructuredOutput());
List<ActorsFilms> films = result.films();
// Option 2: use the default prompt-based approach (no native output required)
List<ActorsFilms> films = chatClient.prompt()
.user("Generate filmographies for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});默认的基于提示词的流程没有此限制——不使用 useProviderStructuredOutput() 时,顶层数组可正常工作。
全局启用
useProviderStructuredOutput() 是一个逐次调用的开关。 要为 ChatClient 上的每次调用启用原生结构化输出,请设置 AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT 顾问参数——作为构建器上的默认值,或逐请求设置:
java
// Per request
ActorsFilms films = chatClient.prompt()
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorsFilms.class);
// Globally on the builder
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultAdvisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.build();
}服务商内置的 JSON 模式
独立于 useProviderStructuredOutput(),一些 AI 模型提供了专门的配置选项,可直接生成结构化(通常是 JSON)输出:
- OpenAI Structured Outputs 能确保模型生成严格符合你提供的 JSON 模式的响应。在
JSON_OBJECT(合法 JSON)与JSON_SCHEMA(附一个给定模式)之间选择(spring.ai.openai.chat.response-format选项)。 - Ollama 提供
spring.ai.ollama.chat.format选项来指定响应格式。目前唯一可接受的值是json。 - Mistral AI 提供
spring.ai.mistralai.chat.response-format选项。把它设为{ "type": "json_object" }可启用 JSON 模式;设为{ "type": "json_schema" }并附一个给定模式,则启用与你模式匹配的原生结构化输出。