Spring AI - 结构化输出

工程 | Christian Tzolov | 2024年5月9日 | ...

更新:(2024年6月4日) 添加了使用新的流畅 ChatClient API 进行结构化输出的代码片段。

更新:(2024年5月17日) BeanOutputConverter 添加了 泛型 支持。

科学处理的是事物的片段和点滴,前提是其连续性;艺术只处理事物的连续性,前提是其片段和点滴。—— 罗伯特·M·波西格

大型语言模型(LLM)生成结构化输出的能力对于依赖可靠解析输出值的下游应用至关重要。开发者希望快速将 AI 模型的结果转换为数据类型,例如 JSON、XML 或 Java 类,以便将其传递给应用程序中的其他函数和方法。

Spring AI 的 Structured Output Converters(结构化输出转换器)有助于将 LLM 输出转换为结构化格式。如下图所示,这种方法围绕 LLM 文本补全端点运行:

structured-output-architecture

使用通用补全 API 从大型语言模型(LLM)生成结构化输出需要仔细处理输入和输出。结构化输出转换器在 LLM 调用之前和之后都起着关键作用,确保实现所需的输出结构。

在调用 LLM 之前,转换器会将格式说明附加到提示词中,为模型生成所需的输出结构提供明确指导。这些说明就像蓝图一样,塑造模型的响应以符合指定的格式。

在调用 LLM 之后,转换器获取模型的输出文本,并将其转换为结构化类型的实例。此转换过程涉及解析原始文本输出,并将其映射到相应的结构化数据表示,例如 JSON、XML 或特定领域的数据结构。

请注意,AI 模型不保证按照请求返回结构化输出。它可能无法理解提示词,或者可能无法按照请求生成结构化输出。


提示:如果您不想深入了解 API 细节,请随意跳过下一段,直接跳转到使用转换器部分。


1. 结构化输出 API

StructuredOutputConverter 接口定义如下:

public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
}

该接口使用目标结构化类型 T 参数化,结合了 Spring Converter<String, T> 接口和 FormatProvider 接口。

public interface FormatProvider {
	String getFormat();
}

下图说明了数据流经结构化输出 API 组件的过程。

structured-output-api

FormatProvider 为 AI 模型提供文本指令,用于格式化生成的文本输出,以便 Converter 可以将其解析为目标类型 T。格式示例如下:

  Your response should be in JSON format.
  The data structure for the JSON should match this Java class: java.util.HashMap
  Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.

格式说明通常使用 PromptTemplate 附加到用户输入的末尾,如下所示:

StructuredOutputConverter outputConverter = ...
String userInputTemplate = """ 
    ... user text input ....
    {format}
    """; // user input with a "format" placeholder.
Prompt prompt = new Prompt(
   new PromptTemplate(
      userInputTemplate, 
      Map.of(..., "format", outputConverter.getFormat()) // replace the "format" placeholder with the converter's format.
   ).createMessage());

Converter<String, T> 负责将模型输出文本转换为目标类型 T 的实例。

可用的输出转换器

目前,Spring AI 提供了 AbstractConversionServiceOutputConverterAbstractMessageOutputConverterBeanOutputConverterMapOutputConverterListOutputConverter 实现。

structured-output-hierarchy4

  • AbstractConversionServiceOutputConverter<T> - 提供预配置的 GenericConversionService 用于将 LLM 输出转换为所需格式。未提供默认的 FormatProvider 实现。
  • AbstractMessageOutputConverter<T> - 提供预配置的 MessageConverter 用于将 LLM 输出转换为所需格式。未提供默认的 FormatProvider 实现。
  • BeanOutputConverter - 配置指定的 Java 类(例如 Bean)或 ParameterizedTypeReference,此转换器采用 FormatProvider 实现,指示 AI 模型生成符合 DRAFT_2020_12 规范的 JSON 响应,该规范源自指定的 Java 类的 JSON Schema。随后,它使用 ObjectMapper 将 JSON 输出反序列化为目标类的 Java 对象实例。
  • MapOutputConverter 扩展了 AbstractMessageOutputConverter 的功能,并添加了一个 FormatProvider 实现,用于指导 AI 模型生成符合 RFC8259 规范的 JSON 响应。此外,它还包含一个转换器实现,利用提供的 MessageConverter 将 JSON 有效载荷转换为 java.util.Map<String, Object> 实例。
  • ListOutputConverter 扩展了 AbstractConversionServiceOutputConverter,并包含一个专为逗号分隔列表输出量身定制的 FormatProvider 实现。转换器实现利用提供的 ConversionService 将模型文本输出转换为 java.util.List

2. 使用转换器

