在做「AI 面试官」这个小项目时,有一个刚开始看起来不难、但踩了不少坑的需求:让大模型稳定地产出结构化的 JSON 数据,而不是一堆"看起来很聪明但不好用"的自然语言。
比如,我希望模型在分析一份简历时,能老老实实给我返回这样一个结构:
{
"professionalKnowledge": "...",
"projectExperience": "...",
"internshipExperience": "...",
"createTime": "2025-11-01T10:00:00Z",
"updateTime": "2025-11-01T10:05:00Z"
}
而不是:
你好,我已经帮你分析完了简历,大致情况如下:
专业知识方面......(后面一大段中文)
后者对人类读者很友好,对代码完全不友好。
这篇文章就以我的项目「AI 面试官」为例,聊聊我在 Spring AI Alibaba 里如何玩转结构化输出,以及在 outputSchema 和 outputType 之间是怎么做选择的。
输出模式
在 Spring AI Alibaba 里,大模型的输出大致有三种模式:
-
outputSchema(String schema)你手写一段 JSON Schema 字符串,告诉模型"必须按这个结构来"。
-
outputType(Class<?> type)直接丢一个 Java 类给它,Spring AI 内部通过
BeanOutputConverter帮你推导 JSON Schema。
当你指定了 outputSchema 或 outputType 之后,Spring AI Alibaba 会再帮你做一层"智能选择":
- 对于支持原生结构化输出 的模型(目前是
OpenAiChatModel、DashScopeChatModel),优先用模型的结构化输出能力。这种方式相当稳,因为服务端会帮你做格式校验。 - 对于其它模型,会退回到 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 不允许 - 开头/结尾顺手加了一句解释
所以在解析层,我的建议是:
- 尽量使用 Jackson 这类成熟 JSON 库来解析,容错能力更好
- 解析逻辑一定要用
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 工具 + 异常兜底,防止偶发格式问题把整个流程带崩
实际效果是:
对于这种"面向后端服务消费"的结构化数据输出,这套组合既稳定,又足够可控。