首页 / Spring AI 入门教程 / 结构化输出

Spring AI 入门教程

结构化输出

本教程共 45 篇 · 第 8 篇 · 更新于 2026-08-16 · 约 8 分钟阅读

Spring AI结构化输出BeanOutputConverterJSON SchemaChatClientMapOutputConverterJava

本节目标:学会用 BeanOutputConverter、MapOutputConverter、ListOutputConverter 把模型输出转成 Java 类型,并了解原生结构化输出。

模型返回的是文本,应用要的是对象。下游代码要解析 JSON、填充实体、落库,每一步都依赖格式可靠。常规做法是让模型输出 JSON,再用 Jackson 解析。问题是”让模型输出 JSON”并不总是成立:它可能加解释,可能格式不对。结构化输出转换器专门解决这件事。

8.1 原理:调用前给指令,调用后做转换

StructuredOutputConverter<T> 接口由两部分组成:

  • Converter<String, T>:把模型输出文本转成 T 类型
  • FormatProvider:提供格式指令文本

调用前,转换器把格式指令追加到提示词末尾。指令大致长这样:

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

这段指令就是给模型的”蓝图”。调用后,转换器解析模型输出,映射成目标类型实例。整个过程围绕 LLM 的文本补全接口展开,前后各加一道工序。两处配合,下游代码拿到的类型才是可靠的。

指令追加的位置有讲究,通常拼在用户输入的末尾,通过模板的 {format} 占位符注入。模型读完全部需求,最后看到格式要求,照做的概率最高。

接口定义也很简洁:

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

public interface FormatProvider {
    String getFormat();
}

getFormat() 产出格式指令,convert() 完成文本到对象的转换。

Note

转换器是尽力而为。模型不保证一定按请求返回格式。生产环境建议加校验,比如第 6 章的 StructuredOutputValidationAdvisor。
另外,工具调用(Tool Calling)不走转换器,它天生就是结构化输出。

8.2 BeanOutputConverter:输出变对象

最常用的是 BeanOutputConverter。给它一个 Java 类型,它自动生成 JSON Schema(DRAFT_2020_12),随提示词发给模型。模型输出后,它用 Jackson 反序列化成对象。

定义目标类型:

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

用 ChatClient 的高层 API 最简单:

ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
    .user(u -> u.text("列出 {actor} 出演的 5 部电影")
                .param("actor", "Tom Hanks"))
    .call()
    .entity(ActorsFilms.class);

.entity() 内部完成三件事:生成格式指令、追加到提示词、解析响应。底层 API 则要手动做这三步:

BeanOutputConverter<ActorsFilms> converter = new BeanOutputConverter<>(ActorsFilms.class);
String template = """
    列出 {actor} 出演的 5 部电影。
    {format}
    """;
Prompt prompt = PromptTemplate.builder()
    .template(template)
    .variables(Map.of("actor", "Tom Hanks", "format", converter.getFormat()))
    .build().create();
Generation generation = chatModel.call(prompt).getResult();
ActorsFilms actorsFilms = converter.convert(generation.getOutput().getText());

注意模板里的 {format} 占位符,它被替换成转换器的格式指令。convert() 负责把模型文本变成 Java 对象。对比两段代码能看出,高层 API 把模板拼接和解析都封装掉了。

JSON Schema 是描述 JSON 结构的标准。转换器根据 ActorsFilms 的字段生成 schema,模型照着 schema 输出,Jackson 再按同样的结构反序列化。schema 贯穿前后,保证两边对齐。

8.3 属性顺序与泛型类型

生成的 schema 里,属性顺序默认跟声明顺序一致。想调整用 @JsonPropertyOrder

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

注解对 record 和普通 class 都有效。record 是 Java 16 引入的语法,Spring Boot 4.0.x 要求 Java 17+,教程的基线版本完全支持。要 List<ActorsFilms> 这种泛型时,Class 表达不了,用 ParameterizedTypeReference

List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt()
    .user("列出汤姆·汉克斯和比尔·默瑞各自的 5 部电影")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});

