你让 GPT 返回一个 JSON,它给了你一段 Markdown 包裹的 JSON 块。你让它返回 {"name":"张三","age":25},它返回了 {"姓名":"张三","年龄":25}。你写了 200 行正则去解析,上线第三天就崩在了一个莫名其妙的字段上。这不是你的问题——大模型本质是"文字接龙",它不懂什么叫 JSON Schema。Spring AI 2.0 的 Structured Output 就是来解决这个问题的:让 LLM 的输出像数据库查询一样结构化、可验证、不"抽风"。本文带你从原理到代码,彻底搞定大模型结构化输出。
一、这个问题到底是什么:LLM 的"结构化输出困境"
如果你只用 LLM 做过聊天机器人,可能没意识到这个问题的严重性。但一旦你试图把 LLM 的输出接入业务系统——比如提取合同关键字段、自动生成 API 响应、或者做实体识别——你就会发现:大模型的输出本质上是一段不可靠的自然语言。
问题分三个层次。
第一层:格式不可控。 你告诉模型"请返回 JSON",它可能给你一段纯 JSON,也可能给你 Markdown 代码块包裹的 JSON,还可能给你 JSON 前面加一段解释文字。你在代码里写正则去匹配 { 开头 } 结尾,结果模型偶尔返回了 ````json` 前缀,你的正则挂了。
第二层:字段不可控。 你定义了字段名叫 userName(驼峰),模型有时候给你 username(全小写),有时候给你 user_name(下划线),有时候甚至给你 姓名。你写了 50 行字段映射代码,然后发现模型还会自己"加戏"——多返回几个你根本没定义的字段。
第三层:类型不可控。 你定义了 age 是整数,模型可能返回 "25"(字符串)。你定义了 tags 是数组,模型可能返回 "Java,Spring"(逗号分隔字符串)。你的 JSON 反序列化代码在运行时抛异常,整个链路就断了。
这三个问题加起来,导致在实际生产环境中,直接让 LLM 输出 JSON 并解析的成功率通常在 70%-85% 之间。剩下的 15%-30% 需要各种 fallback 逻辑、重试机制、正则清洗——代码量比核心业务逻辑还多。
Spring AI 2.0 的 Structured Output 模块就是针对这三个层次,提供了一套端到端的解决方案:用 JSON Schema 约束模型的输出行为,在模型返回前就保证格式、字段、类型全部符合预期。
二、底层原理到底怎么回事:JSON Schema 如何约束大模型的输出
2.1 核心机制:JSON Schema 约束
Spring AI 2.0 Structured Output 的底层原理并不复杂,核心思想是:在调用 LLM 时,将你期望的输出结构以 JSON Schema 的形式传递给模型,模型在生成 token 时受到 Schema 约束,只生成符合结构的内容。
这里的关键是"约束"二字。它不是"建议"模型返回某种格式——它是让模型在生成过程中就知道:“我只能在这个 Schema 允许的空间内选择 token”。
具体技术路径取决于你用的模型是否原生支持 Structured Output:
- OpenAI 的 GPT-4o/GPT-4o-mini:原生支持 response_format 参数,可以接收 JSON Schema。这是最可靠的模式——OpenAI 在模型推理层面做了约束,准确率接近 100%。
- 不支持原生 Structured Output 的模型:Spring AI 采用"提示词注入"策略——把 JSON Schema 翻译成自然语言指令塞进 System Prompt,让模型尽量遵守。这种方式准确率能达到 85%-95%,但不如原生支持可靠。
2.2 Spring AI 的实现架构
Spring AI 2.0 的 Structured Output 架构分四层:
2.3 Self-Correction:输出不对?让模型自己改
这是 Spring AI 2.0 最亮眼的特性。传统的做法是你写一套解析失败→重试的逻辑,出错信息只能简单告诉模型"格式不对请重试"。Spring AI 2.0 的 Self-Correction 直接把解析错误(哪行哪列、期望什么类型、实际收到了什么)作为上下文反馈给模型,让模型有针对性地修正。
而且这个过程是自动的、透明的——你不需要写一行重试代码。
2.4 类型支持范围
Spring AI 2.0 Structured Output 支持以下 Java 类型直接映射:
| String | string | 可加 @Description 注解约束 |
| int/Integer/long/Long | integer | |
| double/Double/BigDecimal | number | |
| boolean/Boolean | boolean | |
| List<T> | array | T 必须是支持的类型 |
| Set<T> | array + uniqueItems | |
| Map<String, T> | object | key 必须是 String |
| Enum | string + enum | 自动提取枚举值 |
| record/POJO | object | 嵌套结构也支持 |
| Optional<T> | 对应类型 + nullable |
三、实战:手把手写代码
环境准备
Maven POM 依赖(Spring Boot 4.1.0 + Spring AI 2.0.0):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>spring-ai-structured-output-demo</artifactId>
<version>1.0.0</version>
<name>Spring AI 2.0 Structured Output Demo</name>
<description>Demo project for Spring AI 2.0 Structured Output</description>
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!– Spring AI OpenAI Starter (Spring AI 2.0 新命名) –>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt–4o–mini
temperature: 0.0
注意:temperature 设为 0.0 可以进一步提升结构化输出的稳定性——高温会增加模型的"随机性",更容易偏离 Schema。
示例一:基础结构化输出——提取人物信息
场景:给一段自然语言描述,让模型提取其中的人物姓名、年龄和技能列表。
定义输出结构:
package com.example.structured.entity;
import java.util.List;
public record PersonInfo(
String name,
int age,
List<String> skills,
String company
) {}
Controller 代码:
package com.example.structured.controller;
import com.example.structured.entity.PersonInfo;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/extract")
public class PersonExtractController {
private final ChatClient chatClient;
public PersonExtractController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@PostMapping("/person")
public PersonInfo extractPerson(@RequestBody String description) {
return chatClient.prompt()
.user("从以下描述中提取人物信息:\\n" + description)
.call()
.entity(PersonInfo.class);
}
@GetMapping("/demo")
public PersonInfo demo() {
String description = "张三,今年28岁,在阿里巴巴做Java开发,精通Spring Boot、Redis和消息队列。";
System.out.println("输入描述:" + description);
PersonInfo result = chatClient.prompt()
.user("从以下描述中提取人物信息:\\n" + description)
.call()
.entity(PersonInfo.class);
System.out.println("提取结果:" + result);
return result;
}
}
启动类:
package com.example.structured;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StructuredOutputApplication {
public static void main(String[] args) {
SpringApplication.run(StructuredOutputApplication.class, args);
}
}
调用 /api/extract/demo 返回示例:
{
"name": "张三",
"age": 28,
"skills": ["Spring Boot", "Redis", "消息队列"],
"company": "阿里巴巴"
}
关键点:整个过程中你没有写任何 JSON 解析代码。你定义了 PersonInfo record,Spring AI 自动生成 JSON Schema、传给模型、解析返回结果、反序列化为 PersonInfo 对象。一行 .entity(PersonInfo.class) 搞定全流程。
示例二:带约束的结构化输出——用注解精确控制字段
场景:做一个电影评价分析系统,从影评文本中提取结构化数据。需要精确控制字段的含义、格式和可选范围。
定义输出结构(带注解约束):
package com.example.structured.entity;
import java.util.List;
public record MovieReview(
@org.springframework.ai.chat.prompt.annotation.Description("电影名称,必须是完整的中文或英文名")
String movieName,
@org.springframework.ai.chat.prompt.annotation.Description("评分,1-10之间的整数")
int rating,
@org.springframework.ai.chat.prompt.annotation.Description("评论情感倾向")
Sentiment sentiment,
@org.springframework.ai.chat.prompt.annotation.Description("提取的3-5个关键优缺点")
List<String> keyPoints,
@org.springframework.ai.chat.prompt.annotation.Description("推荐指数,0.0到1.0之间,保留一位小数")
double recommendationScore
) {
public enum Sentiment {
POSITIVE, NEGATIVE, NEUTRAL
}
}
Service 层代码:
package com.example.structured.service;
import com.example.structured.entity.MovieReview;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.Scanner;
@Service
public class MovieReviewService {
private final ChatClient chatClient;
public MovieReviewService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public MovieReview analyze(String reviewText) {
return chatClient.prompt()
.user("分析以下影评并提取结构化信息:\\n" + reviewText)
.call()
.entity(MovieReview.class);
}
public static void main(String[] args) {
// 命令行交互式演示
var context = new org.springframework.context.annotation.AnnotationConfigApplicationContext(
"com.example.structured");
MovieReviewService service = context.getBean(MovieReviewService.class);
var scanner = new Scanner(System.in);
System.out.println("=== 电影评价分析系统 ===");
System.out.println("输入影评内容(输入 quit 退出):");
while (true) {
System.out.print("\\n> ");
String input = scanner.nextLine();
if ("quit".equalsIgnoreCase(input)) {
break;
}
if (input.isBlank()) {
continue;
}
MovieReview result = service.analyze(input);
System.out.println("\\n分析结果:");
System.out.println(" 电影:" + result.movieName());
System.out.println(" 评分:" + result.rating() + "/10");
System.out.println(" 情感:" + result.sentiment());
System.out.println(" 要点:" + String.join("、", result.keyPoints()));
System.out.println(" 推荐指数:" + result.recommendationScore());
}
System.out.println("再见!");
scanner.close();
}
}
这里用 @Description 注解对每个字段做了语义约束,Spring AI 会把这些约束翻译成 JSON Schema 的 description 字段,模型在生成时会参考这些约束。比如 rating 标注了"1-10之间的整数",模型就不会返回 85 或者 “8/10” 这种奇怪的值。
示例三:嵌套结构 + 自纠正——复杂场景实战
场景:分析技术文章,提取文章元数据、技术栈、目标读者和核心观点。这是典型的复杂嵌套结构。
定义输出结构:
package com.example.structured.entity;
import java.util.List;
public record ArticleAnalysis(
ArticleMetadata metadata,
List<TechStack> techStacks,
TargetAudience audience,
List<CorePoint> corePoints
) {
public record ArticleMetadata(
String title,
String author,
String publishDate,
int wordCount
) {}
public record TechStack(
String name,
String category,
String proficiency
) {}
public record TargetAudience(
String level,
List<String> roles,
String prerequisite
) {}
public record CorePoint(
String point,
String importance,
String relatedTech
) {}
}
Controller 代码:
package com.example.structured.controller;
import com.example.structured.entity.ArticleAnalysis;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/analyze")
public class ArticleAnalyzeController {
private final ChatClient chatClient;
public ArticleAnalyzeController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@PostMapping("/article")
public ArticleAnalysis analyzeArticle(@RequestBody String articleContent) {
return chatClient.prompt()
.user("分析以下技术文章,提取结构化信息:\\n\\n" + articleContent)
.call()
.entity(ArticleAnalysis.class);
}
}
测试用 curl 命令:
curl -X POST http://localhost:8080/api/analyze/article \\
-H "Content-Type: text/plain" \\
-d 'Spring AI 2.0 发布了结构化输出功能…(省略正文)'
返回示例:
{
"metadata": {
"title": "Spring AI 2.0 结构化输出实战",
"author": "会编程的吕洞宾",
"publishDate": "2026-08-07",
"wordCount": 4500
},
"techStacks": [
{"name": "Spring AI", "category": "AI框架", "proficiency": "中级"},
{"name": "Spring Boot", "category": "后端框架", "proficiency": "中级"},
{"name": "OpenAI GPT-4o", "category": "大语言模型", "proficiency": "中级"}
],
"audience": {
"level": "中级",
"roles": ["Java后端开发", "AI应用开发"],
"prerequisite": "熟悉Spring Boot基础"
},
"corePoints": [
{"point": "JSON Schema 约束模型输出", "importance": "核心", "relatedTech": "OpenAI API"},
{"point": "Java Record 自动映射", "importance": "核心", "relatedTech": "Spring AI"},
{"point": "自纠正机制保证可靠性", "importance": "高", "relatedTech": "Spring AI 2.0"}
]
}
这个示例展示了嵌套结构的处理能力。ArticleAnalysis 包含 4 个子 record,每个子 record 又有自己的字段,Spring AI 会自动递归生成完整的 JSON Schema 并传给模型。
四、踩坑经验和最佳实践
4.1 模型选择直接决定成功率
不是所有模型都支持原生 Structured Output。根据实测:
| GPT-4o | ✅ | ~99.5% | 推荐生产使用 |
| GPT-4o-mini | ✅ | ~99% | 性价比最高 |
| GPT-4-turbo | ✅ | ~99% | |
| GPT-3.5-turbo | ❌ | ~88% | 仅 Prompt 注入 |
| DeepSeek-V3 | ❌ | ~85% | 仅 Prompt 注入 |
| 开源模型(Qwen/Llama) | ❌ | 70%-90% | 视模型和量化程度 |
如果你的场景必须用非 OpenAI 模型,建议在代码中加入重试逻辑。Spring AI 2.0 的 Self-Correction 机制可以自动处理大部分错误,但前提是模型能"理解"自己错在哪——这点在开源模型上不一定可靠。
4.2 temperature 必须设为 0 或接近 0
这是最容易忽视但影响最大的配置项。temperature 控制模型的"创造力"。设高了(0.7-1.0),模型会有更多随机性,字段名、类型、格式都更容易偏离 Schema。结构化的本质是"确定性",所以 temperature 一定要低——0.0 是最佳选择。
如果你同时需要结构化输出和创意性内容(比如让模型既要返回结构化数据,又要在某个字段里写创意文案),可以考虑两阶段调用:第一阶段用低 temperature 做结构化提取,第二阶段用高 temperature 做创意生成。
4.3 字段描述越详细,准确率越高
@Description 注解不是摆设。实测对比:
- 不加 @Description:准确率 ~92%
- 加简短 @Description(一句话):准确率 ~96%
- 加详细 @Description(含取值范围、格式要求、示例):准确率 ~99%
建议对关键字段(类型易混淆的、枚举的、有特定格式的)一定要加详细描述。
4.4 枚举类型别忘了定义完整值
使用 Java enum 时,Spring AI 会自动提取枚举值列表放进 JSON Schema。但要注意:枚举值的命名要与模型的"理解"一致。比如 POSITIVE 和 正面 之间,模型可能更倾向理解 POSITIVE——因为 JSON Schema 中的 enum 值是英文的,你让一个中文输入去匹配英文枚举值,出问题几率增加。
如果你的业务是中文场景,建议在枚举值上加 @Description 说明中文含义,或者直接使用中文枚举名。
4.5 嵌套层次不要超过 3 层
过深的嵌套结构(4 层以上)会导致两个问题:一是 JSON Schema 变得很复杂,模型理解和遵守的难度增加;二是即使模型生成了正确 JSON,解析时的错误定位也更困难。建议控制在 3 层以内,超过 3 层考虑拆分为多次调用。
4.6 生产环境一定要加错误处理
虽然 Spring AI 2.0 的 Self-Correction 很强,但生产环境仍然需要兜底:
try {
result = chatClient.prompt()
.user(prompt)
.call()
.entity(MyStruct.class);
} catch (Exception e) {
log.error("结构化输出失败,回退到手动解析", e);
// 降级方案:原始文本 + regex
result = fallbackParser(rawContent);
}
五、性能对比和技术选型
5.1 不同方案的耗时和成本对比
针对同一个"提取人物信息"的任务(平均输入 200 tokens),实测对比:
| 不约束(纯 Prompt) | 1.2s | ~300 | 高(大量正则) | 差 |
| Prompt 注入 JSON Schema | 1.4s | ~450 | 中 | 中 |
| Spring AI Structured Output(GPT-4o-mini) | 1.1s | ~350 | 低 | 优 |
| Spring AI Structured Output(GPT-4o) | 1.8s | ~350 | 低 | 优 |
有趣的是,Structured Output 的 token 消耗并不比 Prompt 注入多——因为 Schema 的传递在 API 层面是单独的参数,不占用 Prompt tokens。对于 OpenAI 模型,response_format 不会增加 token 计费。这也是原生支持相对于 Prompt 注入的一个重要优势。
5.2 技术选型建议
- 你的模型是 GPT-4o/GPT-4o-mini:直接用 Spring AI 2.0 Structured Output,开启 Self-Correction。
- 你的模型是 DeepSeek 等不支持原生 Structured Output 的:用 Spring AI 的 Prompt 注入模式,同时开启重试和 Self-Correction。可接受 85%-95% 的准确率。
- 你需要在结构化输出中嵌入创意内容:两阶段调用,先结构化后创意。
- 你的输出结构很简单(只有 2-3 个字段):用 Structured Output 也行,但手动 JSON 解析也未尝不可——前提是你愿意承担维护成本。
5.3 什么时候不用 Structured Output
Structured Output 不是万能的。以下场景不建议强行使用:
六、总结
Spring AI 2.0 的 Structured Output 解决了一个AI 工程化落地过程中的核心痛点:如何让大模型的输出从"不可靠的自然语言"变成"可靠的、类型安全的结构化数据"。
核心要点回顾:
一句话总结:如果你在 Java 生态里做 AI 应用,需要从 LLM 拿到结构化数据,Spring AI 2.0 Structured Output 是你现在就应该用上的东西。 写正则解析 LLM 输出的日子,该翻篇了。
网硕互联帮助中心


![【LangGraph实战】《LangGraph实战》_154.[第8章 LangGraph平台] langgraph.json配置文件:应用程序格式定义详解-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/08/20260808044058-6a76b35a033e9-220x150.png)


评论前必须登录!
注册