第3篇:《AI的JSON强迫症:我让大模型只回答能解析的格式》

承上:上一篇我们把提示词管得服服帖帖,但AI的回答还是一段自由文本。前端同事说"你返回的文案我直接展示还行,但要做二次处理就得靠正则硬匹配,太不靠谱了"。确实,一个正经的后端接口,返回值怎么能没有Schema?

1. 问题场景:自由文本的"解析地狱"

先看我们现在拿到的AI回复:

arduino 复制代码
用户:帮我查一下订单号ORD123456789的状态

AI回复:
您的订单ORD123456789当前状态是"已发货",快递单号为SF1234567890,
预计2026年7月25日送达。如有疑问请联系客服。

作为人类读着很舒服,但程序要从中提取 订单状态快递单号预计送达时间,就得写一堆正则:

java 复制代码
String status = response.replaceAll(".*状态是"", "").replaceAll("".*", "");  // 灾难
String tracking = response.replaceAll(".*快递单号为", "").replaceAll(",.*", "");  // 更灾难

这段代码的脆弱性,任何一个后端老鸟看到都会血压飙升------AI换个说法,解析全崩。

我们需要的是:让AI直接返回JSON,像调一个正经API一样。

2. 方案对比:三种让AI输出结构化数据的方式

Spring AI提供了三种方式,复杂度递增,适用场景不同:

方式 原理 适用场景 可靠性
① Prompt约束 在提示词里说"请返回JSON" 简单场景、快速验证 ⭐⭐
② Bean输出 直接让AI映射到Java类 明确的DTO结构 ⭐⭐⭐
③ 结构化转换器 自动将结果转为指定类型 复杂嵌套结构 ⭐⭐⭐⭐

我们按顺序来,先知道怎么"野路子"做,再学会正经做法。

3. 方式一:Prompt约束(最原始但最灵活)

3.1. 代码实现

java 复制代码
package com.yunxi.ai.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class ChatService {

    private final ChatClient chatClient;

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

    /**
     * 从用户的输入提取用户信息
     * @param message
     * @return
     */
    public String getUserInfo(String message) {
        String prompt = """
                请根据用户输入的信息:%s
                提取姓名,手机号,省,市,区县,详细地址,并严格按照以下JSON格式返回,不要包含任何其他内容
                {
                    "name": "姓名",  
                    "phone": "手机号",
                    "province": "省",
                    "city": "市",
                    "district": "区县",
                    "address": "详细地址"
                }
                只返回JSON,不要解释。
                """.formatted(message);
        return chatClient.prompt()
                .system("你是一个专业的地址提取助手,可以提取用户信息")
                .user(prompt)
                .call()
                .content();
    }
}

3.2. 测试结果

txt 复制代码
http://localhost:9999/chat/userInfo?message=shiyunxi,17865162833,北京市瑞平家园

返回:

json 复制代码
{
	"name": "shiyunxi",
	"phone": "17865162833",
	"province": "北京市",
	"city": "北京市",
	"district": "朝阳区",
	"address": "瑞平家园"
}

看起来完美,但有两个隐患:

  1. AI有时候不听话 :可能返回 好的,这是您要的JSON:{...},前面多了解释文字
  2. 字段可能缺失或改名 :AI可能自作主张把 name 写成 user_name

4. 方式二:Bean输出(Spring AI原生支持)

Spring AI提供了一个 entity() 方法,可以直接把AI返回的JSON映射到Java类。这才是正经做法。

4.1. 代码实现

java 复制代码
package com.yunxi.ai.dto;

import lombok.Data;

@Data
public class UserInfo {
    private String name;
    private String phone;
    private String province;
    private String city;
    private String district;
   
java 复制代码
public UserInfo getUserInfoV2(String message) {
        return chatClient.prompt()
                .system("你是一个专业的地址提取助手,可以提取用户信息")
                .user(message)
                .call()
                .entity(UserInfo.class);
    }
java 复制代码
@GetMapping("/chat/userInfoV2")
    public UserInfo getUserInfoV2(@RequestParam String message) {
        return chatService.getUserInfoV2(message);
    }

4.2. 测试结果

txt 复制代码
http://localhost:9999/chat/userInfoV2?message=shiyunxi,17865162833,北京市瑞平家园

返回标准JSON:

json 复制代码
{
	"name": "shiyunxi",
	"phone": "17865162833",
	"province": "北京市",
	"city": "北京市",
	"district": "",
	"address": "瑞平家园"
}

关键点.entity(UserInfo.class) 背后做了什么?

  1. Spring AI在发送请求时,自动在Prompt里追加了格式约束
  2. 收到响应后,用Jackson反序列化成 UserInfo 对象
  3. 如果AI返回的不是合法JSON,直接抛异常

5. 方式三:结构化转换器(精准控制Schema)

.entity() 虽然方便,但它对JSON Schema的控制是隐式的。如果你需要精确控制字段类型、必填/可选、描述信息,要用 StructuredOutputConverter

5.1. 代码实现

java 复制代码
@Data
public class UserInfo {