接下来的部分详细介绍了如何利用可用的转换器生成结构化输出。源代码可在 spring-ai-structured-output-demo 仓库中找到。

Bean 输出转换器

以下示例展示了如何使用 BeanOutputConverter 生成演员的电影作品列表。

代表演员电影作品的目标记录

record ActorsFilms(String actor, List<String> movies) {
}

以下是使用新的流畅 ChatClient API 利用 BeanOutputConverter 的方法:

ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
    .user(u -> u.text("Generate the filmography of 5 movies for {actor}.")
            .param("actor", "Tom Hanks"))
    .call()
    .entity(ActorsFilms.class);

或直接使用低层级的 ChatModel API

String userInputTemplate = """
   Generate the filmography of 5 movies for {actor}.
   {format}
   """;
BeanOutputConverter<ActorsFilms> beanOutputConverter = new BeanOutputConverter<>(ActorsFilms.class);
String format = beanOutputConverter.getFormat();
String actor = "Tom Hanks";
Prompt prompt = new Prompt(
   new PromptTemplate(userInputTemplate, Map.of("actor", actor, "format", format)).createMessage());
Generation generation = chatClient.call(prompt).getResult();
ActorsFilms actorsFilms = beanOutputConverter.convert(generation.getOutput().getContent());

支持泛型 Bean 类型

使用 ParameterizedTypeReference 构造函数指定更复杂的目标类结构。例如,表示演员及其电影作品列表:

List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt()
    .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {
    });

或直接使用低层级的 ChatModel API

BeanOutputConverter<List<ActorsFilms>> outputConverter = new BeanOutputConverter<>(
new ParameterizedTypeReference<List<ActorsFilms>>() { });
String format = outputConverter.getFormat();
String template = """
Generate the filmography of 5 movies for Tom Hanks and Bill Murray.
{format}
""";
Prompt prompt = new Prompt(new PromptTemplate(template, Map.of("format", format)).createMessage());
Generation generation = chatClient.call(prompt).getResult();
List<ActorsFilms> actorsFilms = outputConverter.convert(generation.getOutput().getContent());

Map 输出转换器

以下片段展示了如何使用 MapOutputConverter 生成数字列表。

Map<String, Object> result = ChatClient.create(chatModel).prompt()
    .user(u -> u.text("Provide me a List of {subject}")
                    .param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'"))
    .call()
    .entity(new ParameterizedTypeReference<Map<String, Object>>() {
    });

或直接使用低层级的 ChatModel API

MapOutputConverter mapOutputConverter = new MapOutputConverter();
String format = mapOutputConverter.getFormat();
String userInputTemplate = """
   Provide me a List of {subject}
   {format}
   """;
PromptTemplate promptTemplate = new PromptTemplate(userInputTemplate,
   Map.of("subject", "an array of numbers from 1 to 9 under they key name 'numbers'", "format", format));
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatClient.call(prompt).getResult();
Map<String, Object> result = mapOutputConverter.convert(generation.getOutput().getContent());

List 输出转换器

以下片段展示了如何使用 ListOutputConverter 生成冰淇淋口味列表。

List<String> flavors = ChatClient.create(chatModel).prompt()
            .user(u -> u.text("List five {subject}")
            .param("subject", "ice cream flavors"))
            .call()
            .entity(new ListOutputConverter(new DefaultConversionService()));

或直接使用低层级的 ChatModel API

ListOutputConverter listOutputConverter = new ListOutputConverter(new DefaultConversionService());
String format = listOutputConverter.getFormat();
String userInputTemplate = """
   List five {subject}
   {format}
   """;
PromptTemplate promptTemplate = new PromptTemplate(userInputTemplate,   
     Map.of("subject", "ice cream flavors", "format", format));
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = this.chatClient.call(prompt).getResult();
List<String> list = listOutputConverter.convert(generation.getOutput().getContent());

3. 参考资料

4. 结论

LLM 生成结构化输出的能力使开发者能够与下游应用无缝集成。Structured Output Converters 促进了这一过程,确保将模型输出可靠地解析为 JSON 或 Java 类等结构化格式。

本文提供了 BeanOutputConverterMapOutputConverterListOutputConverter 等转换器的实际使用示例,展示了如何为不同数据类型生成结构化输出。

总而言之,Spring AI 的结构化输出转换器为希望利用 LLM 强大功能,同时通过结构化输出格式确保应用程序兼容性和可靠性的开发者提供了强大的解决方案。

获取 Spring 资讯

订阅 Spring 资讯,保持连接

订阅

领先一步

VMware 提供培训和认证,助您加速前进。

了解更多

获取支持

Tanzu Spring 通过一项简单的订阅提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看全部