臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速Obsidian、双向链接与知识图谱
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
中华财险-公司项目
对外接口文档
基础数据码表对外接口(仅支持rpc调用)业务码表对外接口应用主数据对外接口
基础运营查询版本更新日志详情消息中心对外接口站内信对外接口站内信模板配置手册
权限中心权限中心对外接口文档(最新)权限中心对外接口文档前端ACL-CORE FACADE依赖版本
审计中心审计中心对外接口文档audit-center-facade版本
审批中心工作流迁审批流现状工作流迁审批流API能力替换方案老审批流接口文档审批中心接口文档审批中心业务回调FAQ
账号中心内部系统对接单点登录内部系统对接认证中心三方网页应用登录授权账号中心对外接口文档前端账号中心RPC接口文档账户变更对外广播消息文档账户中心对外接口HRMS外部员工变更广播消息文档HRMS外部员工对外接口
组织员工岗位🔥机构映射SDK接口文档员工岗位变更通知说明组织机构管营一体概念与用法组织员工对外广播消息文档组织员工岗位对外接口文档组织员工岗位对外接口文档前端组织员工岗位数据模型组织员工主数据业务场景案例organization-facade版本
hrms(大型人力资源外包管理系统)
项目架构说明HRMS Maven模块与依赖说明
项目说明0.流程中心模块0.组织模块说明1.业务线模块2.计划模块3.项目模块4.协议模块5.供应商模块6.合约域模块7.外包人员模块*核心:外包人员生命周期
项目运维外包人员项目编制差异排查与修复2.HRMS相关问题排查4.修改externalId(externalId和accountId不一致)
需求-系分
1.内部转外包0723外包人员关联历史内部账号系分新增外包人员关联历史账号内容(紧急0723上线)需求
2.工作岗位0820工作岗位0820需求工作岗位0820需求-系分
3.用工模式调整0917内部人员转外包用工系统需求2内部人员转外包用工系统需求-系分
sso(账号中心-单点登录)
项目架构说明aboss-sso项目架构入门AOP统一日志打印链路Maven多模块项目高级知识OAuth2.0
项目说明1.SSO-OAuth2.0与IDaaS登录流程2.外部账号创建流水号并发问题分析
AI
使用说明
第三方插件&技能简介Archify使用与安装指南Ponytail使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.完成可运行聊天接口
GitGit常用命令与Obsidian推送排查
Java
面试题Java基础与集合面试题
AtomicInteger原子计数与并发安全ConcurrentHashMap并发安全与计数Java线程、线程池与Future
python
基础Python基础语法
HTTPXHRMS员工详情接口调用(Python HTTPX)

一、💡 一句话理解

第 6 天核心结论

结构化输出,就是让模型返回可以被 Java 程序直接接收和处理的数据,而不是一段只能给人阅读的自然语言。JSON Schema 则是这份数据的“结构说明书”,规定字段名、字段类型、必填项和允许的取值。

本篇对应 0-学习路线 第 6 天:结构化输出、JSON Schema

今天只完成一件事:让 ChatClient 把用户问题识别成一个 Java 对象,并通过 HTTP 接口返回稳定的 JSON 结构。

本篇不提前学习:

  • SSE 流式响应
  • 会话记忆和 Redis
  • Function Calling / Tool Calling
  • RAG 和 Agent 工作流

二、🧭 理论:它是什么

2.1 结构化输出是什么

第 4 天的普通调用通常这样写:

String answer = chatClient
        .prompt("我的订单什么时候发货?")
        .call()
        .content();

返回值是 String。这适合直接展示给用户,但不适合让后端继续做判断。

例如,业务代码想知道用户到底是在问订单、物流,还是售后。模型如果返回下面这段文字:

用户主要是在咨询订单的发货状态,建议查询订单信息。

人可以看懂,但 Java 代码很难稳定判断。后端不能可靠地通过“包含订单两个字”来实现业务分支。

结构化输出会要求模型返回类似下面的对象:

{
  "intent": "ORDER_STATUS",
  "summary": "用户想查询订单的发货状态",
  "needHumanConfirm": false
}

这时 Java 可以直接读取:

CustomerIntent result = ...;
 
if ("ORDER_STATUS".equals(result.intent())) {
    // 后续第 11~20 天再接入订单查询工具
}

这里的核心变化是:

自然语言字符串
  → 结构明确的 JSON
  → Java 对象
  → 后端可以继续判断、保存或路由

2.2 JSON 是什么

JSON 是一种用文本表示数据的格式。它主要由对象、数组、字符串、数字、布尔值和 null 组成。

下面是一个 JSON 对象:

{
  "name": "张三",
  "age": 28,
  "vip": true,
  "tags": ["java", "agent"]
}

可以先这样理解:

JSON 内容白话理解Java 中常见的对应类型
{ ... }一组带名称的字段对象、Map
[ ... ]一组有顺序的数据List
"张三"字符串String
28数字intlongBigDecimal
true / false是或否boolean
null没有值null

JSON 只负责表达数据,不负责说明“哪些字段必须存在”“年龄必须是整数”或“状态只能取哪几个值”。这些约束需要 JSON Schema 描述。

2.3 JSON Schema 是什么

JSON Schema 是一份用于描述和校验 JSON 数据的 JSON 文档。

可以把它类比成 Java 的类定义,但要注意:这只是帮助理解的类比。Java 类主要用于定义程序中的对象,JSON Schema 主要用于描述 JSON 数据应该长什么样,并让校验器判断数据是否符合要求。

例如,下面的 Schema 规定:数据必须是对象,必须包含 intentsummaryneedHumanConfirm 三个字段,intent 只能从指定枚举值中选择。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "intent": {
      "type": "string",
      "enum": [
        "ORDER_STATUS",
        "LOGISTICS",
        "INVENTORY",
        "AFTER_SALE",
        "POLICY",
        "OTHER"
      ]
    },
    "summary": {
      "type": "string"
    },
    "needHumanConfirm": {
      "type": "boolean"
    }
  },
  "required": [
    "intent",
    "summary",
    "needHumanConfirm"
  ]
}

Schema 和数据的关系是:

JSON Schema:规定应该长什么样
       ↓ 校验
JSON 数据:实际返回了什么

JSON Schema 官方教程将待校验的 JSON 称为 instance,将描述结构和约束的文档称为 schema。typepropertiesrequired 是最常用的基础关键字。JSON Schema 官方入门教程

2.4 结构化输出和普通 JSON 的区别

“让模型返回 JSON”和“让模型返回符合 Schema 的结构化输出”不是一回事。

做法能保证什么不能保证什么
普通文本模型返回一段文本不保证是 JSON
Prompt 要求返回 JSON模型可能更容易返回 JSON仍可能多说解释、缺字段或类型错误
JSON mode通常保证结果是合法 JSON不一定保证字段完全符合你的业务结构
JSON Schema 结构化输出在支持的模型接口中约束字段结构不保证业务事实一定正确,也不替代业务校验

所以结构化输出解决的是“返回数据的形状”,不是“数据内容一定真实”。

例如,模型可能按照 Schema 正确返回:

{
  "intent": "ORDER_STATUS",
  "summary": "订单已经签收",
  "needHumanConfirm": false
}

但模型并没有查询订单系统,summary 中的事实仍然不能直接当成真实订单状态。真正的订单状态要等后续接入订单查询工具后,以业务系统返回的数据为准。

三、⚙️ 理论:它是怎么工作的

3.1 结构化输出的完整链路

在 Spring AI 中,结构化输出大致经过下面的过程:

Java 定义目标类型 CustomerIntent

Spring AI 根据 Java 类型生成 JSON Schema

把格式要求加入 Prompt,或发送给模型的原生结构化输出接口

模型生成 JSON 文本

Spring AI 解析 JSON

转换为 CustomerIntent Java 对象

Controller 返回业务 JSON

