四
结构化输出:让 AI 吐 JSON 而不是废话
Structured Output
大模型默认返回自然语言——"根据您的信息,金额是 128.5 元,日期是 2026 年 9 月 16 日……"。但程序要的不是这段话,是一个能直接解析的对象。结构化输出就是让 AI 严格按你给的格式返回,Spring AI 直接帮你转成 Java Bean。
论为什么需要结构化输出
没有结构化时,你得写正则从一大段话里抠数字,抠错就崩。有了结构化输出,Spring AI 在后台告诉模型"你必须返回 JSON,字段叫 amount、date",然后自动把 JSON 反序列化成 Java 对象。从"猜 AI 说了啥"变成"拿一个确定类型",企业可用。
定义一个 Java Bean,让 AI 把结果填进来
// 你想要 AI 从合同里抽出来的字段
public class ContractInfo {
private String partyA; // 甲方
private String partyB; // 乙方
private BigDecimal amount; // 金额
private LocalDate signDate; // 签署日期
// getters / setters 省略
}
一行调用,直接拿到 ContractInfo 对象
String contractText = "甲方:某某公司,乙方:某某科技,金额 128000 元,2026年9月16日签订……";
ContractInfo info = chatClient.prompt()
.user("从下面合同里抽取信息:" + contractText)
.call()
.entity(ContractInfo.class); // 关键!直接转成对象
System.out.println(info.getAmount()); // 128000,程序直接用
用结构化输出做"信息抽取"实战
信息抽取是结构化输出最典型的应用:从一大段非结构化文本(合同、简历、邮件)里,把关键字段抽成结构化数据,直接进数据库。下面是一个从合同抽字段的完整例子。
抽出来的字段 Bean
public class ContractInfo {
private String title; // 合同标题
private String partyA; // 甲方公司名
private String partyB; // 乙方公司名
private BigDecimal amount; // 合同金额
private LocalDate signDate; // 签署日期
private Integer durationMonths; // 合同期限(月)
// getters / setters 省略
}
抽取调用 + 字段校验
String contract = "本合同由甲方北京某某科技与乙方上海某某贸易签订,"
+ "合同金额人民币 580000 元,签署日期 2026 年 9 月 1 日,有效期 12 个月。";
ContractInfo info = chatClient.prompt()
.system("你是合同信息抽取器,只输出 JSON,不要解释。")
.user(contract)
.call()
.entity(ContractInfo.class);
// 拿到对象后必须校验,别直接存库
if (info.getAmount() == null || info.getAmount().signum() <= 0) {
throw new IllegalStateException("合同金额为空或非法,人工复核:" + info);
}
Map 输出与 JSON Schema
除了 Bean,还能直接拿 Map<String,Object>,字段不固定时用。更精细的场景可以自定义 JSON Schema,精确控制每个字段类型、是否必填。企业级抽取建议用 Bean——字段固定、类型安全、能加校验。
拿 Map 输出(字段不固定时)
Map<String, Object> result = chatClient.prompt(q)
.call()
.entity(new ParameterizedTypeReference<Map<String, Object>>() {});
// 字段不固定、探索性需求时用,生产建议还是定成 Bean
结构化输出不是 100% 可靠
AI 偶尔会少返字段、多返字段、金额算错。拿到对象后一定要做校验:金额不能是负数、日期不能是未来。该抛异常抛异常,别直接存库。RAG 和结构化输出都是"提高准确率",不是"消除幻觉"。
完整案例:带 JSON Schema 注解的实体抽取
想让 AI 严格按字段类型和描述返回,给 Bean 字段加 @JsonPropertyDescription,Spring AI 会把这些描述写进给模型的 JSON Schema 里,AI 照方抓药。
用 @JsonPropertyDescription 描述每个字段
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
public class MeetingNote {
@JsonPropertyDescription("会议主题,一句话概括")
private String topic;
@JsonPropertyDescription("参与人姓名列表")
private List<String> attendees;
@JsonPropertyDescription("待办事项,每条带责任人和截止日期")
private List<Todo> todos;
public static class Todo {
private String what;
private String owner;
}
}
从一段会议纪要抽成结构化对象
String minutes = "今天和张总、李工开会,讨论了官网改版。"
+ "张总说下周五前出设计稿,李工负责接口联调。";
MeetingNote note = chatClient.prompt()
.system("你是会议纪要整理助手,只输出结构化 JSON。")
.user(minutes)
.call()
.entity(MeetingNote.class);
// 输出:
// topic = "官网改版讨论"
// attendees= ["张总", "李工"]
// todos = [{what:"出设计稿", owner:"张总", due:"下周五"},
// {what:"接口联调", owner:"李工"}]
System.out.println(note.getTopic()); // 官网改版讨论
实体抽取前,先想"AI 该不该知道"
结构化输出把非结构化文本变结构化数据,适合合同、简历、会议纪要、工单。但它只能抽"文本里明写或能合理推断"的字段。文本没提截止日期,别指望它编出来——真要补全,那得配合 Function Calling 去查数据库。