Spring AI - 结构化输出

工程 | Christian Tzolov | 2024年05月09日 | ...

更新:(2024年04月06日) 添加了使用结构化输出和新的、流畅的 ChatClient API 的代码片段。

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

科学的工作方式是处理事物零散的部分和片段,并假定其连续性;而艺术则只关注事物的连续性,并假定其零散的部分和片段。- 罗伯特·M·波西格

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

Spring AI 的 结构化输出转换器 有助于将 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(源自指定 Java 类的 JSON Schema)的 JSON 响应。然后,它使用 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) {
}

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

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 生成结构化输出的能力使用户能够与下游应用程序无缝集成。结构化输出转换器 促进了这一过程,确保模型输出能够可靠地解析为 JSON 或 Java 类等结构化格式。

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

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

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

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

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

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

查看所有