Spring AI 官方当前的高层 API 使用 .entity(...) 获取目标类型,而不是使用 .content() 获取原始字符串:

CustomerIntent result = chatClient
        .prompt()
        .user("识别用户意图:我的订单什么时候发货?")
        .call()
        .entity(CustomerIntent.class);

CustomerIntent.class 就是告诉 Spring AI:“我希望模型的结果最终转换成这个 Java 类型”。Spring AI 会根据这个类型生成 Schema,并将模型输出反序列化成对象。Spring AI Structured Output

3.2 ChatClient.content().entity()

这两个方法都位于 .call() 之后,但返回目标不同:

写法返回结果适用场景
.content()模型生成的 String普通问答、聊天回复
.entity(Result.class)指定的 Java 对象分类、抽取、路由、结构化业务结果
.responseEntity(Result.class)Java 对象加完整响应信息同时需要 Token、请求元数据和结构化结果

本篇使用 .entity(CustomerIntent.class),因为第 6 天的目标是先完成“模型输出 → Java 对象”的转换。

entity(...) 需要完整响应后才能解析对象,因此它属于 .call() 路径,不适用于 .stream() 流式输出。流式输出返回的是文本片段,不能在每个片段到达时直接得到完整 Java 对象。Spring AI Structured Output

3.3 Spring AI 如何从 Java 类型生成 Schema

本篇定义一个 Java record

public record CustomerIntent(
        String intent,
        String summary,
        boolean needHumanConfirm
) {
}

它表达了三件事:

  • intent 是字符串。
  • summary 是字符串。
  • needHumanConfirm 是布尔值。

在实际调用时,Spring AI 的 BeanOutputConverter 可以根据 Java 类或 record 生成 JSON Schema,并将模型输出转换为目标类型。BeanOutputConverter API

可以把它记成:

Java record
  → BeanOutputConverter
  → JSON Schema
  → 模型 JSON 输出
  → CustomerIntent

这里不是把模型“变成了 Java 程序”,而是由 Java 类型作为目标格式,帮助模型和后端约定结果形状。

3.4 JSON Schema 中最常用的关键字

本篇只掌握后续 Agent 开发最常用的一小组关键字:

关键字作用示例
type限定数据类型"type": "object"
properties定义对象有哪些字段"properties": { "name": ... }
required指定必须存在的字段"required": ["name"]
enum限定允许的固定值"enum": ["ORDER_STATUS", "OTHER"]
items描述数组中每个元素的结构"items": { "type": "string" }
description给字段补充说明"description": "用户意图"

有两个容易混淆的点:

第一,字段出现在 properties 中,不代表它一定必填。是否必填要看它是否出现在同一层级的 required 数组中。

第二,字段是必填的,不代表字段内容一定正确。required 只能说明字段不能缺失,不能证明订单状态真实存在,也不能替代权限校验。

3.5 两种结构化输出方式

Spring AI 当前提供两条主要路径。

3.5.1 Prompt-based 结构化输出

默认的 .entity(...) 路径会把 Schema 或格式说明加入提示词,让模型按照要求输出;模型返回后,Spring AI 再解析成 Java 对象。

Java 类型
  → 生成 Schema
  → Schema 作为格式要求加入 Prompt
  → 模型返回 JSON 文本
  → Spring AI 转换为 Java 对象

优点是兼容性较好,适合本篇使用的 OpenAI 兼容模型配置。缺点是它属于“要求模型遵守”,默认不是 API 层面的强制约束。

Spring AI 官方将这种转换描述为 best effort:模型不一定完全按照要求返回,因此业务代码仍需处理解析失败和校验失败。Spring AI Output Converters

3.5.2 Provider-native 结构化输出

如果模型供应商原生支持 JSON Schema 结构化输出,可以使用:

CustomerIntent result = chatClient
        .prompt()
        .user("识别用户意图:我的订单什么时候发货?")
        .call()
        .entity(CustomerIntent.class, spec -> spec.useProviderStructuredOutput());

