Spring AI 结构化输出

本文介绍如何在 Spring AI 中使用结构化输出:让模型按约定格式返回内容,并由转换器解析为 Java 类型(List、Map、Bean),避免手写解析逻辑。本文演示三种内置转换器的用法。

示例代码库

您可以在 GitHub 仓库 中找到本文的示例代码。

为什么使用结构化输出

  • 类型安全:直接得到 List<String>Map、或自定义 Bean,无需自己解析字符串。
  • 格式约束:在提示词中注入「输出格式说明」,引导模型按约定格式(如 JSON、列表)返回,再由转换器反序列化。
  • 可复用:同一套 OutputConverter 可在多处复用,与业务提示词解耦。

本示例使用 DeepSeek(OpenAI 兼容 API),配置见 application.properties。以下接口分别演示 ListOutputConverterMapOutputConverterBeanOutputConverter 三种方式。

通用流程

所有结构化输出都遵循同一模式:

  1. 创建转换器ListOutputConverter / MapOutputConverter / BeanOutputConverter<T>
  2. 获取格式说明outputConverter.getFormat() 得到一段「输出格式」文本(如 JSON 结构、列表规则)。
  3. 注入提示词:在 Prompt 中预留占位符 {format},将 getFormat() 的结果传入,让模型按该格式生成。
  4. 调用模型chatClient.prompt(...).call().content() 得到原始文本。
  5. 解析结果outputConverter.convert(response) 将文本转为目标类型。

1. ListOutputConverter:列表输出

适用于「返回若干条字符串」的场景,如标题列表、关键词列表。

接口POST /api/suggest-titles
请求体{ "topic": "Spring Boot Tips", "count": 5 }
返回{ "titles": [ "标题1", "标题2", ... ] }

代码要点

ListOutputConverter outputConverter = new ListOutputConverter();

PromptTemplate pt = new PromptTemplate("""
I would like to give a presentation about the following:

{topic}

Give me {count} title suggestions for this topic.
Make sure the title is relevant to the topic and it should be a single short sentence.

{format}
""");

Map<String, Object> vars = Map.of(
    "topic", req.topic(),
    "count", req.count(),
    "format", outputConverter.getFormat()
);
Message message = pt.createMessage(vars);
String response = chatClient.prompt().messages(message).call().content();

List<String> titles = outputConverter.convert(response);
return new TitleSuggestionsResponse(titles);
  • outputConverter.getFormat() 会生成「请按某种列表格式输出」的说明,并放入 {format}
  • 模型返回的文本经 outputConverter.convert(response) 解析为 List<String>

调用示例

curl -s -X POST http://localhost:8080/api/suggest-titles \
  -H "Content-Type: application/json" \
  -d '{"topic":"Spring Boot Tips and Tricks","count":5}'

2. MapOutputConverter:键值对输出

适用于「返回一组键值对」的场景,如编程语言与诞生年份。

接口GET /api/langs
返回{ "Java": "1995", "Python": "1991", ... }(由模型生成,键值对数量不固定)

代码要点

MapOutputConverter outputConverter = new MapOutputConverter();

PromptTemplate pt = new PromptTemplate("""
Return all popular programming languages and their inception year.

{format}
""");

Map<String, Object> vars = Map.of("format", outputConverter.getFormat());
Message message = pt.createMessage(vars);
String response = chatClient.prompt().messages(message).call().content();

Map<String, Object> languages = outputConverter.convert(response);
return languages;
  • MapOutputConverter.getFormat() 会要求模型返回 RFC8259 兼容的 JSON 对象。
  • convert(response) 将模型返回的 JSON 解析为 Map<String, Object>

调用示例

curl -s http://localhost:8080/api/langs

3. BeanOutputConverter:Bean 输出

适用于「返回固定结构对象」的场景,如推文(内容 + 标签列表)。

接口POST /api/gen-tweet
请求体{ "prompt": "一段要写成推文的内容" }
返回{ "content": "推文正文", "hashtags": [ "#Spring", "#AI" ] }

目标类型(Record):

record Tweet(String content, List<String> hashtags) {}

代码要点

BeanOutputConverter<Tweet> beanOutputConverter = new BeanOutputConverter<>(Tweet.class);
String format = beanOutputConverter.getFormat();

PromptTemplate pt = new PromptTemplate("""
Generate a tweet for the following content:

{content}

{format}
""");

Map<String, Object> vars = Map.of("content", input.prompt(), "format", format);
Message userMessage = pt.createMessage(vars);

Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
String response = chatClient.prompt(prompt).call().content();

Tweet tweet = beanOutputConverter.convert(response);
return tweet;
  • BeanOutputConverter<Tweet> 会根据 Tweet 的字段生成 JSON Schema 说明,通过 getFormat() 注入到提示词。
  • 模型按该结构返回 JSON,convert(response) 反序列化为 Tweet 实例。

调用示例

curl -s -X POST http://localhost:8080/api/gen-tweet \
  -H "Content-Type: application/json" \
  -d '{"prompt":"IntelliJ IDEA 2025.2 发布,支持 Java 25 EA、Maven 4、Spring Debugger 插件。"}'

三种转换器对比

转换器目标类型典型场景示例接口
ListOutputConverterList<String>标题列表、关键词、多选列表/api/suggest-titles
MapOutputConverterMap<String, Object>键值对、属性表、不固定字段/api/langs
BeanOutputConverter<T>自定义 Bean/Record固定结构(如推文、工单)/api/gen-tweet

配置与运行

本示例使用 DeepSeek(OpenAI 兼容 API)。请先设置环境变量:

export DEEPSEEK_API_KEY=<your-deepseek-api-key>

application.properties 示例:

spring.application.name=05-structured-output

spring.ai.openai.base-url=https://api.deepseek.com
spring.ai.openai.api-key=${DEEPSEEK_API_KEY}
spring.ai.openai.chat.options.model=deepseek-reasoner

logging.level.org.springframework.ai.chat.client.advisor=DEBUG

启动应用后,可按上文 curl 依次测试 /api/suggest-titles/api/langs/api/gen-tweet

注意事项

  • 尽力而为:模型不保证严格按格式返回,转换器解析失败时可能抛异常,生产环境建议对 convert() 做 try-catch 或校验。
  • 提示词清晰:在业务提示中明确任务(如「只返回列表,每行一条」),再配合 {format},可提高模型遵守格式的概率。
  • Bean 字段BeanOutputConverter 依赖 Bean 的 getter/setter 或 Record 的访问器,字段名会体现在生成的 Schema 中,模型返回的 JSON 键名需与之一致。

总结

  • 结构化输出 = 在提示词中注入「输出格式」(getFormat())+ 模型返回文本后由转换器解析(convert(response))。
  • ListOutputConverter:列表字符串;MapOutputConverter:键值对 JSON;BeanOutputConverter<T>:固定结构的 Bean/Record。
  • 同一套流程可用于其他接口:定义目标类型、选合适转换器、在 Prompt 中加入 {format},即可得到类型化的结构化输出。