截至 2024 年 2 月 5 日,旧的 OutputParserBeanOutputParserListOutputParserMapOutputParser 类已被弃用,取而代之的是新的 StructuredOutputConverterBeanOutputConverterListOutputConverterMapOutputConverter 实现。 后者是前者的直接替代品,提供相同的功能。更改的原因主要是命名,因为没有进行任何解析,同时也与 Spring org.springframework.core.convert.converter 包保持一致,带来了一些改进的功能。

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

Spring AI Structured Output Converters 帮助将 LLM 输出转换为结构化格式。 如下图所示,这种方法围绕 LLM 文本完成端点运行:

结构化输出转换器架构

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

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

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

StructuredOutputConverter 是尽最大努力将模型输出转换为结构化输出。 AI 模型不能保证按照请求返回结构化输出。 模型可能不理解提示或无法生成请求的结构化输出。 考虑实现验证机制以确保模型输出符合预期。

StructuredOutputConverter 不用于 LLM 工具调用,因为此功能默认提供结构化输出。

结构化输出 API

StructuredOutputConverter 接口允许您获取结构化输出,例如将输出映射到 Java 类或从基于文本的 AI 模型输出中获取值数组。 接口定义如下:

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

}

它结合了 Spring Converter 接口和 FormatProvider 接口

public interface FormatProvider {
	String getFormat();
}

下图显示了使用结构化输出 API 时的数据流。

结构化输出 API

FormatProvider 为 AI 模型提供特定的格式指南,使其能够生成可以转换为指定目标类型 T 的文本输出。以下是此类格式指令的示例:

您的响应应该是 JSON 格式。
JSON 的数据结构应该匹配这个 Java 类:java.util.HashMap
不要包含任何解释,只提供符合此格式的 RFC8259 兼容 JSON 响应,不要有任何偏差。

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

StructuredOutputConverter outputConverter = ...
String userInputTemplate = """
    ... 用户文本输入 ....
    {format}
    """; // 带有 "format" 占位符的用户输入。
Prompt prompt = new Prompt(
   new PromptTemplate(
		   this.userInputTemplate,
      Map.of(..., "format", outputConverter.getFormat()) // 用转换器的格式替换 "format" 占位符。
   ).createMessage());

转换器负责将模型的输出文本转换为指定类型 T 的实例。

可用的转换器

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

结构化输出类层次结构

使用转换器

以下部分提供了如何使用可用转换器生成结构化输出的指南。

Bean 输出转换器

以下示例显示如何使用 BeanOutputConverter 生成演员的电影作品。

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

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

以下是使用高级、流畅的 ChatClient API 应用 BeanOutputConverter 的方法:

ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("为 {actor} 生成 5 部电影的电影作品。")
                    .param("actor", "Tom Hanks"))
        .call()
        .entity(ActorsFilms.class);

或直接使用低级 ChatModel API:

BeanOutputConverter<ActorsFilms> beanOutputConverter =
    new BeanOutputConverter<>(ActorsFilms.class);

String format = this.beanOutputConverter.getFormat();

String actor = "Tom Hanks";

String template = """
        为 {actor} 生成 5 部电影的电影作品。
        {format}
        """;

Generation generation = chatModel.call(
    new PromptTemplate(this.template, Map.of("actor", this.actor, "format", this.format)).create()).getResult();

ActorsFilms actorsFilms = this.beanOutputConverter.convert(this.generation.getOutput().getText());

生成模式中的属性排序

BeanOutputConverter 通过 @JsonPropertyOrder 注解支持在生成的 JSON 模式中进行自定义属性排序。 此注解允许您指定属性在模式中出现的确切顺序,无论它们在类或记录中的声明顺序如何。

例如,要确保 ActorsFilms 记录中的属性特定排序:

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

此注解适用于记录和常规 Java 类。

泛型 Bean 类型

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

List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt()
        .user("为 Tom Hanks 和 Bill Murray 生成 5 部电影的电影作品。")
        .call()
        .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});

或直接使用低级 ChatModel API:

BeanOutputConverter<List<ActorsFilms>> outputConverter = new BeanOutputConverter<>(
        new ParameterizedTypeReference<List<ActorsFilms>>() { });

String format = this.outputConverter.getFormat();
String template = """
        为 Tom Hanks 和 Bill Murray 生成 5 部电影的电影作品。
        {format}
        """;

Prompt prompt = new PromptTemplate(this.template, Map.of("format", this.format)).create();

Generation generation = chatModel.call(this.prompt).getResult();

List<ActorsFilms> actorsFilms = this.outputConverter.convert(this.generation.getOutput().getText());

Map 输出转换器

以下代码片段显示如何使用 MapOutputConverter 将模型输出转换为地图中的数字列表。

Map<String, Object> result = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("为我提供一个 {subject}")
                    .param("subject", "一个从 1 到 9 的数字数组,键名为 'numbers'"))
        .call()
        .entity(new ParameterizedTypeReference<Map<String, Object>>() {});

或直接使用低级 ChatModel API:

MapOutputConverter mapOutputConverter = new MapOutputConverter();

String format = this.mapOutputConverter.getFormat();
String template = """
        为我提供一个 {subject}
        {format}
        """;

Prompt prompt = new PromptTemplate(this.template,
        Map.of("subject", "一个从 1 到 9 的数字数组,键名为 'numbers'", "format", this.format)).create();

Generation generation = chatModel.call(this.prompt).getResult();

Map<String, Object> result = this.mapOutputConverter.convert(this.generation.getOutput().getText());

List 输出转换器

以下代码片段显示如何使用 ListOutputConverter 将模型输出转换为冰淇淋口味列表。

List<String> flavors = ChatClient.create(chatModel).prompt()
                .user(u -> u.text("列出五种 {subject}")
                            .param("subject", "冰淇淋口味"))
                .call()
                .entity(new ListOutputConverter(new DefaultConversionService()));

或直接使用低级 ChatModel API

ListOutputConverter listOutputConverter = new ListOutputConverter(new DefaultConversionService());

String format = this.listOutputConverter.getFormat();
String template = """
        列出五种 {subject}
        {format}
        """;

Prompt prompt = new PromptTemplate(this.template,
        Map.of("subject", "冰淇淋口味", "format", this.format)).create();

Generation generation = this.chatModel.call(this.prompt).getResult();

List<String> list = this.listOutputConverter.convert(this.generation.getOutput().getText());

支持的 AI 模型

以下 AI 模型已经过测试,支持 List、Map 和 Bean 结构化输出。

内置 JSON 模式

一些 AI 模型提供专门的配置选项来生成结构化(通常是 JSON)输出。

文档有误?请协助编辑

发现文档问题?点击此处直接在 GitHub 上编辑并提交 PR,帮助我们改进文档!