这会把 Schema 作为模型 API 的结构化输出参数发送,而不是只放在 Prompt 文本里。它通常更可靠,但依赖具体模型和供应商的支持情况。Spring AI 当前文档说明,原生支持检测依赖底层 Chat Options;如果模型不支持,不能把这段代码当成所有供应商都必然有效。Spring AI Provider-Native Structured Output

本篇的完整实践先使用默认 .entity(...),因为它不依赖某个具体供应商的原生能力。确认当前模型支持原生 Schema 后,再在单独的实验中开启 useProviderStructuredOutput()

3.6 Schema 校验和自动纠错

Spring AI 当前支持在实体解析时开启 Schema 校验:

CustomerIntent result = chatClient
        .prompt()
        .user("识别用户意图:我的订单什么时候发货?")
        .call()
        .entity(CustomerIntent.class, spec -> spec.validateSchema());

当模型返回的数据缺字段、类型错误或结构不符合 Schema 时,Spring AI 会把校验错误补充到下一次请求中并重试。当前官方文档说明,默认最多尝试 3 次;启用校验后需要完整响应,因此不支持流式解析。Spring AI Schema Validation

它解决的是“结构不符合要求”,不是“业务结果一定正确”。例如:

  • 可以校验 needHumanConfirm 是否为布尔值。
  • 可以校验 intent 是否属于枚举值。
  • 不能证明订单是否真的已经发货。
  • 不能替代登录态、权限和数据库校验。

四、🚀 实践:从准备到验证

4.1 前置准备

4.1.1 已完成前置学习

本篇沿用:

如果第 4 天的 GET /test1 还不能返回模型回答,请先修复第 4 天的调用链,再继续本篇。

4.1.2 已有 Spring Boot 项目和 Spring AI 依赖

沿用当前项目的 pom.xml。至少需要已经配置第 4 天使用的 Spring AI OpenAI Starter:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

不要为了本篇重复添加第二份同名依赖。Spring AI 版本由当前项目的 BOM 或版本管理方式统一决定。

本文代码使用当前 Spring AI 结构化输出 API;如果你的项目版本较旧,先以项目实际版本的官方文档为准。

4.1.3 沿用已有模型配置

继续使用第 4 天已经验证过的 application.properties 和 IntelliJ IDEA 运行配置。例如,配置文件中可以保留模型连接信息:

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

如果你的实际模型名、地址或供应商不同,以当前项目已经验证成功的配置为准,不要机械复制上面的值。

API Key 继续放在 IntelliJ IDEA 的运行配置环境变量中,不写入 Java 源码,也不要提交到 Git。

4.1.4 今天的实践目标

新增一个接口:

POST /test2

请求正文:

{
  "question": "我的订单什么时候发货?"
}

接口内部调用模型完成意图识别,最终返回:

{
  "intent": "ORDER_STATUS",
  "summary": "用户想查询订单的发货状态",
  "needHumanConfirm": false
}

4.2 可以拿来干什么

4.2.1 用于意图识别和任务路由

用途:把用户自然语言转换成固定的意图枚举,方便后端决定下一步进入哪个业务分支。

前置:需要已经注入 ChatClient,并定义目标 Java 类型:

public record CustomerIntent(
        String intent,
        String summary,
        boolean needHumanConfirm
) {
}

调用代码:

CustomerIntent result = chatClient
        .prompt()
        .system("""
                你是企业售后系统的意图识别助手。
                只做意图识别,不查询订单,不修改数据,不创建工单。
                intent 只能是 ORDER_STATUS、LOGISTICS、INVENTORY、AFTER_SALE、POLICY、OTHER。
                """)
        .user("请识别用户问题:我的订单什么时候发货?")
        .call()
        .entity(CustomerIntent.class);

输入是用户问题,处理过程是模型根据系统规则选择意图,输出是 CustomerIntent 对象。

适用场景:售后路由、客服分类、FAQ 分类、后续 Tool Calling 前的任务判断。本篇只完成分类,不执行任何业务工具。

4.2.2 用于从文本中抽取固定字段