底层写法同样支持,BeanOutputConverter 的构造函数接受 ParameterizedTypeReference

8.4 MapOutputConverter:不建类也能用

输出结构不固定时,为每种结构建类太累。MapOutputConverter 把输出转成 Map<String, Object>,模型的 JSON 直接变成键值对,灵活但需要自己处理嵌套。

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>>() {});

拿到 Map 之后,按 key 取数即可。适合结果形状经常变的场景。代价是没有类型安全,字段名拼错只有运行时才知道。键值结构简单直观,调试时把 Map 打出来就能看全貌。

8.5 ListOutputConverter:逗号分隔列表

输出是简单列表时用 ListOutputConverter。它引导模型输出逗号分隔的文本,再转成 List<String>

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

三种转换器的选择很简单:固定结构用 Bean,不固定结构用 Map,简单列表用 List。前两个产出 JSON,第三个产出纯文本列表。除这三个外,还有两个抽象基类 AbstractConversionServiceOutputConverterAbstractMessageOutputConverter,需要自定义转换器时从它们继承,省去配置转换服务的麻烦。

8.6 原生结构化输出

提示词方案靠模型自觉。现在很多模型提供原生结构化输出 API:schema 直接传给模型,模型保证输出符合 schema。更可靠,提示词也更干净,模型还能针对结构化输出做内部优化。

开启方式是在调用前加 advisor 参数:

ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
    .advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
    .user("随机选一位演员,列出他的 5 部电影")
    .call()
    .entity(ActorsFilms.class);

也可以全局开启:

@Bean
ChatClient chatClient(ChatClient.Builder builder) {
    return builder
        .defaultAdvisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
        .build();
}
Warning

原生结构化输出默认不开启,各模型支持程度差异大。启用前按模型逐个测试。

提示词方案兼容所有模型,原生方案更可靠但挑模型。官方建议:默认用提示词方案,需要 API 级强约束时再开原生,并且必须针对具体模型版本验证。两种方案在代码上只有一行之差,切换成本低,先用提示词跑通业务,再按需升级。

官方列出的支持列表:OpenAI GPT-4o 及以后、Anthropic Claude 3.5 Sonnet 及以后、Google Gemini 1.5 Pro 及以后、Mistral Small 及以后、Ollama 部分模型。集成测试还覆盖了 Azure OpenAI 和 Vertex AI Gemini。

列表会随模型迭代变化。接入新模型前,查一下官方文档的当前状态。

两个已知的坑:

OpenAI 不支持顶层数组。 原生模式下请求 List<T> 会报错。包一层容器 record 解决:

record FilmographyList(List<ActorsFilms> films) {}

Ollama 带思考模式的模型不稳定。 qwen3:8b 这类模型可能把思考过程当文本输出,导致反序列化失败。换模型(如 llama3.1),或退回提示词方案。也可以用 useProviderStructuredOutput() 配合 validateSchema(),让格式错误的响应自动重试。校验和重试正是第 6 章递归 advisor 的用武之地。

8.7 内置 JSON 模式

部分模型提供专门的 JSON 配置项,写进 application.properties 即可:

  • OpenAI:spring.ai.openai.chat.response-format.type,可选 JSON_OBJECT 或 JSON_SCHEMA
  • Ollama:spring.ai.ollama.chat.format,目前只接受 json
  • Mistral:spring.ai.mistralai.chat.response-format,可设 {"type": "json_object"}

JSON_OBJECT 保证输出是合法 JSON,JSON_SCHEMA 保证输出匹配指定 schema。这是模型层面的硬保证,和转换器配合使用效果最好。配置项按厂商分开,换模型时记得改对应前缀。内置 JSON 模式和原生结构化输出解决的是同一个问题,区别在触发方式:一个靠配置,一个靠 API 参数。实际项目里两者常二选一,不要重复叠加。

8.8 小结

结构化输出的核心是转换器:调用前给格式指令,调用后解析文本。Bean 转换器最常用,Map 和 List 适合轻量场景。追求可靠性就上原生结构化输出,注意模型兼容性。再配合校验重试 advisor,输出的可靠性才算完整。