    @JsonPropertyDescription("姓名")
    private String name;
    @JsonPropertyDescription("手机号")
    private String phone;
    @JsonPropertyDescription("省份")
    private String province;
    @JsonPropertyDescription("城市")
    private String city;
    @JsonPropertyDescription("区县")
    private String district;
    @JsonPropertyDescription("详细地址")
    private String address;
}
java 复制代码
public UserInfo getUserInfoV3(String message) {
        return chatClient.prompt()
                .system("你是一个专业的地址提取助手,可以提取用户信息")
                .user(message)
                .call()
                .entity(new BeanOutputConverter<>(UserInfo.class));
    }

区别在于 new BeanOutputConverter<>(OrderInfoV2.class),它会读取 @JsonPropertyDescription 等注解,生成更精确的Schema约束发给AI。AI看到的不再是笼统的"返回JSON",而是:

txt 复制代码
Here is the JSON Schema instance your output must adhere to:
```{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "address" : {
      "type" : "string",
      "description" : "详细地址"
    },
    "city" : {
      "type" : "string",
      "description" : "城市"
    },
    "district" : {
      "type" : "string",
      "description" : "区县"
    },
    "name" : {
      "type" : "string",
      "description" : "姓名"
    },
    "phone" : {
      "type" : "string",
      "description" : "手机号"
    },
    "province" : {
      "type" : "string",
      "description" : "省份"
    }
  },
  "additionalProperties" : false
}```

这种精确约束下,AI返回的结果会更符合预期。

如何可以通过日志看到请求大模型的数据呢?

5.2. 打印大模型请求日志

5.2.1. 配置内置的 SimpleLoggerAdvisor

这是最快的方式,Spring AI 已经提供了一个现成的日志增强器 SimpleLoggerAdvisor

在你的 ChatClient 配置中,将它加到 defaultAdvisors() 里就行。

java 复制代码
private final ChatClient chatClient;

    public ChatService(ChatClient.Builder builder) {
        this.chatClient = builder
                .defaultAdvisors(new MyLoggerAdvisor())
                .build();
    }

5.2.2. 配置日志级别

这是最关键的一步,也是最容易漏掉的。SimpleLoggerAdvisor 默认用的是 DEBUG 级别,而 Spring Boot 默认日志级别是 INFO,所以不加配置是看不到日志的。

你需要在 application.yml 里把它的包日志级别调低:

yaml 复制代码
logging:
  level:
    # 让这个包的日志打印出来
    org.springframework.ai.chat.client.advisor: DEBUG

6. 避坑指南

6.1. 坑1:AI在JSON前后加了"废话"

json 复制代码
好的,根据您的查询,返回结果如下:
{
    "orderId": "ORD123456789",
    ...
}
希望这个结果对您有帮助!

解决方式

  • 方式一(Prompt约束):在提示词里强调"只返回JSON,不要任何解释"
  • 方式二/三(Bean输出):Spring AI会自动提取JSON块,多数情况下能正确处理
  • 终极方案:手动清理,取出第一个 { 和最后一个 } 之间的内容

6.2. 坑2:字段名对不上

AI有时会自作主张改字段名,比如你定义的是 trackingNumber,AI返回 tracking_number

解决 :在DTO上加 @JsonProperty 指定多个可能的名称,或在提示词里给字段加描述约束。方式三最靠谱。

6.3. 坑3:必填字段返回null

解决:在提示词里明确"如果某个字段无法确定值,请填入'未知'而不是省略该字段"。

6.4. 坑4:数组/嵌套对象解析失败

如果DTO里有 List<OrderItem> 这样的复杂类型,AI返回的嵌套JSON可能结构不对。

解决:拆分查询,先让AI返回简单的扁平结构,再用多次调用来组装复杂对象。

6.5. 坑5:数值类型返回了字符串

期望 "price": 99.00,AI返回 "price": "99.00元"

解决 :在 @JsonPropertyDescription 里明确类型和格式:"price(number):商品价格,纯数字,不含单位"****

7. 本篇小结

这一篇我们解决了"AI返回自由文本不好处理"的问题:

方式 代码量 可靠性 适用场景
Prompt约束 一般 快速验证、一次性脚本
.entity(Class) 极少 较高 明确的DTO结构
BeanOutputConverter 略多 最高 需要精确控制Schema

核心心法:把AI当作一个返回JSON的微服务,而不是一个聊天机器人。 用约束取代猜测,用DTO取代字符串切割。

现在我们的AI已经能返回规规矩矩的结构化数据了,但还有一件事让人不爽:每次调用都要等上好几秒才能拿到完整结果,用户体验跟传统HTTP接口没区别。 下一篇,我们来点丝滑的------Stream流式输出,让AI像ChatGPT一样一个字一个字往外蹦。


本文与DeepSeek协作完成

相关推荐
ServBay2 小时前
Gemini 3.6 Flash 正式发布:省了 Token,但智商没跟上?
aigc·ai编程·gemini
AI大模型-小雄2 小时前
Codex 长任务总要重新开始?买 Credits 还是升级 ChatGPT Pro
人工智能·chatgpt·程序员·ai编程·codex·ai办公·chatgpt pro
Shirley~~2 小时前
Code-Review-Graph:面向 AI 辅助代码审查的结构化上下文引擎
前端·ai编程
Onesoft%J1ao3 小时前
【2026年7月份有感】VibeCoding的入门到免费API的精通
aigc·免费api·ai编程·vibecoding
未秃头的程序猿3 小时前
给公司做了个AI客服Agent,用的Spring AI 1.0,3天上线领导拍板了
java·后端·ai编程
Ai拆代码的曹操3 小时前
opencode 源码调试环境搭建:bun install → F5 断点全流程
ai编程·opencode·源码拆解
栩栩云生4 小时前
命令行的门槛从"会写"变成了"会拦"
安全·ai编程·命令行
东小西4 小时前
第2篇:《Prompt不是玄学:我把提示词放到了配置文件里》
openai·ai编程
「QT(C++)开发工程师」4 小时前
AI Agent 术语
人工智能·ai作画·aigc·ai编程·ai写作