用途:从一段自然语言中提取订单号、问题类型和是否需要人工确认等字段。

前置:定义字段结构,字段名要稳定,缺失情况要在 Prompt 中明确说明:

public record AfterSaleRequest(
        String orderNo,
        String issueType,
        String description,
        boolean needHumanConfirm
) {
}

调用代码:

AfterSaleRequest result = chatClient
        .prompt()
        .system("""
                你是售后信息抽取助手。
                只从用户输入中提取已有信息,不要猜测不存在的订单号。
                没有订单号时,orderNo 返回空字符串。
                """)
        .user("用户输入:订单 A20260821001 收到的商品有破损,我想申请售后。")
        .call()
        .entity(AfterSaleRequest.class);

输入是自然语言,处理过程是模型按 Java 类型对应的结构返回字段,输出是 AfterSaleRequest

适用场景:表单预填、客服信息归一化、接口参数准备。实际创建售后工单时,仍要进行订单存在性、用户归属、权限和幂等校验。

4.2.3 用于让模型结果进入普通 Java 分支

用途:结构化结果可以直接进入 Java 的 switch 或条件判断,而不需要对自然语言做字符串匹配。

前置:已经得到 CustomerIntent 对象:

CustomerIntent result = ...;

业务代码可以这样写:

String nextStep = switch (result.intent()) {
    case "ORDER_STATUS" -> "准备进入订单查询流程";
    case "LOGISTICS" -> "准备进入物流查询流程";
    case "INVENTORY" -> "准备进入库存查询流程";
    case "AFTER_SALE" -> "准备进入售后流程";
    case "POLICY" -> "准备进入制度知识库";
    default -> "转为人工或普通问答";
};

输入是模型已经转换好的 CustomerIntent,处理过程是 Java 根据枚举值路由,输出是下一步业务动作描述。

适用场景:Agent 工作流的意图路由。但第 6 天只练习“识别并返回结构”,不在本篇实现真实工具调用和多步骤工作流。

4.3 完整实践:用结构化输出识别售后意图

4.3.1 创建请求类型和结果类型

要解决的问题:定义 HTTP 输入和模型输出的两个边界对象。

AiController 中使用两个 record

public record IntentRequest(String question) {
}
 
public record CustomerIntent(
        String intent,
        String summary,
        boolean needHumanConfirm
) {
}

IntentRequest 是前端传给后端的数据,CustomerIntent 是模型输出后由 Spring AI 转换得到的数据。

4.3.2 编写完整 Controller

文件位置:

src/main/java/com/afyke/ai/controller/AiController.java

如果当前项目已经存在 AiController,请在原文件中合并 /test2 方法,不要创建第二个同名 Controller。下面是本篇最小可运行版本:

package com.afyke.ai.controller;
 
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class AiController {
 
    private final ChatClient chatClient;
 
    public AiController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }
 
    @GetMapping("/test1")
    public String test1() {
        return chatClient
                .prompt("Spring Boot 和 Spring AI 有什么关系?")
                .call()
                .content();
    }
 
    @PostMapping("/test2")
    public CustomerIntent test2(@RequestBody IntentRequest request) {
        return chatClient
                .prompt()
                .system("""
                        你是企业售后系统的意图识别助手。
 
                        你的任务是识别用户问题,不要查询订单,不要修改数据,不要创建工单。
                        intent 只能使用以下值:
                        ORDER_STATUS、LOGISTICS、INVENTORY、AFTER_SALE、POLICY、OTHER。
 
                        判断规则:
                        - 询问订单状态、发货状态、订单是否完成:ORDER_STATUS
                        - 询问快递、物流、配送进度:LOGISTICS
                        - 询问商品库存、是否有货:INVENTORY
                        - 申请退货、退款、换货或售后:AFTER_SALE
                        - 询问企业制度、流程或售后规则:POLICY
                        - 无法归入以上类别:OTHER
 
                        summary 使用简短中文说明用户的问题。
                        只有涉及退款、退货、换货、创建工单等可能改变业务状态的请求时,
                        needHumanConfirm 才返回 true;普通查询返回 false。
                        """)
                .user(user -> user
                        .text("请识别下面的用户问题:{question}")
                        .param("question", request.question()))
                .call()
                .entity(CustomerIntent.class);
    }
 
    public record IntentRequest(String question) {
    }
 
    public record CustomerIntent(
            String intent,
            String summary,
            boolean needHumanConfirm
    ) {
    }
}

