Spring AI 2.0中的自纠正结构化输出
大型语言模型是文本输入、文本输出的系统——它们的界面是自然语言。[LOADING...]
自然语言对人类来说是极佳的界面,但对软件而言则很差。当下游代码需要对字段进行路由、持久化某个值或根据结果分支时,对话就必须变成一条记录。结构化输出弥补了这一鸿沟。模型被引导生成符合某种模式(Schema)的文本;应用程序将其解析回一个类型化对象,代码库的其余部分可以像对待其他领域类型一样对待它。
Spring AI 从第一天起就通过 ChatClient.call().entity(...) 支持结构化输出。Spring AI 2.0 为其增加了两个新旋钮——提供商原生结构化输出和自修正模式校验。默认行为不变,因此现有代码继续正常工作。
本文带你深入了解结构化输出,首先从一个简单可用的案例开始,然后逐个添加可靠性保障。
类型化响应
首先定义一个 Java 记录(Record)来描述你想要返回的形状:
然后让 ChatClient 来填充它。不再用 .content() 结束调用——它会返回原始文本回复——而是用 .entity(...) 并传入你的目标类型:
搞定。结果是一个类型化的 ActorsFilms 对象,你可以将其传递给代码的其余部分:
.entity(...)仅限.call()使用。 类型化解析需要完整的响应,因此它不适用于流式路径(.stream()返回文本块,而非类型化对象)。这一点适用于下面介绍的所有变体——Class、ParameterizedTypeReference、自定义转换器,无论是否使用新旋钮。
在幕后,Spring AI 做了三件事:一个模式生成器将你的 ActorsFilms 记录转换为 JSON 模式,该模式被附加到提示的系统上下文中,然后模型的 JSON 回答被交给一个类型转换器,它将其解析回你的记录。
[LOADING...]
这在 Spring AI 支持的所有模型上都可以工作——没有任何提供商特定的限制。
但它也没有任何保证。模型只是被要求生成符合模式的 JSON,而不是强制生成。大多数情况下它会遵守。有时则不会——它返回一个额外的字段、省略了必需字段,或者用散文包裹 JSON。此时解析器会抛出异常。
接下来的两个部分分别介绍如何解决这个问题。
添加安全网:validateSchema()
处理格式错误输出的最简单方法是检测并重试。Spring AI 2.0 通过一个开关自动完成此操作:
spec -> spec.validateSchema() 消费者开启了一个自修正重试循环:
- 模型响应。
- Spring AI 根据
ActorsFilms的模式验证响应。 - 如果验证通过,你将获得类型化记录。
- 如果失败,验证错误("缺少必需字段
actor"、"期望array,得到string")会被附加到用户提示中,并重新发起调用——默认最多尝试 3 次。
模型在每次重试时都看到具体的错误。第二次尝试不是盲目重试;模型知道哪里错了并可以纠正。
[LOADING...]
这是由 StructuredOutputValidationAdvisor 驱动的,这是一个递归顾问,当你调用 validateSchema() 时会自动注册。你无需额外配置;这个开关就是全部配置。
自定义顾问
StructuredOutputValidationAdvisor 默认重试 3 次,并使用 Spring AI 的默认 JsonMapper。要自定义——例如更多重试次数、预提供模式或不同的映射器——构建你自己的实例并在 ChatClient 上注册。显式注册的顾问会替换自动注册的那个:
添加上游保证:useProviderStructuredOutput()
validateSchema() 是一个响应侧安全网——它在事后捕获错误输出并重试。互补的方法是请求侧约束:在 API 层面告诉模型提供商,响应必须符合某个模式。大多数现代提供商都支持这一点(OpenAI 的结构化输出、Anthropic 的结构化输出扩展、Gemini 的 responseSchema、Mistral 的 response_format)。
Spring AI 通过同一个消费者上的另一个开关以可移植方式暴露它:
在传输层面发生的变化:
- 系统提示不再携带 JSON 格式指令(更干净,更少的 token)。
- 模式以 API 级别的字段发送给提供商。
- 提供商的运行时强制遵守——根本不会发出无效响应。
截至 2.0 版本支持的提供商:OpenAI、Anthropic、Google GenAI、Mistral AI、Ollama(模型相关)。无论接入的是哪个提供商,.useProviderStructuredOutput() 调用都有效。
Spring AI 通过检查模型的聊天选项是否实现 StructuredOutputChatOptions 接口来检测支持情况。如果不支持,该标志会被静默忽略,调用回退到基于提示的默认方式。
为什么默认关闭
兼容性。较旧或不支持的模型会拒绝请求,而基于提示的默认方式在所有地方都能工作。即使在受支持的提供商上,也有一些已知的限制值得提及:
- 部分 JSON 模式支持。 原生结构化输出支持通常是部分的——即使在宣传此功能的提供商上,接受的 JSON 模式表面也有所不同。
$ref、深层嵌套数组、allOf/anyOf/oneOf、正则模式以及递归类型是常见的限制。这种形状偏差正是validateSchema()(下一节)擅于捕获的。 - Ollama 搭配推理(“思考”)模型——像
qwen这样的变体可能会将其内部推理痕迹作为纯文本输出,而不是 JSON,导致解析错误。请使用非推理模型,或者将原生输出与validateSchema()结合使用,以使行为不正常的响应被重试。 - OpenAI 不接受顶级数组在其结构化输出 API 中。在原生请求之前,将列表包装在一个容器记录中(
record FilmographyList(List<ActorsFilms> films) {})。
两者结合
两个开关解决不同的问题,并且自然组合:
useProviderStructuredOutput() 通过在 API 层面约束模型来最小化格式错误输出的可能性。validateSchema() 捕获剩余情况——提供商的边缘情况、上面提到的 Ollama 推理怪癖——并自动纠正。
当下游代码无法容忍形状偏差时——缺失字段或错误类型会导致状态损坏、后续抛出异常或静默路由错误——请同时使用两者。
泛型类型:List、Map 等
.entity(Class) 用于具体类。对于泛型类型——List<ActorsFilms>、Map<String, ActorsFilms>——使用 ParameterizedTypeReference:
同样的 EntityParamSpec 消费者也适用:
需要注意一点:OpenAI 的结构化输出 API 不接受顶级数组。如果你在 OpenAI 上将 List<...> 与 .useProviderStructuredOutput() 结合使用,调用会失败。解决方法是一个单行包装记录:
默认的基于提示的流程没有这样的限制——顶级数组在不使用 useProviderStructuredOutput()时完全正常。
获取完整响应
.entity(...) 只返回解析后的对象。如果你还需要底层的 ChatResponse——用于 token 用量、可观测性元数据或超出实体之外的任何内容——请使用 .responseEntity(...):
它具有与 .entity(...) 相同的重载集——Class、ParameterizedTypeReference 以及 EntityParamSpec 消费者都适用。
内置转换器不够用时
内置的 BeanOutputConverter 很严格:它期望模型的响应必须是可解析的 JSON,仅此而已。但模型经常将 JSON 包裹在 Markdown 代码块中:
BeanOutputConverter 会在 "Here's" 的第一个 "H" 处抛出异常。常见的解决方法是自定义转换器,它去除代码块标记、提取 JSON,然后委托给默认解析器:
将其传递给 .entity(...) 而不是 Class:
因为此转换器将 getJsonSchema() 委托给底层的 BeanOutputConverter,两个新旋钮仍然有效——validateSchema() 和 useProviderStructuredOutput() 都针对默认转换器使用的相同模式进行操作。你获得了弹性解析加上自修正,无需额外配置。
getJsonSchema()的作用。 在 2.0 中作为StructuredOutputConverter的默认方法添加,getJsonSchema()是让转换器参与useProviderStructuredOutput()和validateSchema()的桥梁。实现它以返回你的模式(通常通过委托给BeanOutputConverter),新旋钮即可工作;保留默认实现,两个旋钮对该转换器都变成空操作。
非 JSON 格式
对于 JSON 无法覆盖的格式——用作配置生成器的 YAML、用于数据提取的 CSV——从头实现 StructuredOutputConverter:编写你自己的 getFormat() 提示和你自己的 convert(...) 解析器。将 getJsonSchema() 保留为默认值,两个新旋钮将不参与——基于提示的路径会像内置转换器一样运行。
速查表
总结
Spring AI 2.0 中的结构化输出仍然是你已经熟悉的 .entity(...) 调用,增加了两个新开关:用于响应侧自修正的 validateSchema(),以及用于请求侧强制约束的 useProviderStructuredOutput()。每个都可独立使用;组合起来则能约束请求并自修正响应。现有代码保持不变;新代码可以在每次调用中选择加入,而无需重新配置应用。
参考
- 结构化输出转换器参考
- ChatClient 参考
- 递归顾问参考——涵盖
StructuredOutputValidationAdvisor - Spring AI 2.0 中的工具调用——配套文章;工具输入模式使用相同的生成器
- JSON Schema 规范 2020-12——Spring AI 生成模式时所依据的模式方言