Spring AI 生产级实战-结构化输出

一、为什么要有结构化输出?

在很多 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 结构化输出真正的价值。

相关推荐
imDwAaY1 小时前
从非线性分类到多层神经网络 CS188 Note21 学习笔记
人工智能·笔记·python·神经网络·学习·机器学习·分类
稳如磐石.1 小时前
北京工控机生产工厂
大数据·人工智能·python
之歆1 小时前
在 IntelliJ IDEA 里复刻 Cursor 式内联审查:架构复盘-从放弃到拾起:如何用 LineStatusTracker 拯救一个烂掉的项目
java·架构·intellij-idea
梓䈑1 小时前
C++ AI模型统一接入引擎(第一篇):项目介绍与环境搭建
c++·人工智能·chatgpt
疏狂难除1 小时前
JetBrains IDE插件开发教程(四)——Action
java·ide·kotlin
viskaz1 小时前
Agent的记忆系统
人工智能·prompt
图灵农场1 小时前
Spring AI Alibaba-ReactAgent框架-chatbot智能体应用
人工智能
白狐_7981 小时前
从空白模板到文旅风 PPT:用 Claude Code + Kimi API 优化 AI 生成演示文稿
大数据·人工智能
laufing1 小时前
java web 基础 ---- servlet
java·servlet·web开发