代码中的关键点:

  • ChatClient.Builder 的注入方式沿用第 4 天。
  • .system(...) 放稳定的分类规则,沿用第 5 天的 Prompt 设计。
  • .user(...) 放本次 HTTP 请求传入的具体问题。
  • .entity(CustomerIntent.class) 要求 Spring AI 将模型输出转换为 CustomerIntent
  • needHumanConfirm 只是模型返回的判断结果,不是安全权限控制。

4.3.3 编写验证请求

要解决的问题:向后端传入一个真实问题,验证模型结果能否转换成稳定 JSON。

在项目根目录的 request.http 中添加:

#### 4.3.3.1 第 6 天:结构化输出和 JSON Schema
POST http://localhost:8080/test2
Content-Type: application/json
 
{
  "question": "我的订单什么时候发货?"
}

再补充一个售后问题,用于观察 needHumanConfirm 的变化:

#### 4.3.3.2 第 6 天:售后意图
POST http://localhost:8080/test2
Content-Type: application/json
 
{
  "question": "商品有破损,我想申请退货退款。"
}

4.3.4 启动项目

按照当前项目已有工作流,在 IntelliJ IDEA 中运行 AiApplication

启动前确认:

  • DEEPSEEK_API_KEY 已配置在运行配置环境变量中。
  • 第 4 天的模型调用已经验证成功。
  • 8080 端口没有被其他程序占用。

如果使用 Maven Wrapper,也可以在项目根目录执行:

./mvnw spring-boot:run

4.3.5 预期结果

第一个请求应返回类似:

{
  "intent": "ORDER_STATUS",
  "summary": "用户想查询订单的发货状态",
  "needHumanConfirm": false
}

第二个请求应返回类似:

{
  "intent": "AFTER_SALE",
  "summary": "用户想申请退货退款",
  "needHumanConfirm": true
}

模型生成的 summary 文字可能不同,但验收重点是:

  • HTTP 状态为 200
  • 返回结果是 JSON 对象,而不是 Markdown 或解释性长文本。
  • 存在 intentsummaryneedHumanConfirm 三个字段。
  • intent 使用约定的枚举值。
  • needHumanConfirm 是布尔值,不是字符串 "true""false"

4.3.6 查看 Java 类型转换结果

如果在 Controller 中临时打印:

CustomerIntent result = ...;
System.out.println(result.intent());
System.out.println(result.summary());
System.out.println(result.needHumanConfirm());

就可以看到模型返回的 JSON 已经被 Spring AI 转换为 Java record,后端不需要自己从字符串中查找字段。

完整调用链如下:

POST /test2
  → Spring MVC 读取 IntentRequest
  → ChatClient 组装 system/user 消息
  → Spring AI 根据 CustomerIntent 生成格式要求
  → 模型返回 JSON
  → Spring AI 解析为 CustomerIntent
  → Spring MVC 序列化为 HTTP JSON

4.3.7 增强结构校验

如果当前项目使用的 Spring AI 版本支持 validateSchema(),可以将最后一段调用改成:

.call()
.entity(CustomerIntent.class, spec -> spec.validateSchema());

它适合对结构正确性要求更高的场景:模型返回缺字段、字段类型错误或不符合 Schema 时,Spring AI 会进行 Schema 校验并尝试自我纠正。

如果当前项目编译时提示 validateSchema() 不存在,说明项目使用的 Spring AI 版本与当前文档 API 不一致。先保留基础写法:

.call()
.entity(CustomerIntent.class);

