SpringAI如何结构化输出?通过ReactAgent 实现两种方案!

在做「AI 面试官」这个小项目时,有一个刚开始看起来不难、但踩了不少坑的需求:让大模型稳定地产出结构化的 JSON 数据,而不是一堆"看起来很聪明但不好用"的自然语言。

比如,我希望模型在分析一份简历时,能老老实实给我返回这样一个结构:

复制代码
{
  "professionalKnowledge": "...",
  "projectExperience": "...",
  "internshipExperience": "...",
  "createTime": "2025-11-01T10:00:00Z",
  "updateTime": "2025-11-01T10:05:00Z"
}

而不是:

你好,我已经帮你分析完了简历,大致情况如下:

专业知识方面......(后面一大段中文)

后者对人类读者很友好,对代码完全不友好。

这篇文章就以我的项目「AI 面试官」为例,聊聊我在 Spring AI Alibaba 里如何玩转结构化输出,以及在 outputSchemaoutputType 之间是怎么做选择的。


输出模式

在 Spring AI Alibaba 里,大模型的输出大致有三种模式:

  1. outputSchema(String schema)

    你手写一段 JSON Schema 字符串,告诉模型"必须按这个结构来"。

  2. outputType(Class<?> type)

    直接丢一个 Java 类给它,Spring AI 内部通过 BeanOutputConverter 帮你推导 JSON Schema。

当你指定了 outputSchemaoutputType 之后,Spring AI Alibaba 会再帮你做一层"智能选择":

  • 对于支持原生结构化输出 的模型(目前是 OpenAiChatModelDashScopeChatModel),优先用模型的结构化输出能力。这种方式相当稳,因为服务端会帮你做格式校验
  • 对于其它模型,会退回到 Spring AI 的内置 ToolCall 策略,通过一个动态 ToolCall 把输出"揉"成结构化数据。

在我的项目里,用的是 DashScopeChatModel,所以可以吃上这波"原生结构化输出"的红利。


outputSchema

先看我在项目里用的一个实际例子:简历分析的结构。

手写 JSON Schema

我一开始就明确知道自己要的结构,所以很自然地选择了 outputSchema

复制代码
private String analysisSchema = """
        {
          "$schema": "https://json-schema.org/draft/2020-12/schema",
          "type": "object",
          "properties": {
            "professionalKnowledge": {
              "type": "string",
              "description": "专业知识"
            },
            "projectExperience": {
              "type": "string",
              "description": "项目经验"
            },
            "internshipExperience": {
              "type": "string",
              "description": "实习经验"
            },
            "createTime": {
              "type": "string",
              "format": "date-time",
              "description": "创建时间"
            },
            "updateTime": {
              "type": "string",
              "format": "date-time",
              "description": "更新时间"
            }
          },
          "additionalProperties": false
        }""";

这段 Schema 就是对目标 JSON 的精确定义

  • type: object:整体是一个对象
  • 每个字段是什么类型、干什么用、是不是时间
  • additionalProperties: false:不给你随便多加字段

在 Agent 里接上 outputSchema

然后在构建 ReactAgent 的时候,把这段 Schema 丢进去:

复制代码
public ResumeAgent(@Qualifier("dashScopeChatModel") ChatModel chatModel) {
    this.reactAgent = ReactAgent.builder()
            .name("ResumeAgent")
            .model(chatModel)
            .systemPrompt(
                "你是专业的简历分析专家,擅长提取和结构化简历信息。" +
                "你输出的JSON结构必须是纯净JSON内容,以'{'开头,以'}'结尾,不能包含任何多余的文本描述。"
            )
            .outputSchema(analysisSchema)
            .build();
}

outputSchema 带来的体验

这套方案上线后,最直观的感受是:输出变得很"乖"

只要 Schema 定义得清楚、字段描述足够直观,模型会非常努力地贴合你给的蓝图:

  • 字段名不会乱起
  • 类型基本准确
  • 不会动不动多出一堆"建议""总结"这种自然语言

尤其是在你的业务结构比较复杂或者不太标准的时候,手写 Schema 的方式会非常香。


outputType

如果你懒得手写 JSON Schema,或者你的类结构本身就很清晰,那可以直接用 outputType

Java

复制代码
public ResumeAgent(@Qualifier("dashScopeChatModel") ChatModel chatModel) {
    this.reactAgent = ReactAgent.builder()
            .name("ResumeAgent")
            .model(chatModel)
            .systemPrompt("你是专业的简历分析专家,擅长提取和结构化简历信息。")
            .outputType(ResumeVo.class)
            .build();
}

这里的玩法就是:

  • 你只负责定义好 ResumeVo 这个类
  • Spring AI 用 BeanOutputConverter 自动从这个类生成对应的 JSON Schema
  • 模型再根据这个"推导出的 Schema"来输出 JSON

