默认情况下,.entity(…)会将JSON Schema作为文本指令追加到提示词中。这属于响应侧方案:只是请求模型遵守格式,拿到返回结果之后再做解析。
与之互补的另一种方案是请求侧约束:在API调用层面告知模型服务商,响应必须严格遵循指定Schema。如今绝大多数主流大模型服务商都支持该能力:OpenAI的Structured Outputs、Anthropic结构化输出扩展、Gemini的responseSchema、Mistral的response_format。
Spring AI提供跨厂商兼容开关,配置在EntityParamSpec回调中:
|
1 2 3 4 |
ActorsFilms films = chatClient.prompt() .user("Generate the filmography for a random actor.") .call() .entity(ActorsFilms.class, spec -> spec.useProviderStructuredOutput()); |
网络传输层面发生的变化如下:
- 系统提示词不再携带JSON格式指令(提示词更精简,消耗Token更少);
- Schema直接作为API请求字段发送给服务商;
- 由服务商运行时强制校验格式,从根源上杜绝输出非法JSON。
带来三点收益:
- ✅可靠性更高:服务商保证输出严格匹配Schema;
- ✅提示词更干净:无需追加大段格式说明文本;
- ✅性能更优:模型内部可以针对结构化输出路径做优化。
Spring AI如何检测该能力支持
Spring AI检测依据:判断模型的聊天配置类是否实现StructuredOutputChatOptions接口。 如果模型不实现该接口,则该开关会被静默忽略,自动回退到基于提示词的默认实现。
支持的模型(Spring AI 2.0版本)
无论底层接入哪一家模型,同一套useProviderStructuredOutput()代码均可直接使用:
- OpenAI:GPT‑4o及之后支持JSON Schema的模型
- Anthropic:Claude 3.5 Sonnet以及更新版本
- Google GenAI:Gemini 1.5 Pro及之后版本
- Mistral AI:Mistral Small及之后支持JSON Schema的模型
- Ollama:支持JSON Schema的模型(因模型而异,详见已知限制)
为什么该功能默认关闭
兼容性考量。老旧或者不支持该特性的模型会直接拒绝该类请求;而基于提示词的方案可以在全部模型上运行。
原生结构化输出在不同模型、不同服务商之间支持程度差异很大,因此默认不启用。仅当业务需要API层面强Schema约束时才开启,务必针对你实际使用的模型版本做充分测试。
已知限制
即便厂商对外宣称支持该特性,原生结构化输出往往也是部分实现:可识别的JSON Schema子集有限。常见不支持特性:$ref引用、深层嵌套数组、allOf/anyOf/oneOf、正则表达式、递归类型。 正是这类Schema能力缺失会造成输出结构偏移,这正好是validateSchema()可以捕获的问题。
Ollama:模型层面存在不稳定性
并不是全部Ollama模型都可以稳定遵守结构化输出Schema约束。 尤其是带有内置推理/思考模式的模型(例如 qwen3:8b、qwen3.5:9b以及其他新版通义千问系列),会输出内部思考文本而不是JSON,触发BeanOutputConverter反序列化异常,报错示例:
|
1 |
StreamReadException: Unrecognized token 'The': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false') |
遇到该问题的处理方案:
- 更换其他模型,例如 llama3.1:latest;
- 回退到默认基于提示词的实现;
- 组合开启两个开关,格式错误自动重试修复:
|
1 2 3 4 5 6 |
ActorsFilms films = chatClient.prompt() .user("Generate the filmography for a random actor.") .call() .entity(ActorsFilms.class, spec -> spec .useProviderStructuredOutput() .validateSchema()); |
OpenAI:不支持顶层JSON数组
OpenAI原生结构化输出API,不允许根节点直接是数组(参考 OpenAI社区讨论)。 如果开启原生结构化输出,请求返回List会直接触发API报错。
|
1 2 3 4 5 6 |
// ❌ 该写法在OpenAI原生结构化输出模式下会失败 List films = chatClient.prompt() .user("Generate filmographies for Tom Hanks and Bill Murray.") .call() .entity(new ParameterizedTypeReference<List>() {}, spec -> spec.useProviderStructuredOutput()); |
两种替代方案: 方案1:使用容器Record把数组包装一层
|
1 2 3 4 5 6 7 |
record FilmographyList(List films) {} FilmographyList result = chatClient.prompt() .user("Generate filmographies for Tom Hanks and Bill Murray.") .call() .entity(FilmographyList.class, spec -> spec.useProviderStructuredOutput()); List films = result.films(); |
方案2:使用默认基于提示词的模式(不开启原生输出)
|
1 2 3 4 5 |
// 不开启useProviderStructuredOutput,顶层数组完全不受限制 List films = chatClient.prompt() .user("Generate filmographies for Tom Hanks and Bill Murray.") .call() .entity(new ParameterizedTypeReference<List>() {}); |
注意事项: 默认提示词驱动的流程没有该限制,可以直接返回顶层数组。
全局开启原生结构化输出
useProviderStructuredOutput() 默认是单次调用的开关。 想要让该ChatClient实例每一次调用都启用原生结构化输出,可以设置顾问参数 AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT;既可以在构建器设置全局默认,也可以单次请求指定。
|
1 2 3 4 5 6 |
// 方式1:单次请求生效 ActorsFilms films = chatClient.prompt() .advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT) .user("Generate the filmography for a random actor.") .call() .entity(ActorsFilms.class); |
|
1 2 3 4 5 6 7 |
// 方式2:ChatClient构建器全局生效 @Bean ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultAdvisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT) .build(); } |
各服务商内置JSON模式补充说明
和useProviderStructuredOutput()相互独立,部分大模型还提供独立配置项直接开启JSON输出模式:
- OpenAI:
spring.ai.openai.chat.response‑format,支持JSON_OBJECT(合法JSON对象)或者传入schema的JSON_SCHEMA; - Ollama:配置项
spring.ai.ollama.chat.format,仅支持取值json; - Mistral AI:
spring.ai.mistralai.chat.response‑format;设置{"type":"json_object"}开启普通JSON模式;设置{"type":"json_schema"}并传入schema,则启用原生结构化输出。