一、为什么要有结构化输出?
在很多 AI 应用刚开始开发时,我们通常会这样调用大模型:
"请分析这段报告是否存在问题。"
模型返回一段自然语言:
"这份报告整体没有明显错误,但建议关注性别与检查部位是否一致......"
这种结果适合人阅读,但并不适合程序继续处理。
在真实业务系统中,我们往往需要让 AI 的输出继续参与后续流程,例如:
- 写入数据库;
- 展示到前端表格;
- 作为接口响应返回;
- 触发不同的业务规则;
- 进入质控、审核、告警、统计流程。
这时,单纯的自然语言输出就不够了。我们更希望模型返回类似这样的结构:
json
{
"passed": false,
"riskLevel": "HIGH",
"problems": [
{
"type": "GENDER_LOGIC",
"description": "男性患者报告中出现子宫相关描述",
"suggestion": "请核对患者性别与报告模板"
}
]
}
这就是结构化输出要解决的问题:
让大模型不只是"会说话",而是能够输出程序可解析、可校验、可落库、可流转的数据结构。
在 Spring AI 中,对应的能力就是 Structured Output Converter。
二、Structured Output Converter 是什么?
Spring AI 的 StructuredOutputConverter 用来把大模型生成的文本结果转换成指定的结构化对象。
它的核心目标是:
将 AI 模型返回的字符串,转换成 Java 应用可以直接使用的数据类型。
例如:
- 转成 Java Bean;
- 转成
Map<String, Object>; - 转成
List<String>; - 转成复杂泛型对象;
- 转成符合 JSON Schema 的结构。
从接口设计上看,StructuredOutputConverter<T> 同时继承了两个能力:
java
public interface StructuredOutputConverter<T>
extends Converter<String, T>, FormatProvider {
}
这里有两个关键点。
1. FormatProvider:告诉模型应该怎么输出
FormatProvider 负责生成格式说明。
例如,它会告诉模型:
text
你的响应应该是 JSON 格式。
JSON 结构应该匹配某个 Java 类。
不要输出解释说明,只输出符合规范的 JSON。
也就是说,在模型调用之前,Spring AI 会通过格式说明引导模型按照指定结构输出。
2. Converter:把模型文本转换成 Java 对象
模型返回后,Converter<String, T> 负责把字符串解析成目标类型。
例如:
java
ReportQcResult result = converter.convert(modelOutput);
所以,结构化输出的完整过程可以理解为:
text
业务输入
↓
Prompt + 格式要求
↓
调用大模型
↓
模型返回结构化文本
↓
Converter 转换成 Java 对象
↓
业务系统继续处理
三、Spring AI 提供了哪些结构化输出转换器?
Spring AI 官方提供了几个常用转换器。
1. BeanOutputConverter
BeanOutputConverter 是生产项目中最常用的转换器。
它适合将模型输出直接转换成 Java 类、record 或复杂对象。
例如定义一个报告质控结果对象:
java
public record ReportQcResult(
Boolean passed,
String riskLevel,
List<QcProblem> problems
) {
}
public record QcProblem(
String type,
String description,
String suggestion
) {
}
然后可以让模型直接返回 ReportQcResult:
java
ReportQcResult result = ChatClient.create(chatModel)
.prompt()
.user("""
请对下面的医学影像报告进行质控分析:
{reportText}
""")
.call()
.entity(ReportQcResult.class);
这种写法非常适合业务系统集成。
相比手工拼接 Prompt,然后自己解析 JSON,BeanOutputConverter 的优势是:
- 目标结构清晰;
- Java 类型明确;
- 代码可维护;
- 便于后续校验;
- 适合接口返回和数据库持久化。
在实际项目中,推荐优先使用这种方式。
2. MapOutputConverter
MapOutputConverter 适合结构不固定的场景。
例如,用户希望模型返回一组动态字段:
java
Map<String, Object> result = ChatClient.create(chatModel)
.prompt()
.user("""
请从下面文本中提取关键信息,
返回 JSON 对象:
{text}
""")
.call()
.entity(new ParameterizedTypeReference<Map<String, Object>>() {});
适合以下场景:
- 字段不确定;
- 临时数据提取;
- 原型验证;
- 后续还要二次清洗;
- 不想提前定义 Java Bean。
但是在生产系统中,如果字段结构已经稳定,建议还是定义明确的 Java 类,而不是长期使用 Map。
因为 Map 虽然灵活,但缺点也很明显:
- 缺少类型约束;
- 字段名容易写错;
- 后续代码可读性差;
- 不利于长期维护。
3. ListOutputConverter
ListOutputConverter 用于将模型结果转换成列表。
例如让模型返回若干关键词:
java
List<String> keywords = ChatClient.create(chatModel)
.prompt()
.user("请从下面报告中提取 5 个关键词:{reportText}")
.call()
.entity(new ListOutputConverter(new DefaultConversionService()));
它适合简单列表类输出,例如:
- 关键词列表;
- 标签列表;
- 推荐项列表;
- 风险点列表;
- 问题清单。
不过,如果列表中的每一项是复杂对象,例如:
json
[
{
"type": "LOGIC_ERROR",
"description": "描述内容"
}
]
这种情况更建议使用 BeanOutputConverter + 泛型类型。
四、复杂泛型对象如何处理?
实际业务中,经常会遇到列表对象,例如:
java
List<ReportQcResult>
由于 Java 泛型存在类型擦除,不能简单写成:
java
.entity(List.class)
这种写法会丢失具体泛型信息。
Spring AI 支持使用 ParameterizedTypeReference 来处理复杂泛型:
java
List<ReportQcResult> results = ChatClient.create(chatModel)
.prompt()
.user("""
请批量分析以下医学影像报告,
返回每一份报告的质控结果。
{reports}
""")
.call()
.entity(new ParameterizedTypeReference<List<ReportQcResult>>() {});
这类写法适合:
- 批量报告质控;
- 批量商品信息抽取;
- 批量合同条款分析;
- 批量简历解析;
- 批量病例摘要生成。
在生产系统中,只要返回结构包含泛型,建议优先使用 ParameterizedTypeReference。
五、结构化输出的底层原理
很多人容易误解结构化输出,以为大模型天然就能稳定返回 Java 对象。
其实不是。
对于普通文本补全模型来说,它本质上仍然是在生成文本。
Spring AI 的 Structured Output Converter 主要做了两件事:
1. 调用模型前:追加格式说明
例如用户原始输入是:
text
请分析这份报告是否存在问题。
Spring AI 会在 Prompt 中追加格式要求:
text
请按照指定 JSON Schema 返回。
不要输出解释说明。
只返回符合 RFC8259 的 JSON。
这样模型就知道自己应该输出什么格式。
2. 调用模型后:解析模型输出
模型返回文本后,Spring AI 会尝试把它转换成目标 Java 类型。
例如:
json
{
"passed": true,
"riskLevel": "LOW",
"problems": []
}
最终被转换成:
java
ReportQcResult result
所以,它不是魔法,本质是:
text
Prompt 约束 + JSON Schema 引导 + 文本解析 + Java 对象映射
理解这一点非常重要。
因为只要模型仍然可能输出不规范内容,业务系统就必须做好异常处理和结果校验。
六、生产级使用时必须注意的问题
1. 不要完全相信模型输出
官方文档也明确提醒:结构化输出是 best effort。
也就是说,模型不一定每次都严格按照要求返回。
它可能出现:
- 多输出了解释性文字;
- JSON 格式不完整;
- 字段缺失;
- 字段类型错误;
- 枚举值不符合预期;
- 返回空结果;
- 返回与业务事实不一致的内容。
所以生产环境中不能只依赖 Converter。
建议增加一层业务校验。
例如:
java
if (result == null) {
throw new IllegalStateException("AI 返回结果为空");
}
if (result.riskLevel() == null) {
throw new IllegalStateException("AI 返回结果缺少风险等级");
}
if (!List.of("LOW", "MEDIUM", "HIGH").contains(result.riskLevel())) {
throw new IllegalStateException("非法风险等级:" + result.riskLevel());
}
结构化输出解决的是"格式问题",不是"正确性问题"。
2. 输出对象不要设计得过于复杂
有些开发者会定义非常复杂的嵌套结构,让模型一次性返回大量字段。
例如:
text
患者信息、检查信息、报告内容、风险项、建议、审核意见、质控评分、规则命中详情、证据链......
这种结构虽然看起来完整,但模型更容易出错。
生产环境建议遵循一个原则:
输出结构越关键,越要简单、明确、可校验。
可以把复杂任务拆成多个小任务:
text
第一步:抽取基础信息
第二步:识别风险问题
第三步:生成审核建议
第四步:汇总成最终结果
这样每一步的结构都更稳定。
3. 字段命名要业务化,不要太抽象
不推荐这样设计:
java
record AiResult(
String result,
String message,
Object data
) {
}
这种结构虽然通用,但不利于模型理解。
更推荐这样:
java
record ReportQcResult(
Boolean passed,
String riskLevel,
List<QcProblem> problems
) {
}
字段名越贴近业务语义,模型越容易按照预期输出。
4. 枚举值要明确限制
如果业务中有固定分类,最好在 Prompt 中明确说明。
例如:
text
riskLevel 只能取以下值:
LOW:低风险
MEDIUM:中风险
HIGH:高风险
对应 Java 对象可以这样设计:
java
public record ReportQcResult(
Boolean passed,
String riskLevel,
List<QcProblem> problems
) {
}
也可以进一步使用枚举:
java
public enum RiskLevel {
LOW,
MEDIUM,
HIGH
}
不过使用枚举时要注意,模型输出必须和枚举名称完全一致,否则反序列化可能失败。
5. 关键流程要增加重试和兜底
生产环境中建议增加异常兜底:
java
try {
ReportQcResult result = chatClient.prompt()
.user(prompt)
.call()
.entity(ReportQcResult.class);
validate(result);
return result;
} catch (Exception e) {
log.warn("AI 结构化输出解析失败", e);
return ReportQcResult.builder()
.passed(false)
.riskLevel("UNKNOWN")
.problems(List.of(
new QcProblem(
"AI_OUTPUT_PARSE_ERROR",
"AI 返回结果无法解析",
"建议进入人工审核"
)
))
.build();
}
对于医疗、金融、合同、质控等严肃场景,不建议因为 AI 输出失败就直接中断主流程。
更合理的做法是:
- 记录原始输入;
- 记录模型原始输出;
- 标记解析失败;
- 转人工审核;
- 后续用于优化 Prompt 或模型配置。
七、Native Structured Output:更可靠的结构化输出方式
传统 Structured Output Converter 主要依赖 Prompt 引导模型输出 JSON。
但现在越来越多模型开始支持原生结构化输出能力。
Spring AI 也支持 Native Structured Output。
使用方式类似这样:
java
ReportQcResult result = ChatClient.create(chatModel)
.prompt()
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.user("""
请对下面医学影像报告进行质控:
{reportText}
""")
.call()
.entity(ReportQcResult.class);
也可以在 ChatClient.Builder 中全局启用:
java
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultAdvisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.build();
}
Native Structured Output 的核心区别是:
text
传统方式:
Prompt 中追加格式说明,让模型尽量按 JSON 输出。
原生方式:
直接把 JSON Schema 传给模型的结构化输出 API。
因此,它通常更可靠,也能减少 Prompt 中大量格式说明。
不过需要注意,不是所有模型都支持原生结构化输出。
不同模型厂商的能力也不完全一致。
所以生产环境中可以采用这样的策略:
text
优先使用 Native Structured Output
↓
如果模型不支持,则退回普通 Structured Output Converter
↓
再配合业务校验、重试、兜底和人工审核
八、结构化输出和 Tool Calling 的区别
很多人会把结构化输出和 Tool Calling 混在一起。
它们确实都和"结构化数据"有关,但适用场景不同。
结构化输出适合什么?
适合让模型返回一个结构化结果。
例如:
text
请分析这份报告,返回质控结果。
请提取合同中的甲方、乙方、金额和日期。
请把用户输入整理成任务清单。
重点是:
text
模型生成结果 → 程序解析结果
Tool Calling 适合什么?
适合让模型决定是否调用某个工具或函数。
例如:
text
用户问天气 → 调用天气接口
用户问订单状态 → 调用订单查询接口
用户要求创建任务 → 调用任务系统 API
重点是:
text
模型理解意图 → 调用外部工具 → 返回结果
所以二者的区别可以简单理解为:
| 能力 | 主要目的 | 典型场景 |
|---|---|---|
| 结构化输出 | 让模型输出可解析对象 | 信息抽取、分类、审核、总结 |
| Tool Calling | 让模型调用外部工具 | 查数据库、调接口、执行操作 |
在实际系统中,两者经常组合使用。
例如医学影像报告质控系统:
text
Tool Calling:查询患者历史检查、获取影像信息、读取规则库
结构化输出:返回最终质控结果、风险等级、问题清单
九、一个更接近生产级的示例
下面以"医学影像报告质控"为例。
1. 定义输出对象
java
public record ReportQcResult(
Boolean passed,
String riskLevel,
List<QcProblem> problems,
String summary
) {
}
public record QcProblem(
String type,
String field,
String description,
String suggestion
) {
}
2. 编写 Prompt
java
String prompt = """
你是一名医学影像报告质控助手。
请对下面的影像报告进行质控分析,重点检查:
1. 性别与解剖部位是否冲突;
2. 报告描述与诊断结论是否矛盾;
3. 是否存在明显错别字或术语错误;
4. 是否存在风险较高的漏诊提示。
风险等级只能取:
LOW、MEDIUM、HIGH。
报告内容如下:
{reportText}
""";
3. 调用模型并返回 Java 对象
java
ReportQcResult result = chatClient.prompt()
.user(u -> u.text(prompt)
.param("reportText", reportText))
.call()
.entity(ReportQcResult.class);
4. 增加结果校验
java
private void validate(ReportQcResult result) {
if (result == null) {
throw new IllegalArgumentException("AI 质控结果为空");
}
if (result.passed() == null) {
throw new IllegalArgumentException("passed 字段不能为空");
}
if (result.riskLevel() == null) {
throw new IllegalArgumentException("riskLevel 字段不能为空");
}
if (!List.of("LOW", "MEDIUM", "HIGH").contains(result.riskLevel())) {
throw new IllegalArgumentException("非法风险等级:" + result.riskLevel());
}
if (result.problems() == null) {
throw new IllegalArgumentException("problems 字段不能为空");
}
}
5. 记录原始结果,便于审计
生产环境中建议保存以下内容:
text
请求参数
Prompt 内容
模型名称
模型原始输出
结构化解析结果
解析是否成功
业务校验是否通过
异常信息
人工审核结果
这对于 AI 系统非常重要。
因为 AI 应用不是传统确定性程序,后续一定会遇到:
- 模型输出异常;
- Prompt 需要优化;
- 质控结果被医生质疑;
- 需要追溯当时为什么给出这个结论;
- 需要对模型效果进行评估。
结构化输出只是第一步,审计和反馈闭环才是生产级落地的关键。
十、最佳实践总结
Spring AI 的结构化输出非常适合生产系统中的 AI 能力集成。
但是在落地时,需要注意以下原则。
1. 优先使用明确的 Java Bean
稳定业务结构建议使用:
java
.entity(YourResult.class)
不要长期依赖 Map。
2. 复杂泛型使用 ParameterizedTypeReference
例如:
java
.entity(new ParameterizedTypeReference<List<YourResult>>() {})
3. Prompt 中明确字段含义和取值范围
尤其是:
- 枚举值;
- 风险等级;
- 分类类型;
- 是否通过;
- 输出语言;
- 不允许输出额外解释。
4. 必须增加业务校验
不要认为模型返回 JSON 就一定正确。
要校验:
- 字段是否为空;
- 枚举是否合法;
- 数值范围是否正确;
- 列表是否超限;
- 业务逻辑是否一致。
5. 关键业务要有兜底方案
例如:
- 解析失败转人工;
- 重试一次;
- 降级为普通文本结果;
- 标记为待审核;
- 记录原始输出。
6. 能用 Native Structured Output 时优先使用
如果当前模型支持原生结构化输出,建议优先启用。
它比单纯依赖 Prompt 格式约束更可靠。
十一、结语
结构化输出是 Spring AI 从 Demo 走向生产级应用的关键能力之一。
因为真实业务系统关心的不只是"模型回答得好不好",还关心:
text
能不能被程序解析?
能不能进入业务流程?
能不能被校验?
能不能被审计?
能不能稳定运行?
Structured Output Converter 解决的是 AI 应用工程化中的一个核心问题:
把大模型从"聊天工具"变成"业务系统中的一个可集成组件"。
在生产实践中,我们不能只关注模型能力本身,更要关注模型输出如何进入系统、如何被校验、如何被追踪、如何被修正。
这才是 Spring AI 结构化输出真正的价值。