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 工具 + 异常兜底,防止偶发格式问题把整个流程带崩

实际效果是:

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

相关推荐
言慢行善21 小时前
sqlserver模糊查询问题
java·数据库·sqlserver
专吃海绵宝宝菠萝屋的派大星21 小时前
使用Dify对接自己开发的mcp
java·服务器·前端
大数据新鸟21 小时前
操作系统之虚拟内存
java·服务器·网络
Tong Z21 小时前
常见的限流算法和实现原理
java·开发语言
凭君语未可21 小时前
Java 中的实现类是什么
java·开发语言
He少年21 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
克里斯蒂亚诺更新1 天前
myeclipse的pojie
java·ide·myeclipse
迷藏4941 天前
**eBPF实战进阶:从零构建网络流量监控与过滤系统**在现代云原生架构中,**网络可观测性**和**安全隔离**已成为
java·网络·python·云原生·架构
迷藏4941 天前
**发散创新:基于Solid协议的Web3.0去中心化身份认证系统实战解析**在Web3.
java·python·web3·去中心化·区块链
qq_433502181 天前
Codex cli 飞书文档创建进阶实用命令 + Skill 创建&使用 小白完整教程
java·前端·飞书