Spring AI调用entity()时JSON解析失败?Markdown包裹、字段缺失与Schema不兼容完整排查
文章摘要
Spring AI的ChatClient.call().entity()可以把模型输出直接转换为Java对象,但生产项目经常遇到JSON前后带解释文字、被Markdown代码块包裹、必填字段缺失、枚举值不匹配、日期格式错误或Provider不支持完整JSON Schema等问题。默认结构化输出本质上仍可能是“Prompt约束+文本解析”,并不天然保证100%成功。本文从目标类型设计、原始响应留存、Provider原生结构化输出、Schema校验与自修复、流式限制和错误分级六个方面给出完整排查方案。
一、典型错误表现
代码:
OrderRisk result = chatClient.prompt()
.user(prompt)
.call()
.entity(OrderRisk.class);
可能出现:
JsonParseException
MismatchedInputException
UnrecognizedPropertyException
InvalidFormatException
Cannot deserialize value of type
模型实际返回:
以下是分析结果:
```json
{
"riskLevel": "HIGH",
"reason": "客户连续逾期"
}
人能看懂,但Jackson看到的不是纯JSON。
## 二、先保留原始响应
很多项目只调用:
```java
.entity(OrderRisk.class)
一旦失败,只看到反序列化异常,看不到模型到底返回了什么。
排查时先改为:
String raw = chatClient.prompt()
.user(prompt)
.call()
.content();
log.debug("structured.raw={}", sanitize(raw));
然后再手工转换:
BeanOutputConverter converter =
new BeanOutputConverter(OrderRisk.class);
OrderRisk result = converter.convert(raw);
生产环境不要记录敏感原文,可以记录:
- 响应哈希;
- 长度;
- 前后少量脱敏片段;
- 错误类型;
- 模型和Prompt版本;
- Schema版本。
三、目标Java类型是否适合生成
错误类型设计:
public class OrderRisk {
private final RiskLevel riskLevel;
private final BigDecimal score;
private final LocalDateTime createdAt;
public OrderRisk(...) {
...
}
}
可能存在:
- 无默认构造;
- 字段没有Getter;
- Jackson配置不一致;
- 时间格式不明确;
- 枚举值约束不清晰;
- 非静态内部类;
- 循环引用。
推荐优先使用Record:
public record OrderRisk(
RiskLevel riskLevel,
double score,
String reason,
List evidenceIds
) {
}
结构越简单,模型和反序列化器越稳定。
四、枚举值为什么最容易失败
定义:
public enum RiskLevel {
LOW,
MEDIUM,
HIGH
}
模型可能返回:
{
"riskLevel": "高风险"
}
或:
{
"riskLevel": "high"
}
这都会造成枚举转换失败。
Prompt中应明确:
riskLevel只能取LOW、MEDIUM、HIGH之一。
不得输出中文值、缩写或其他值。
业务层仍应对未知值进行错误处理,而不是偷偷映射为默认值。
五、日期和数字格式需要收敛
日期
不推荐让模型自由输出:
2026年8月2日上午
08/02/26
2 Aug 2026
推荐:
ISO-8601
2026-08-02T08:30:00+08:00
如果只是业务日期,可以使用:
LocalDate
并要求:
YYYY-MM-DD
数字
金额不要让模型返回:
1,200元
约1.2K
人民币1200
结构化字段使用:
{
"amount": 1200.00,
"currency": "CNY"
}
六、额外字段导致失败怎么办
模型返回:
{
"riskLevel": "HIGH",
"reason": "连续逾期",
"analysis": "详细推理过程"
}
而Java类型没有analysis。
两种策略:
严格模式
禁止额外字段,任何漂移都视为错误。
适合:
- 工作流路由;
- 工具参数;
- 订单操作;
- 财务数据;
- 审批结果。
宽松模式
忽略未知字段。
适合:
- 非关键展示;
- 兼容旧模型;
- 渐进迁移。
企业系统建议对关键结构使用严格模式,避免模型偷偷扩展协议。
七、默认entity()并不是强制保证
结构化输出存在两个层次。
Prompt约束
把JSON Schema或格式指令加入Prompt
→ 模型尽量遵守
→ 客户端解析
优点:
- Provider兼容广;
- 大多数模型可用。
缺点:
- 可能输出解释文字;
- 可能漏字段;
- 可能多字段;
- 可能输出无效JSON。
Provider原生结构化输出
JSON Schema作为API级约束发送
→ Provider约束模型输出
可靠性通常更高,但存在:
- Provider支持差异;
- 模型版本差异;
- JSON Schema子集限制;
- 顶层数组限制;
- 复杂递归结构不支持。
八、启用Provider原生结构化输出
Spring AI 2.0可以在目标模型支持时启用原生结构化输出:
OrderRisk result = chatClient.prompt()
.user(prompt)
.call()
.entity(
OrderRisk.class,
spec -> spec.useProviderStructuredOutput()
);
也可以在ChatClient级启用相应参数。
但要注意:
打开开关
≠ 所有Provider都严格支持全部Schema
上线前必须对具体:
- Provider;
- 模型;
- API版本;
- Schema;
- 顶层结构;
进行实测。
九、使用validateSchema()自修复
当输出不符合Schema时,可以让Spring AI把具体校验错误反馈给模型并重新生成:
OrderRisk result = chatClient.prompt()
.user(prompt)
.call()
.entity(
OrderRisk.class,
spec -> spec.validateSchema()
);
也可以组合:
.entity(
OrderRisk.class,
spec -> spec
.useProviderStructuredOutput()
.validateSchema()
)
逻辑:
Provider约束输出
→ 本地Schema再次校验
→ 失败后携带错误重试
这种方式比盲目重试更有效,因为模型会看到:
缺少哪个字段
哪个字段类型错误
哪个值不符合枚举
十、重试不是免费的
结构化输出自修复会增加:
- Token;
- 延迟;
- 模型费用;
- 并发占用;
- Provider限流压力。
需要记录:
structured_output_attempts
schema_validation_failure_count
structured_output_total_tokens
structured_output_latency_ms
repair_success_rate
如果大量请求都需要第二次或第三次修复,说明问题不应只靠重试解决。
应检查:
- Schema是否过于复杂;
- 模型是否适合;
- Prompt是否冲突;
- 字段描述是否不清;
- 是否选择了不稳定的推理模式;
- Provider原生支持是否完整。
十一、不要用一个超大对象承载全部结果
错误Schema:
客户分析
+风险判断
+行动计划
+邮件正文
+审批意见
+工具参数
一次返回几十个嵌套字段,失败率会快速上升。
推荐拆成:
第一步:分类
第二步:提取字段
第三步:生成建议
第四步:生成文本
关键路由对象保持小而稳定:
public record IntentDecision(
IntentType intent,
double confidence,
boolean requiresHumanReview
) {
}
十二、顶层数组为什么容易踩坑
部分Provider的原生结构化输出对顶层数组支持有限。
不推荐:
List
推荐包装:
public record OrderRiskList(
List items
) {
}
JSON:
{
"items": [
{
"riskLevel": "HIGH",
"score": 0.91
}
]
}
这种结构更容易跨Provider迁移。
十三、流式输出不能直接entity()
.entity()需要完整响应才能解析,因此通常用于:
.call()
流式:
.stream()
返回的是文本片段,不能在每个Chunk上完成完整JSON反序列化。
如果必须流式展示结构化任务,建议:
流式发送进度事件
→ 模型完整生成
→ 服务端聚合
→ Schema校验
→ 最终发送result事件
不要尝试把半截JSON当成完整对象解析。
十四、BeanOutputConverter升级后Schema变化
Spring AI 2.0中,BeanOutputConverter的JSON Schema生成逻辑与工具调用Schema进一步对齐。
升级时需要检查:
- Kotlin可选属性是否仍在required中;
@JsonProperty(required = false)行为;LocalDateTime等format提示;- 自定义Schema后处理扩展点;
- 新旧Schema是否兼容历史Prompt和测试集。
不要只验证代码能编译,还要保存并比较生成Schema差异。
十五、推荐错误分级
public enum StructuredOutputError {
EMPTY_RESPONSE,
INVALID_JSON,
SCHEMA_MISMATCH,
ENUM_VALUE_INVALID,
DATE_FORMAT_INVALID,
PROVIDER_NOT_SUPPORTED,
RETRY_EXHAUSTED
}
不同错误使用不同策略:
空响应
→ 可短次数重试
Schema不匹配
→ 带错误自修复
Provider不支持
→ 回退Prompt模式
重试耗尽
→ 转人工或返回稳定错误
十六、完整排查清单
□ 是否保留了脱敏后的原始响应
□ Java目标类型是否可反序列化
□ 枚举取值是否明确
□ 日期和金额格式是否固定
□ 是否存在额外字段
□ Schema是否过度复杂
□ Provider是否支持原生结构化输出
□ 是否启用了useProviderStructuredOutput
□ 是否启用了validateSchema
□ 是否记录了修复次数和累计Token
□ 是否误在stream()中直接解析entity
□ 升级后生成Schema是否发生变化
总结
entity()解析失败并不只是“模型偶尔不听话”,而可能来自:
目标类型设计
+Prompt约束不足
+Provider能力差异
+Schema不兼容
+流式调用方式错误
生产级方案应采用:
简单稳定的领域类型
+Provider原生约束
+本地Schema校验
+有上限的错误自修复
+失败降级和完整观测
延伸阅读
如果你正在关注企业级AI应用、Spring AI、RAG、Agent与MCP工程化落地,欢迎访问 智元界:
https://www.zyentor.com/
智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。