你让 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 架构分四层:

  1. 类型定义层:你定义 Java record 或 POJO,Spring AI 自动生成对应的 JSON Schema。
  2. Schema 传递层:ChatClient 将 Schema 传递给底层 Chat Model。对于 OpenAI 模型,通过 response_format 参数;对于其他模型,通过 Prompt 注入。
  3. 输出解析层:模型返回后,Spring AI 自动将响应反序列化为你定义的 Java 类型。
  4. 验证纠错层:如果解析失败,Spring AI 2.0 支持自动重试和自纠正(Self-Correction),将错误信息反馈给模型重新生成。

2.3 Self-Correction:输出不对?让模型自己改

这是 Spring AI 2.0 最亮眼的特性。传统的做法是你写一套解析失败→重试的逻辑,出错信息只能简单告诉模型"格式不对请重试"。Spring AI 2.0 的 Self-Correction 直接把解析错误(哪行哪列、期望什么类型、实际收到了什么)作为上下文反馈给模型,让模型有针对性地修正。

而且这个过程是自动的、透明的——你不需要写一行重试代码。

2.4 类型支持范围

Spring AI 2.0 Structured Output 支持以下 Java 类型直接映射:

Java 类型JSON Schema 类型备注
Stringstring可加 @Description 注解约束
int/Integer/long/Longinteger
double/Double/BigDecimalnumber
boolean/Booleanboolean
List<T>arrayT 必须是支持的类型
Set<T>array + uniqueItems
Map<String, T>objectkey 必须是 String
Enumstring + enum自动提取枚举值
record/POJOobject嵌套结构也支持
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),实测对比:

方案平均耗时Token 消耗开发成本可维护性
不约束(纯 Prompt)1.2s~300高(大量正则)差
Prompt 注入 JSON Schema1.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 不是万能的。以下场景不建议强行使用:

  1. 输出高度自由文本(如写一篇文章、生成一段对话):结构化反而限制创造力。
  2. 输出结构动态变化(如根据用户输入决定返回哪些字段):Java record 是编译期确定的,动态结构需要用 Map 或自定义方案。
  3. 对延迟极其敏感(< 500ms):Self-Correction 的重试会引入额外延迟。

六、总结

Spring AI 2.0 的 Structured Output 解决了一个AI 工程化落地过程中的核心痛点:如何让大模型的输出从"不可靠的自然语言"变成"可靠的、类型安全的结构化数据"。

核心要点回顾:

  1. 定义即约束:用 Java record/POJO 定义输出结构,Spring AI 自动生成 JSON Schema 并传给模型。
  2. 原生支持优先:GPT-4o/GPT-4o-mini 在推理层面遵守 Schema,准确率接近 100%;非原生模型通过 Prompt 注入,准确率 85%-95%。
  3. Self-Correction 是杀手锏:解析失败时自动将错误反馈给模型重试,不需要你写一行重试代码。
  4. 细节决定成败:temperature=0.0、详细的 @Description 注解、合理的嵌套深度,这些细节直接影响准确率。

一句话总结:如果你在 Java 生态里做 AI 应用,需要从 LLM 拿到结构化数据,Spring AI 2.0 Structured Output 是你现在就应该用上的东西。 写正则解析 LLM 输出的日子,该翻篇了。


更多推荐