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

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法