大模型输出工程契约设计:从Prompt软约束到Schema硬约束

在Java AI Agent、智能抽取、工单分类、函数调用等核心业务场景中,大模型输出不可控是最常见的痛点:返回冗余文字、JSON格式错乱、字段缺失、枚举值非法等问题,会直接导致业务接口报错、流程中断。

很多开发者仅靠 Prompt文字约束+样例演示 的软约束方式,完全依赖模型"自觉",稳定性极差。

本文将深度拆解 OpenAI 三大结构化输出能力:JSON Mode、JSON Schema、Structured Outputs,结合 Spring AI 实战代码,全程使用业务常用的Lombok POJO,搭配完整可复现实战案例、错误演示,给出可直接落地的 AI Agent 工程级最佳实践,彻底解决大模型输出不规范问题。

一、核心概念精准区分

三者层级递进,解决的问题完全不同,杜绝混用、错用:

1. JSON Mode(json_object):语法层约束(最低级)

核心能力:仅强制模型输出语法合法的JSON字符串

禁止内容:禁止前缀后缀废话(好的、以下是结果等)、禁止Markdown JSON包裹(```json)

核心短板:只管语法,不管业务,不约束字段、枚举、必填项,可出现字段缺失、内容乱填、非法枚举值

2. JSON Schema:契约描述层(纯规则文档)

核心定位:一份标准化的JSON规则契约,定义字段名、数据类型、必填项、枚举值、是否允许多余字段

关键误区:Schema本身不会主动约束模型生成!仅作为规则文档,需要结合「模型API传递」或「后端代码校验」才能生效

两大核心用途:

  • 为 Structured Outputs 提供模型侧生成规则
  • 后端服务端兜底校验,拦截非法输出,配合重试机制保障稳定性

3. Structured Outputs:生成层强约束(OpenAI原生高阶能力)

核心能力:将JSON Schema通过API参数直接传给大模型,在Token生成阶段就约束输出规则

支持字段必填校验、枚举值锁定、多余字段拦截,从源头大幅降低非法输出概率

工程底线:即便开启该能力,仍需后端Schema校验兜底,复杂Schema场景模型仍可能出错

二、前置准备:统一实体与测试入参

2.1 业务POJO实体(Lombok,生产通用)

定义工单分类实体,替代Java17 Record,兼容所有SpringBoot版本:

复制代码
import lombok.Data;

/**
 * 工单结构化输出实体
 * 适配Spring AI结构化输出、Schema自动生成、后端校验
 */
@Data
public class Ticket {
    /**
     * 工单类型:仅支持 pay(支付) / refund(退款)
     */
    private String category;

    /**
     * 工单优先级:仅支持 urgent(紧急) / normal(普通)
     */
    private String priority;
}

2.2 统一测试入参

全文所有案例统一使用该业务输入,方便对照效果:

用户工单输入:用户反馈要退款,情况十分紧急

期望标准输出:category=refund,priority=urgent

💡 踩坑前置说明:

  • @Data 自动生成无参构造,满足反序列化要求,无需手动编写
  • 字段统一使用包装类型,避免基础类型默认值干扰(int默认0、boolean默认false,无法区分是模型返回还是初始化默认值)
  • 普通POJO不支持 @JsonSchema(enumValues) 注解,枚举约束需手动实现,下文给出完整解决方案

三、三种能力 Spring AI 实战

1、JSON Mode 完整案例(仅语法合法,业务完全不受控)

核心特征:只保证JSON能被解析,不保证字段、枚举、必填合规,生产禁止单独使用

1.1 OpenAI 原生API写法
复制代码
{
  "model":"gpt-4o-mini",
  "messages":[...],
  "response_format": {"type":"json_object"} // 开启纯JSON模式
}
1.2 Spring AI 实战代码
复制代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.options.ChatOptions;
import org.springframework.ai.chat.options.ResponseFormat;
import org.springframework.stereotype.Service;

@Service
public class TicketAiService {

    private final ChatClient chatClient;

    public TicketAiService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    /**
     * 仅开启JSON Mode:只保证语法合法,不约束业务字段
     */
    public String jsonModeDemo(String userInput) {
        ChatOptions options = ChatOptions.builder()
                // 开启JSON Mode:强制返回标准JSON字符串
                .responseFormat(ResponseFormat.JSON_OBJECT)
                .build();

        return chatClient.prompt()
                .user("提取下面工单信息:" + userInput)
                .options(options)
                .call()
                .content();
    }
}
1.3 真实运行效果

✅ 必然保证的效果:不会输出前缀废话、不会被Markdown包裹,返回一定是可解析JSON字符串

❌ 高频错误输出(真实复现):语法正确,业务全部错误

复制代码
// 场景1:枚举值乱自定义(最常见)
{"category":"refund_apply","priority":"very_urgent"}

// 场景2:必填字段缺失
{"category":"refund"}

// 场景3:非法多余字段
{"category":"refund","priority":"urgent","desc":"用户投诉情绪紧急"}

案例结论:JSON Mode 只能解决「JSON解析报错」,完全解决不了「业务数据错误」,核心链路绝对不能单用。

2、JSON Schema 完整案例

核心误区案例:只定义Schema、不传给模型,完全不生效,彻底弄懂Schema的本质是「校验规则」而非「约束能力」。

2.1 标准业务Schema(带枚举/必填/防多余字段)
复制代码
{
  "type":"object",
  "required":["category","priority"],
  "properties":{
    "category":{"type":"string","enum":["pay","refund"]},
    "priority":{"type":"string","enum":["urgent","normal"]}
  },
  "additionalProperties":false
}
2.2 错误用法案例

仅定义Schema、不传入API、只开JSON Mode:模型完全看不到契约规则,输出依旧乱码

复制代码
import org.springframework.ai.schema.JsonSchema;
import org.springframework.ai.schema.validator.JsonSchemaValidator;
import org.springframework.ai.schema.validator.ValidationResult;

public void schemaOnlyErrorDemo() {
    String schemaStr = """
            {
              "type":"object",
              "required":["category","priority"],
              "properties":{
                "category":{"type":"string","enum":["pay","refund"]},
                "priority":{"type":"string","enum":["urgent","normal"]}
              },
              "additionalProperties":false
            }
            """;
    // 1.仅在内存构建Schema,未传给模型API
    JsonSchema schema = JsonSchema.parse(schemaStr);

    // 2.仅开启JSON Mode,模型无任何契约约束
    String rawJson = chatClient.prompt()
            .user("提取下面工单信息:用户反馈要退款,情况十分紧急")
            .options(ChatOptions.builder().responseFormat(ResponseFormat.JSON_OBJECT).build())
            .call()
            .content();

    // 3.模型依旧输出非法结果:{"category":"refund_apply","priority":"very_urgent"}

    // 4.手动执行Schema校验,拦截错误
    JsonSchemaValidator validator = JsonSchemaValidator.create(schema);
    ValidationResult result = validator.validate(rawJson);
    if (!result.isValid()) {
        // 打印校验错误:枚举值非法、字段不匹配
        System.out.println("校验失败:" + result.getErrors());
    }
}

核心案例结论:Schema 只是静态文档!不传给模型、不做后端校验,就完全没有任何作用。

2.3 自动生成Schema案例(POJO反向生成)
复制代码
public void printAutoSchema() {
    // 从POJO自动推导生成Schema
    JsonSchema schema = JsonSchema.from(Ticket.class);
    // 打印自动生成的JSON规则
    System.out.println("POJO自动生成Schema:" + schema.toJson());
}

缺陷:自动生成的Schema无法识别枚举约束,生产必须手写Schema补齐枚举规则。

3、Structured Outputs 生产级完整案例(强约束+自动重试)

工程最优方案:手写Schema枚举约束 + 模型生成阶段约束 + 后端校验自动重试,双层兜底彻底稳定输出。

3.1 完整可运行代码
复制代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.schema.JsonSchema;
import org.springframework.stereotype.Service;

@Service
public class TicketStructuredService {

    private final ChatClient chatClient;

    public TicketStructuredService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    /**
     * 生产级结构化输出:模型层约束 + 后端校验重试双层兜底
     */
    public Ticket structuredOutputDemo(String userInput) {
        // 手写带完整枚举约束的Schema
        String schemaStr = """
                {
                  "type":"object",
                  "required":["category","priority"],
                  "properties":{
                    "category":{"type":"string","enum":["pay","refund"]},
                    "priority":{"type":"string","enum":["urgent","normal"]}
                  },
                  "additionalProperties":false
                }
                """;
        JsonSchema manualSchema = JsonSchema.parse(schemaStr);

        // 结构化调用,自动约束+校验重试
        return chatClient.prompt()
                .user("请对下面工单分类:" + userInput)
                .call()
                .entity(spec -> spec
                        .targetType(Ticket.class)    // 绑定业务POJO
                        .schema(manualSchema)        // 传入枚举约束Schema
                        .validateSchema()            // 核心:校验失败自动重试修正
                );
    }
}
3.2 正常运行效果

输入:用户反馈要退款,情况十分紧急

✅ 模型合规输出:

复制代码
{"category":"refund","priority":"urgent"}

✅ 业务直接使用强类型对象:

复制代码
Ticket ticket = structuredOutputDemo("用户反馈要退款,情况十分紧急");
System.out.println(ticket.getCategory()); // 输出:refund
System.out.println(ticket.getPriority()); // 输出:urgent
3.3 异常重试案例(核心亮点演示)

极端场景:即便Structured Outputs开启,模型偶尔输出非法值(如 refund_apply)

错误输出:{"category":"refund_apply","priority":"urgent"}

自动执行流程:

复制代码
1. 框架捕获原始返回JSON
2. 后端Schema校验拦截:category枚举非法
3. 自动将「错误信息+原始结果」回传给大模型
4. 触发自动重试,修正为合规结果
5. 重试成功后序列化为POJO返回

💡 优势:业务无需手动写重试、无需手动校验、无需手动解析JSON,全程框架闭环。

3.4 重试机制深度澄清(重要)

很多开发者误以为开启 Structured Outputs 后模型会自动重试,实际上并非如此:

  • OpenAI Structured Outputs :是模型生成阶段的强约束,在 token 生成时尽量输出合法结果,本身不带重试。即使开启,仍有极小概率输出非法数据(如枚举值错误)。
  • Spring AI .validateSchema() :是 Java 后端侧的校验 + 框架自动重试机制。当开启 validateSchema() 后,框架会在后端对模型返回的 JSON 执行 Schema 校验;若校验不通过,会自动构造提示词,将错误信息和上一轮错误 JSON 回传给大模型,要求其重新生成符合 Schema 的结果,并内置最大重试次数防止无限循环。整个过程对业务代码透明。

简言之:模型层硬约束尽量防错,后端校验兜底抓错,框架自动重试纠错,形成双层防护。

⚠️ 降级方案(如 TicketFallbackService)中,由于不支持 Structured Outputs 且未使用 .entity() 组件,代码仅手动校验并抛出异常,没有自动重试,需要开发者自行实现重试逻辑(如 for 循环、RetryTemplate 等)。

3.4 重试机制深度澄清(重要)

很多开发者误以为开启 Structured Outputs 后模型会自动重试,实际上并非如此:

  • OpenAI Structured Outputs :是模型生成阶段的强约束,在 token 生成时尽量输出合法结果,本身不带重试。即使开启,仍有极小概率输出非法数据(如枚举值错误)。
  • Spring AI .validateSchema() :是 Java 后端侧的校验 + 框架自动重试机制。当开启 validateSchema() 后,框架会在后端对模型返回的 JSON 执行 Schema 校验;若校验不通过,会自动构造提示词,将错误信息和上一轮错误 JSON 回传给大模型,要求其重新生成符合 Schema 的结果,并内置最大重试次数防止无限循环。整个过程对业务代码透明。

简言之:模型层硬约束尽量防错,后端校验兜底抓错,框架自动重试纠错,形成双层防护。

⚠️ 降级方案(如 TicketFallbackService)中,由于不支持 Structured Outputs 且未使用 .entity() 组件,代码仅手动校验并抛出异常,没有自动重试,需要开发者自行实现重试逻辑(如 for 循环、RetryTemplate 等)。

🚨 常见误区:用了 Structured Outputs 就不用后端校验了吗?

不行。 供应商提供的 Structured Outputs 能力只能降低生成阶段 的格式出错概率,并不代表服务端可以放弃边界防御。仍然需要独立完成以下校验:

  • 非法参数(如越权传入不属于当前用户的工单 ID)
  • 越权访问(防止用户通过构造请求访问他人数据)
  • 重放请求(防止恶意重复提交)
  • 业务状态冲突(如工单已被处理却再次提交)

Structured Outputs 解决的是"模型输出是否符合约定结构",而业务安全、权限校验、幂等控制必须由后端自行实现,二者不可混为一谈。

4、降级方案案例(不支持Structured Outputs的廉价模型)

针对开源模型、第三方低价兼容模型,不支持JSON Schema结构化入参,采用:JSON Mode + Prompt约束 + 后端手动校验重试

复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.options.ChatOptions;
import org.springframework.ai.chat.options.ResponseFormat;
import org.springframework.ai.schema.JsonSchema;
import org.springframework.ai.schema.validator.JsonSchemaValidator;
import org.springframework.ai.schema.validator.ValidationResult;
import org.springframework.stereotype.Service;

@Service
public class TicketFallbackService {

    private final ChatClient chatClient;
    private final ObjectMapper objectMapper;

    public TicketFallbackService(ChatClient.Builder builder, ObjectMapper objectMapper) {
        this.chatClient = builder.build();
        this.objectMapper = objectMapper;
    }

    public Ticket fallbackDemo(String userInput) throws Exception {
        // 1. 定义标准契约Schema
        String schemaStr = """
                {
                  "type":"object",
                  "required":["category","priority"],
                  "properties":{
                    "category":{"type":"string","enum":["pay","refund"]},
                    "priority":{"type":"string","enum":["urgent","normal"]}
                  },
                  "additionalProperties":false
                }
                """;
        JsonSchema schema = JsonSchema.parse(schemaStr);

        // 2. Prompt写入约束规则(软约束兜底)
        String prompt = """
                请严格解析用户输入为JSON,必须遵守以下规则:
                1. category 只能取值:pay / refund
                2. priority 只能取值:urgent / normal
                3. 必须返回两个字段,不允许额外字段
                用户输入:%s
                """.formatted(userInput);

        // 3. 开启基础JSON Mode
        String rawJson = chatClient.prompt()
                .user(prompt)
                .options(ChatOptions.builder().responseFormat(ResponseFormat.JSON_OBJECT).build())
                .call()
                .content();

        // 4. 后端强制校验+手动容错(注意:此处仅抛异常,无自动重试,需自行实现重试逻辑)
        JsonSchemaValidator validator = JsonSchemaValidator.create(schema);
        ValidationResult result = validator.validate(rawJson);
        if (!result.isValid()) {
            throw new RuntimeException("模型输出不合规:" + result.getErrors());
        }

        // 5. 手动反序列化
        return objectMapper.readValue(rawJson, Ticket.class);
    }
}

注意:上述降级方案代码仅演示校验与抛异常,未包含重试逻辑。在实际生产中,你需要在此处增加重试机制(例如循环调用、使用 Resilience4j 或 Spring Retry),以提升容错能力。

四、案例效果对比总表

实现方案 统一输入 典型模型输出 最终效果
纯JSON Mode 用户反馈要退款,情况十分紧急 {"category":"refund_apply","priority":"very_urgent"} 语法正常,业务数据全错,无任何拦截
仅Schema、不传入模型 用户反馈要退款,情况十分紧急 {"category":"refund_apply","priority":"very_urgent"} 模型输出依旧错误,仅手动校验能发现问题
Structured Outputs + validateSchema 用户反馈要退款,情况十分紧急 {"category":"refund","priority":"urgent"} 优先合规输出,异常自动重试修正,强类型返回
降级方案(JSON Mode+手动校验) 用户反馈要退款,情况十分紧急 合规/非法不定 非法输出可被后端拦截,需自行处理重试逻辑

五、三大能力选型对照表

方案 核心作用 保障能力 缺点 适用场景
JSON Mode API开启json_object,约束语法 仅保证JSON语法合法 不约束字段、枚举、必填,极易出现业务错误 简单Demo、临时测试、非核心链路
JSON Schema 定义业务契约规则 纯规则文档,无主动约束能力 单独使用无任何效果,必须配合模型API或后端校验 结构化输出规则定义、后端兜底校验
Structured Outputs 模型生成阶段强约束 约束字段、必填、枚举、多余字段 复杂Schema关键字不支持,无法100%信任 AI Agent、工单抽取、函数调用、线上核心业务链路

六、生产环境标准完整链路

这是适配所有Spring AI + OpenAI AI Agent项目的最终落地方案,兼顾可读性、稳定性、容错性:

  1. 定义 Lombok @Data 普通POJO 作为输出载体,适配所有Java项目

  2. 手写带枚举、必填、防多余字段的JSON Schema,弥补POJO注解短板

  3. 调用 .schema() 绑定自定义规则,开启OpenAI Structured Outputs

  4. 强制开启 .validateSchema(),实现后端校验+失败自动重试

  5. 校验通过直接获取强类型POJO对象,无手动解析、手动校验成本

❌ 三大禁止陋习(生产必规避)

  • 禁止仅靠Prompt文字+样例约束输出(纯软约束,极不稳定)
  • 禁止生产环境仅开启JSON Mode
  • 禁止开启Structured Outputs后省略后端Schema校验(模型存在容错盲区)

七、降级方案(适配不支持结构化输出的模型)

若使用廉价开源模型、第三方兼容模型,不支持Structured Outputs API,采用以下降级方案:

  1. 开启 JSON Mode 保证语法合法

  2. 将完整Schema规则、枚举取值范围写入Prompt,做软约束

  3. 后端手动执行 JSON Schema 校验

  4. 校验失败触发自定义重试逻辑,兜底保障业务可用

八、总结

  1. JSON Mode 是语法兜底,JSON Schema 是业务契约,Structured Outputs 是源头约束,三者层级递进、缺一不可;

  2. 业务开发无需强行使用Java17 Record,普通POJO + 手写Schema + 自动校验重试是最优工程方案;

  3. 所有AI Agent、大模型抽取核心链路,必须遵循「模型API硬约束 + 后端Schema校验兜底」的双层架构,彻底告别Prompt软约束的不稳定问题。

相关推荐
大连好光景1 天前
提示词工程01
prompt·cot·few-shot·提示词工程
Jing_jing_X2 天前
大模型只会输出token,是怎么“调用工具“的?
ai·agent·个人开发·ai应用开发
霸道流氓气质3 天前
Prompt 版本控制与 A/B 测试实战:语义版本管理、一致性哈希分流与 Thompson Sampling 的 Java 生产级实现
java·prompt·哈希算法
jason.zeng@15022074 天前
(六)Prompt 优化
python·ai·langchain·prompt·ai编程·llama
网络毒刘4 天前
把个人 Prompt 库迁到 AtomGit:版本化、评审与在 Cursor 中通过 @ 引用
prompt·cursor·atomgit·版本化·工具实践
jason.zeng@15022074 天前
(九)多轮对话式新增维修记录实现方案
python·prompt·交互·llama
IT_Octopus4 天前
【零基础入门 LLM 开发 · Day 10】:LangChain 入门——一条管道符串起 prompt 和模型
python·langchain·prompt
oscar9994 天前
用 Opik Prompt Library 管理提示词:版本、追踪与多轮对话的完整指南
prompt·opik
jason.zeng@15022074 天前
(十)分层架构的多文件工程
python·架构·prompt·交互·ai编程·llama