不要为了绕过版本问题而复制不匹配的依赖版本;先检查项目 BOM 和当前版本对应的 Spring AI 官方文档。

4.3.8 常见报错排查

4.3.8.1 401 Unauthorized

原因通常是 API Key 未读取、环境变量名称不一致,或运行配置没有应用到当前启动任务。

检查:

  • application.properties 中是否使用 ${DEEPSEEK_API_KEY}
  • IntelliJ IDEA 运行配置中是否存在同名环境变量。
  • 是否误把占位符文本当成真实 Key。
  • 是否把 Key 写进了源码但配置仍然读取另一个变量。
4.3.8.2 model not found 或模型不可用

原因通常是模型名与供应商当前提供的名称不一致,或者当前账号没有权限使用该模型。

处理方式:

  • 沿用第 4 天已经验证成功的模型名称。
  • 以模型供应商当前官方文档为准。
  • 不要把示例中的模型名当成永久不变的固定值。
4.3.8.3 JSON 解析失败

常见返回可能是:

Here's the result:
```json
{ ... }

这说明模型在 JSON 外面添加了说明文字或 Markdown 代码围栏。`BeanOutputConverter` 默认要求返回内容可以直接解析为目标 JSON,不能只依赖“模型应该听话”。

处理顺序:

1. 在 `system` 和 `user` 中明确要求“只返回 JSON,不要解释”。
2. 缩小 Schema 和 Prompt,先确保最小字段可以正确返回。
3. 使用当前 Spring AI 支持的 `validateSchema()` 做校验和重试。
4. 如果模型经常输出代码围栏,再考虑自定义 `StructuredOutputConverter`,不要用字符串截取掩盖所有解析问题。

##### 4.3.8.4 字段缺失或类型错误

例如模型返回:

```json
{
  "intent": "ORDER_STATUS",
  "summary": "查询订单",
  "needHumanConfirm": "false"
}

这里 needHumanConfirm 是字符串,不是 JSON 布尔值。应检查:

  • Java 字段类型是否写成了 boolean
  • Prompt 是否明确了字段含义和类型。
  • 是否开启了 Schema 校验。
  • 当前模型是否支持 provider-native structured output。
4.3.8.5 把结构化输出误当成业务安全控制

下面的做法是不安全的:

if (result.needHumanConfirm()) {
    // 直接创建售后工单
}

结构化输出只能告诉程序模型返回了一个布尔字段,不能证明用户有权限,也不能证明操作经过了人工确认。

第 19 天学习权限校验和人工确认,第 20 天验收 Tool Calling 时,再把这些判断放到 Java 服务和数据库事务中完成。

五、📌 总结

  • 快速回顾:普通 .content() 返回字符串,结构化输出使用 .entity(...) 返回 Java 类型。
  • 快速回顾:JSON 是数据格式,JSON Schema 是描述和校验 JSON 结构的规则。
  • 快速回顾:typepropertiesrequiredenum 是今天最重要的 Schema 关键字。
  • 快速回顾:Spring AI 可以根据 Java record 生成 Schema,并把模型 JSON 转换成 Java 对象。
  • 快速回顾:默认结构化输出是 best effort;需要更强可靠性时,再考虑 validateSchema() 或模型原生结构化输出。
  • 快速回顾:结构正确不等于事实正确,结构化输出不替代权限、数据库和业务校验。
  • 快速回顾:entity(...) 需要完整响应,不与今天的 SSE 流式输出混用。

记忆句:Java 类型定义结果形状,JSON Schema 描述约束,模型返回 JSON,Spring AI 转成对象,业务代码再做真实校验。

第 6 天验收:

  • 能解释 JSON 和 JSON Schema 的区别。
  • 能说出 typepropertiesrequiredenum 的用途。
  • 能使用 .entity(CustomerIntent.class) 获得结构化 Java 对象。
  • 能通过 POST /test2 返回固定字段的 JSON。
  • 能区分结构校验、事实校验和权限校验。

相关笔记:0-学习路线2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板

六、🔗 官方资料