StackTraces
Spring AI已验证

Spring AI BeanOutputConverter 解析失败

@debug_master32 天前更新阅读 6 min00

用 .entity() 收结构化输出,模型偶尔把 JSON 包进 Markdown 代码围栏再返回,BeanOutputConverter 直接抛异常,还把原始响应藏起来不让你看。1.x 得自己清理重建;2.0 用 validateSchema() 让框架自动重试校正。

Issue & Env(案发现场与环境)

  • 业务场景:用 ChatClient 的 .entity() 把模型返回映射成强类型对象,偶尔解析失败。
  • 环境:Spring AI 1.1.0(issue #5060 报告版本),该问题在 1.x 持续存在;2.0 引入自校正机制。

StackTrace(堆栈日志)

issue #5060 报告的异常形态:模型返回的 JSON 反序列化失败时,JsonParseException 被包进一个不带任何额外字段的 RuntimeException。报错原文通常是 Jackson 的解析错误:

com.fasterxml.jackson.core.JsonParseException: Unexpected character ('H' (code 72)): expected a valid value (number, String, array, object, 'true', 'false' or 'null')

完整堆栈:

java.lang.RuntimeException
    at org.springframework.ai.converter.BeanOutputConverter.convert(BeanOutputConverter.java:120)
    at org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$InternalChatClientRequest.entity(DefaultChatClient.java:720)
    at com.example.demo.movie.FilmographyController.filmography(FilmographyController.java:27)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character ('H' (code 72)): expected a valid value (number, String, array, object, 'true', 'false' or 'null')
    at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:2391)
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:745)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3751)

触发它的典型返回是这种——模型把 JSON 用 Markdown 代码围栏包了起来:

Here's the filmography:
```json
{ "actor": "Tom Hanks", "movies": ["Forrest Gump", "Cast Away"] }
```

BeanOutputConverter 拿到的是整段文字,JSON 前面多了说明和围栏,解析直接失败。

Reproduce(复现代码)

定义一个目标类型,再用 .entity() 收:

record ActorsFilms(String actor, List<String> movies) {}
ActorsFilms films = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorsFilms.class);

多数时候能成,但只要某一次模型把 JSON 包进围栏、或塞进几句说明,.entity() 就崩。

Root Cause(根因分析)

BeanOutputConverter 是严格转换器,对模型输出只有一个预期:纯 JSON。它不会帮你剥掉 Markdown 围栏、说明文字、尾逗号,也不做任何容错。模型一旦在 JSON 外面多写点什么,Jackson 就抛 JsonParseException。

更麻烦的是 issue #5060 指出的点:这个异常被包成不带任何字段的 RuntimeException。你想在 catch 里看看模型到底回了什么,既拿不到原始文本,也读不到完整错误——设计上就没给你留这个后门。

本质原因是 LLM 是文本模型,没人保证它每次都输出裸 JSON。官方文档也明说 StructuredOutputConverter 只是「尽力而为」,不是保证。

The Fix(解决方案)

1.x 的绕法:自己清理再转

先拿原始文本,剥掉围栏和前后噪声,再手动转换:

String raw = chatClient.prompt()
    .user("generate filmography")
    .call()
    .content();

String json = raw.replaceAll("```json", "")
                 .replaceAll("```", "")
                 .trim();
ActorsFilms obj = new BeanOutputConverter<>(ActorsFilms.class).convert(json);

或者干脆收 String,用 Jackson 自己反序列化,把清洗逻辑集中到一处。

2.0 的正解:validateSchema()

Spring AI 2.0 给 .entity() 加了自校正,校验失败就带着具体错误重试:

ActorsFilms films = chatClient.prompt()
    .user("generate the filmography for a random actor")
    .call()
    .entity(ActorsFilms.class, spec -> spec.validateSchema());

开关一拉,StructuredOutputValidationAdvisor 自动注册。模型返回后先按 schema 校验,失败就把「缺了哪个字段、类型哪里不对」拼回 prompt 重发,默认最多重试 3 次。想调次数就自己建 advisor:

var advisor = StructuredOutputValidationAdvisor.builder()
    .outputType(ActorsFilms.class)
    .maxRepeatAttempts(5)
    .build();

ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(advisor)
    .build();

References(参考来源)

最后更新于 2026-09-09

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法