项目刚开始,我用的是一个比较"教科书式"的 Resume 类:

  • 字段命名非常规范
  • 语义清晰
  • 结构也比较扁平

在这个版本下,配上 outputType(Resume.class),效果非常好。

模型能很自然地生成正确的 JSON,开发体验也很丝滑。后来我对系统做了一轮改造:

  • Resume 换成了 ResumeVo
  • 对字段做了一些删减和调整
  • 从业务视角看:更贴近实际需求、更精简
  • 从模型视角看:可能没那么好懂了

结果就是:同样的 outputType(ResumeVo.class),输出开始不稳定了:

  • 有时字段会缺失
  • 有时结构对不上
  • 有时直接返回不合法 JSON,解析直接抛异常

到这一步,我就很明确地意识到:
让模型"猜" Schema 在简单情况下没问题,一旦结构稍微不直观,最好还是人为给一份标准答案。

于是我干脆切换到了前面那套 outputSchema 的方案,手写一份 JSON Schema,事情立刻变得规矩了。


兜底

不管你用的是 outputSchema 还是 outputType,有一个现实要接受:

大模型有时候就是会给你一个「差一点」正确的 JSON。

比如:

  • 末尾多一个逗号
  • 转义字符少了个反斜杠
  • 某个字段偶尔给了 null,但 Schema 不允许
  • 开头/结尾顺手加了一句解释

所以在解析层,我的建议是:

  1. 尽量使用 Jackson 这类成熟 JSON 库来解析,容错能力更好
  2. 解析逻辑一定要用 try { ... } catch { ... } 包起来,别把"不完美的 JSON"直接传导到业务异常

一个示例写法(示意):

复制代码
try {
    ResumeVo resumeVo = objectMapper.readValue(jsonResponse, ResumeVo.class);
    // 正常业务逻辑
} catch (JsonProcessingException e) {
    // 记录日志 / 告警
    log.error("解析简历分析结果失败,原始响应:{}", jsonResponse, e);
    // 可以做一些降级,比如返回空对象/提示人工审核
}

在真正落地时,这一层"兜底"比你想象的重要,尤其是线上流量一大、模型偶尔抽风的时候。


outputSchema vs outputType

结合这次做「AI 面试官」的经验,我给出一个比较接地气的选择建议:

更适合 outputType 的情况

如果满足下面这些条件,我会优先选 outputType

  • 你的 Java 类:
    • 字段命名规范、语义清晰
    • 结构不复杂(扁平为主)
    • 后续不会频繁、大幅重构
  • 你希望:
    • 改类 = 自动改 Schema
    • 少写点 Schema 字符串

这种情况下,outputType 的开发体验最好,特别像在用"正常的 Spring 项目"。

更适合 outputSchema 的情况

如果你遇到的是这种情况,我会建议你直接上 outputSchema

  • 你的类结构对模型来说不太直观:
    • 字段语义有业务黑话
    • 结构层级较深
    • 或者是某种"视图类""VO 类",含义比较抽象
  • 你已经实际遇到过:
    • 模型老是生成不完整的 JSON
    • 字段类型经常不对
    • 部分字段老是被忽略

这种场景下,手写 Schema 就是给模型一份"合同书" ------

每个字段是什么、必须要不要、能不能多字段,全讲清楚,稳定性会明显提升。


我在项目里的最终选择

在我的 AI 面试官项目中,简历分析模块的最终方案是:

  • 使用 DashScopeChatModel,吃上原生结构化输出的优势
  • 对简历分析结果使用 outputSchema
    • 手写一份 analysisSchema
    • 搭配严格的 system prompt(只输出纯 JSON)
  • 解析层使用 JSON 工具 + 异常兜底,防止偶发格式问题把整个流程带崩

实际效果是:

对于这种"面向后端服务消费"的结构化数据输出,这套组合既稳定,又足够可控。

相关推荐
WZTTMoon2 分钟前
Spring Boot 启动全解析:4 大关键动作 + 底层逻辑
java·spring boot·后端
章鱼哥7303 分钟前
[特殊字符] SpringBoot 自定义系统健康检测:数据库、Redis、表统计、更新时长、系统性能全链路监控
java·数据库·redis
深圳佛手10 分钟前
Sharding-JDBC 和 Sharding-Proxy 区别
java
kk哥889919 分钟前
inout参数传递机制的底层原理是什么?
java·开发语言
小二·1 小时前
Spring框架入门:深入理解Spring DI的注入方式
java·后端·spring
避避风港1 小时前
转发与重定向
java·servlet
毕设源码-钟学长1 小时前
【开题答辩全过程】以 基于springboot和协同过滤算法的线上点餐系统为例,包含答辩的问题和答案
java·spring boot·后端
q***44152 小时前
Spring Security 新版本配置
java·后端·spring
o***74172 小时前
Springboot中SLF4J详解
java·spring boot·后端