Spring AI 结构化输出实战指南
[LOADING...]
1. 概述
在与聊天机器人交互时,我们已经习惯了大型语言模型(LLMs)返回的纯文本响应。然而,当把这些 LLM 集成到应用程序中时,以编程方式使用这些响应就成了一个问题。
Spring AI 通过其结构化输出支持解决了这个问题。无需处理原始字符串,我们可以指示模型返回直接映射到 Java 类、集合和其他类型的数据。
在本教程中,我们将探讨如何使用 Spring AI 从 LLM 接收结构化输出。
2. 搭建项目
在深入实现之前,我们先搭建项目。
2.1. 配置聊天模型
首先,在项目的 pom.xml 文件中添加必要的依赖:
这里,我们导入 Spring AI 的 OpenAI starter 依赖,用它来与聊天模型交互。
接下来,在 application.yaml 文件中配置 OpenAI API 密钥和聊天模型:
这里,我们通过 gpt-5.6-luna 模型 ID 指定 OpenAI 的 GPT 5.6 Luna模型。或者,我们也可以使用其他聊天模型,因为具体 AI 模型或提供商对本演示并不重要。
设置这两个属性后,Spring AI 会自动创建一个 ChatModel 类型的 bean,我们将用它来构建 ChatClient bean:
ChatClient 类是与配置好的聊天补全模型交互的主要入口点。
2.2. 定义领域实体
接下来,定义一个领域实体,用于将模型响应转换为该实体:
这里我们定义了一个 Recipe record,其中包含嵌套的 Ingredient record 和 Difficulty enum。
在定义领域实体时,我们应选择具有描述性的字段名,因为这有助于 LLM 正确填充它们。对于含义模糊的字段,我们可以用 @JsonPropertyDescription 标注它们,为模型提供额外上下文。
3. 将响应转换为领域实体
设置完成后,我们使用 ChatClient bean 向模型请求一份食谱,并将其响应转换为我们的 record:
这里,我们传入一条生成食谱的指令,然后使用 Recipe 类调用 entity() 方法,而不是调用 content()——后者会返回原始文本响应。
entity() 方法会交给我们一个完整填充的 Recipe 实例,而我们无需编写任何解析逻辑。在幕后,Spring AI 执行以下步骤:
- 首先,根据我们传入的目标类型生成 JSON schema。
- 然后,将该 schema 以及一组格式指令追加到用户提示词中。
- 最后,当模型回复后,将响应文本反序列化为目标类型的实例。
4. 自校正结构化输出
即使提示词中已有清晰的 schema 和格式指令,聊天模型仍可能返回不符合要求的响应。下面看看一些从这类失败中恢复的方法。
4.1. 使用 validateSchema() 进行客户端验证
在第一种方法中,响应在反序列化之前会在我们这一侧进行验证:
这里,我们向 entity() 传入一个额外的 lambda,并调用 validateSchema() 方法。
启用后,Spring AI 会在反序列化之前,根据生成的 JSON schema 验证模型的响应。如果失败,它会将验证错误发送回模型。此过程会重复,直到输出有效或达到重试上限;默认上限为三次。
或者,我们可以在构建 ChatClient bean 时注册 StructuredOutputValidationAdvisor。使用这种方法,我们可以覆盖默认设置,并对每次调用应用客户端验证:
这里,我们将重试上限提高到五次,并声明 Recipe 为默认输出类型。此外,我们提供一个自定义 JsonMapper 实例来执行验证和反序列化任务。这里我们只是用默认设置配置了一个实例,但可以根据需求进行自定义。
4.2. 使用 useProviderStructuredOutput() 进行服务端验证
另一种方法是,不自行验证响应,而是将这项工作委托给模型提供商。大多数现代提供商,如 OpenAI、Anthropic、Google 和 Mistral,都接受在 API 请求中包含 schema,并保证响应符合该 schema。
我们可以通过同一个 lambda 上的另一个方法使用此能力:
启用后,Spring AI 会将 schema 作为专用 API 字段发送给提供商,而不是将指令追加到用户提示词中。
不过,在依赖它之前,我们应确认模型和提供商都支持它。我们甚至可以将其与 validateSchema() 一起使用,以获得更具弹性的设置。
5. 将响应转换为 List
有时我们可能希望模型返回一组结果,而不是单个对象:
这里,我们向 entity() 方法传入的不是一个类,而是一个 ParameterizedTypeReference 实例。这个包装器保留了有关 Recipe record 的泛型类型信息,并允许 Spring AI 相应地生成 JSON schema。
或者,当我们只需要一个普通字符串列表时,可以向 entity() 方法传入一个 ListOutputConverter 实例:
在这个轻量级选项中,转换器要求模型返回简单的逗号分隔列表,而不是 JSON,然后将回复拆分为 String 值的 List。
6. 将响应转换为 Map
在事先不知道响应形态的场景中,我们可以向 entity() 方法传入一个 MapOutputConverter 实例:
这里,我们收到一个以 String 为键、Object 为值的 Map。转换器指示模型以 JSON 对象回复。在缺少固定 schema 的情况下,由模型决定返回哪些键。
然而,这种灵活性会牺牲类型安全性,因此当我们知道期望的结构时,仍应优先使用专用的领域实体。
7. 使用 ChatModel 手动转换响应
到目前为止,我们都让 ChatClient bean 为我们处理一切。然而,当我们直接使用更底层的 ChatModel 抽象时,entity() 方法并不可用。
在这种情况下,我们可以自行使用转换器类:
这里,我们为 Recipe record 创建一个 BeanOutputConverter 实例。这与 Spring AI 在内部为我们的领域实体使用的是同一个转换器。
然后,我们将其 getFormat() 方法的输出追加到提示词中,其中包含生成的 JSON schema 和格式指令。最后,将模型的响应传给它的 convert() 方法,以获得 Recipe 实例。
这种手动方法并不局限于领域实体。我们也可以对列表和映射遵循完全相同的流程。唯一需要改变的是所创建的转换器实例。
8. 创建自定义输出转换器
到目前为止,我们一直依赖 Spring AI 提供的转换器,它们满足了大多数需求。然而,我们只需实现 StructuredOutputConverter 接口,就可以自己实现一个转换器:
这里,我们创建了一个用于处理 YAML 响应的转换器。
我们复用 BeanOutputConverter 为目标类型生成 JSON schema,并在 getFormat() 方法中编写要追加到用户提示词中的指令。然后,在 convert() 中,我们使用 YAMLMapper将模型响应反序列化为目标类型。
现在,验证一下我们的转换器能否正确反序列化响应:
这里,我们将一个示例模型响应传给转换器的 convert() 方法,并确认 Recipe record 被填充。
要将其用于真实模型,我们只需将 YamlOutputConverter 实例传给 entity() 方法。
9. 结论
在本文中,我们探讨了 Spring AI 中的结构化输出支持。
我们介绍了如何将聊天模型的响应转换为自定义领域实体、列表和映射。此外,还讨论了有助于在模型返回不符合 schema 的响应时进行恢复的自校正功能。最后,我们探索了如何实现自定义转换器。
一如既往,本文中使用的所有代码示例都可以在 GitHub 上获取。
文章 《Spring AI 结构化输出指南》 首次出现在 Baeldung。