Spring AI entity() 方法:告别手撕 JSON,实现结构化输出

发布时间:2026/10/14 23:48:33
Spring AI entity() 方法:告别手撕 JSON,实现结构化输出 1. 为什么我劝你别再手撕 JSON 了做 AI 应用开发的朋友十个里有八个踩过同一个坑模型返回的是一坨字符串你得自己写正则、切字符串、JSON.parse然后还要处理各种边界情况——模型今天心情好多返回一个字段明天心情不好少返回一个字段后天干脆在 JSON 外面裹一层“好的以下是您要的结果”这样的废话。代码越写越丑维护成本越来越高最后整个项目里最脆弱的环节就是那段解析逻辑。Spring AI 提供的entity()方法就是专门来解决这个问题的。它做的事情说起来很简单你给它一个 JavaBean 的 Class 对象它帮你把模型的回答直接映射成这个对象。你不用再写ObjectMapper.readValue()不用再处理JsonProcessingException更不用在 prompt 里反复强调“请只返回 JSON不要加任何解释”——虽然该强调的还是得强调但至少解析这一层框架帮你兜住了。这篇文章适合谁看如果你正在用 Spring Boot 做 AI 应用或者你手头有一个需要从模型输出里提取结构化数据的场景——比如信息抽取、意图识别、表单填充、分类打标——那这篇内容能帮你省下至少两天的调试时间。我会从设计思路讲到实操细节再把我踩过的坑一个个摊开来说。2. 结构化输出的整体设计思路2.1 模型输出为什么不能直接当 JSON 用大语言模型的本质是“下一个 token 预测器”它输出的是一段自然语言文本。你让它返回 JSON它确实会返回一个看起来像 JSON 的东西但这个“看起来像”和“真的是”之间隔着好几道坎。第一道坎是格式漂移。你要求返回{name: 张三, age: 25}模型可能返回{name: 张三, age: 25}——注意 age 从数字变成了字符串。这在 Java 里映射到int字段就会直接抛异常。第二道坎是多余包裹。模型经常会在 JSON 前后加一些“礼貌用语”比如“好的根据您的要求结果如下”或者“希望这个回答对您有帮助”这些东西如果不清理掉ObjectMapper直接解析会报错。第三道坎是字段缺失或多余。你定义了五个字段模型可能只返回三个或者多返回两个你没定义的。如果没有合理的容错机制程序就会在运行时崩溃。Spring AI 的entity()方法在设计上就是冲着这三道坎去的。它的核心思路是在框架层面统一处理“模型输出到 Java 对象”的转换把格式清洗、类型转换、容错处理这些脏活累活都封装起来让开发者只需要关心“我要什么对象”这一个问题。2.2 entity() 和 map() 的本质区别Spring AI 的ChatClient提供了两个看起来很像的方法entity()和map()。很多人第一次用的时候会懵——这俩到底啥区别简单来说map()是“我自己来”的版本。你给它一个ParameterizedTypeReference它帮你把 JSON 反序列化成对应的类型但前提是模型返回的必须是一个干净的、可以直接解析的 JSON 字符串。如果模型返回了多余的文字map()不会帮你清理直接抛异常。entity()则是“框架帮你兜底”的版本。它内部会做几件事先从模型输出里提取出 JSON 部分去掉前后的废话然后尝试反序列化如果失败还会根据配置进行重试或降级处理。更重要的是entity()支持直接传入一个 JavaBean 的 Class框架会自动根据这个 Class 生成 JSON Schema并把它塞进 prompt 里告诉模型“你应该按这个格式返回”。这个区别在实际项目里非常关键。我做过一个信息抽取的需求从用户的一段自然语言描述里提取出“姓名、电话、地址、意向产品”四个字段。用map()的时候模型十次里有三次会返回带前缀的文本我得自己写正则去清洗。换成entity()之后框架自动在 prompt 里加了格式约束模型返回的干净程度明显提升即使偶尔有杂质框架也能自动处理掉。2.3 为什么选择 JavaBean 而不是 Map有人可能会问我直接用MapString, Object接不就行了干嘛还要定义一个 JavaBean这个问题我一开始也纠结过。用 Map 确实灵活模型返回什么字段我就接什么字段不用提前定义类。但实际用下来JavaBean 的优势在三个地方体现得特别明显。类型安全。Map 里的值都是 Object你要用的时候得强转转错了就是ClassCastException。JavaBean 的字段类型是编译期确定的IDE 能帮你检查运行时也少了很多意外。可读性。三个月后你回头看代码MapString, Object你根本不知道里面该有什么。但UserInfo这个类一摆出来字段一目了然配合 Lombok 的Data注解代码干净得像诗一样。Schema 生成。entity()方法会根据 JavaBean 的字段自动生成 JSON Schema 塞进 prompt。你字段定义得越清晰模型返回的格式就越稳定。用 Map 的话框架没法生成精确的 Schema只能给一个泛泛的“返回 JSON”的指令效果差很多。所以我的建议很明确只要你的输出结构是确定的就老老实实定义 JavaBean。只有那种字段完全动态、事先无法预知的场景才考虑用 Map 兜底。3. 核心细节解析与实操要点3.1 JavaBean 的定义规范与常见陷阱定义一个用于entity()解析的 JavaBean看起来简单但有几个细节不注意就会翻车。首先必须有无参构造函数。Jackson 反序列化的时候需要先创建对象再填充字段如果没有无参构造会直接报InvalidDefinitionException。用 Lombok 的话Data注解默认会生成无参构造但如果你同时用了AllArgsConstructor无参构造就会被覆盖掉这时候需要显式加上NoArgsConstructor。其次字段名要和模型返回的 key 对应。Java 习惯用驼峰命名比如userName但模型可能返回user_name或username。这时候有两种处理方式一是在字段上加JsonProperty(user_name)注解显式指定映射关系二是在 prompt 里明确告诉模型用驼峰命名。我一般两个都做双保险。第三避免使用复杂嵌套类型。entity()对嵌套对象的支持是有的但嵌套层级越深模型返回格式出错的概率越大。如果确实需要嵌套建议把嵌套对象也定义成独立的 JavaBean并且加上JsonIgnoreProperties(ignoreUnknown true)注解防止模型多返回字段导致解析失败。Data NoArgsConstructor JsonIgnoreProperties(ignoreUnknown true) public class UserInfo { JsonProperty(user_name) private String userName; JsonProperty(phone_number) private String phoneNumber; private String address; JsonProperty(intent_product) private String intentProduct; }这个类定义里JsonIgnoreProperties(ignoreUnknown true)是我强烈建议加的。模型有时候会“自作聪明”多返回几个字段比如confidence或者timestamp如果没有这个注解Jackson 遇到未知字段会直接抛异常。加上之后多余的字段会被静默忽略程序不会崩。3.2 Prompt 里必须写清楚的几件事虽然entity()会自动生成 JSON Schema 塞进 prompt但你不能完全当甩手掌柜。有几件事必须在 prompt 里说清楚否则模型还是会跑偏。第一明确告诉模型“只返回 JSON”。Schema 只是告诉模型“格式长什么样”但模型可能还是会加一些解释性文字。我通常会在 system prompt 里加一句“你的回答必须是一个合法的 JSON 对象不要包含任何额外的解释、前缀或后缀。”第二给出字段的语义说明。Schema 里只有字段名和类型模型不知道intentProduct到底该填什么。你需要在 prompt 里补充“intentProduct 字段表示用户咨询的产品名称如果用户没有提到具体产品该字段填 null。”第三处理边界情况。比如用户输入里没有电话信息模型应该返回什么是空字符串还是 null这些规则要提前定好并在 prompt 里说明。我一般建议统一用 null因为空字符串在后续处理时容易和“用户真的输入了空字符串”混淆。一个我常用的 prompt 模板长这样你是一个信息抽取助手。请从用户输入中提取以下字段 - userName: 用户姓名字符串类型 - phoneNumber: 电话号码字符串类型如果没有则返回 null - address: 地址字符串类型如果没有则返回 null - intentProduct: 意向产品字符串类型如果没有则返回 null 要求 1. 只返回一个 JSON 对象不要包含任何其他文字 2. 所有字段都必须出现没有值的字段填 null 3. 不要编造用户没有提供的信息3.3 温度参数对结构化输出的影响这是一个很多人忽略的点温度参数temperature直接影响结构化输出的稳定性。温度参数控制的是模型输出的随机性。温度越高模型越“有创造力”但同时也越容易偏离你要求的格式。做结构化输出的时候我一般把温度设到 0 或者 0.1让模型尽可能确定性地输出。实测下来温度从 0.7 降到 0格式错误率能从 15% 左右降到 2% 以下。这个提升非常明显。当然温度太低也有副作用——如果模型第一次就理解错了你的意图它会一直错下去不会“换个思路试试”。所以我的建议是结构化输出场景温度设 0 到 0.3 之间具体看模型的表现。在 Spring AI 里设置温度很简单ChatClient chatClient ChatClient.builder(chatModel) .defaultOptions(ChatOptions.builder() .temperature(0.1) .build()) .build();3.4 重试机制与降级策略即使做了上面所有准备模型偶尔还是会返回无法解析的内容。这时候不能直接让程序崩掉得有重试和降级机制。Spring AI 的entity()方法本身不包含重试逻辑但你可以自己包一层。我的做法是写一个工具方法最多重试三次每次重试时在 prompt 里追加一句“上次的回答格式不正确请严格按照 JSON 格式返回”。public T T extractWithRetry(String input, ClassT clazz, int maxRetries) { for (int i 0; i maxRetries; i) { try { String prompt i 0 ? input : input \n\n注意上次回答格式有误请只返回合法 JSON。; return chatClient.prompt() .user(prompt) .call() .entity(clazz); } catch (Exception e) { log.warn(第 {} 次解析失败: {}, i 1, e.getMessage()); } } // 降级返回一个默认对象或抛出自定义异常 throw new StructuredOutputException(结构化输出解析失败已重试 maxRetries 次); }如果三次都失败说明要么模型能力不够要么输入太复杂。这时候可以考虑降级到map()方法手动处理或者直接返回一个“解析失败”的标记让上游业务决定怎么处理。4. 完整实操流程与代码实现4.1 项目依赖与基础配置先说一下环境。我用的是 Spring Boot 3.2.x 加上 Spring AI 1.0.0-M1 版本。Maven 依赖主要有两个dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0-M1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency配置文件里需要设置模型的基本参数spring: ai: openai: api-key: ${AI_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.1这里模型我选的是 gpt-4o-mini原因是它在结构化输出任务上表现稳定而且成本低。如果你对精度要求极高可以换成更大的模型但成本会上去。温度设 0.1 前面说过了是为了格式稳定性。4.2 定义输出对象与 Service 层假设我们要做一个“用户咨询信息抽取”的功能从一段用户描述里提取姓名、电话、地址、意向产品。先定义 JavaBeanData NoArgsConstructor JsonIgnoreProperties(ignoreUnknown true) public class ConsultationInfo { JsonProperty(user_name) private String userName; JsonProperty(phone_number) private String phoneNumber; private String address; JsonProperty(intent_product) private String intentProduct; }然后写 ServiceService RequiredArgsConstructor public class ConsultationExtractService { private final ChatClient chatClient; private static final String SYSTEM_PROMPT 你是一个信息抽取助手。请从用户输入中提取以下字段 - user_name: 用户姓名字符串类型没有则返回 null - phone_number: 电话号码字符串类型没有则返回 null - address: 地址字符串类型没有则返回 null - intent_product: 意向产品字符串类型没有则返回 null 要求 1. 只返回一个 JSON 对象不要包含任何其他文字 2. 所有字段都必须出现没有值的字段填 null 3. 不要编造用户没有提供的信息 ; public ConsultationInfo extract(String userInput) { return chatClient.prompt() .system(SYSTEM_PROMPT) .user(userInput) .call() .entity(ConsultationInfo.class); } }注意chatClient是通过构造器注入的Spring AI 的 starter 会自动配置好这个 Bean。你只需要在配置类里定义一下Configuration public class ChatClientConfig { Bean public ChatClient chatClient(ChatModel chatModel) { return ChatClient.builder(chatModel) .defaultOptions(ChatOptions.builder() .temperature(0.1) .build()) .build(); } }4.3 测试与效果验证写个简单的测试用例跑一下SpringBootTest class ConsultationExtractServiceTest { Autowired private ConsultationExtractService service; Test void testExtract() { String input 你好我叫王小明电话是13800138000 住在北京市朝阳区想咨询一下你们的企业版产品。; ConsultationInfo info service.extract(input); assertNotNull(info); assertEquals(王小明, info.getUserName()); assertEquals(13800138000, info.getPhoneNumber()); assertEquals(北京市朝阳区, info.getAddress()); assertEquals(企业版产品, info.getIntentProduct()); } }实测下来这个用例在 gpt-4o-mini 上十次能过九次以上。偶尔失败的情况通常是地址字段被拆成了“北京市”和“朝阳区”两部分或者意向产品被填成了“企业版”而不是“企业版产品”。这些属于模型理解层面的偏差不是格式问题可以通过在 prompt 里加更精确的说明来改善。4.4 性能与成本考量用entity()相比手动解析会多消耗一些 token。原因是框架会在 prompt 里塞入 JSON Schema 的描述这部分内容大概会多出 100 到 200 个 token。按 gpt-4o-mini 的价格算每次调用多花不到一厘钱基本可以忽略。真正影响成本的是重试。如果格式错误率高重试次数多成本就会上去。所以前面强调的温度调低、prompt 写清楚不仅是为了稳定性也是为了省钱。性能方面entity()的解析本身是毫秒级的瓶颈还是在模型推理上。我实测下来一次完整的抽取调用包括网络往返大概在 1.5 到 3 秒之间取决于输入长度和模型负载。5. 常见问题与排查技巧实录5.1 解析失败的五大原因与解决方案我把实际项目中遇到的解析失败问题整理成了一个速查表问题现象根本原因解决方案UnrecognizedPropertyException模型返回了 JavaBean 里没有的字段加JsonIgnoreProperties(ignoreUnknown true)MismatchedInputException字段类型不匹配如字符串给了数字检查 prompt 里的类型说明或把字段改成兼容类型JsonParseException模型返回的不是合法 JSON降低温度强化 prompt 里的格式约束字段值为 null模型没提取到对应信息检查 prompt 里的字段说明是否清晰中文乱码编码问题确保请求和响应都用 UTF-8其中最常见的是第一个和第三个。UnrecognizedPropertyException加上注解就能解决属于一次性工作。JsonParseException则需要持续调优 prompt没有一劳永逸的办法。5.2 模型“自作聪明”加字段怎么办模型有时候会额外返回一些你没要求的字段比如confidence置信度、timestamp时间戳、source来源之类的。这些字段本身可能有用但如果你没在 JavaBean 里定义解析就会失败。两种处理方式一是加JsonIgnoreProperties(ignoreUnknown true)直接忽略二是在 JavaBean 里也定义这些字段把它们接住。我一般选第一种因为模型加的字段往往不稳定今天有明天没有接住了反而增加维护负担。但有一种情况例外如果模型频繁返回某个额外字段而且这个字段确实有价值那说明你的 prompt 可能没写清楚模型在“帮你补充”。这时候应该反思一下是不是该把这个字段正式加到需求里。5.3 嵌套对象解析的坑前面提到过嵌套对象的问题这里展开说一下。假设你要抽取的信息里包含一个“地址”对象里面有省、市、区三个字段Data NoArgsConstructor JsonIgnoreProperties(ignoreUnknown true) public class Address { private String province; private String city; private String district; } Data NoArgsConstructor JsonIgnoreProperties(ignoreUnknown true) public class UserInfo { private String userName; private Address address; }这种嵌套结构在 prompt 里需要描述得更详细否则模型很容易把地址拍平成一个字符串。我通常会在 prompt 里加一个示例address 字段是一个对象包含 province、city、district 三个子字段。 例如{province: 北京市, city: 北京市, district: 朝阳区}加了示例之后嵌套结构的解析成功率会明显提升。但即便如此嵌套层级也不建议超过两层太深了模型容易迷路。5.4 批量抽取时的注意事项如果你需要从一批文本里抽取信息不要在一个 prompt 里塞多条记录让模型返回数组。我试过模型在处理数组时格式错误率会显著上升而且一旦某一条出错整个数组都解析不了。正确的做法是逐条调用每条独立解析。虽然调用次数多了但稳定性高很多。如果非要批量可以考虑用并行流加速ListConsultationInfo results inputs.parallelStream() .map(service::extract) .collect(Collectors.toList());但要注意模型的速率限制并行度别开太高一般 3 到 5 就够了。5.5 我的独家避坑心得最后分享几个文档里不会写、但实际项目中特别有用的经验。第一先用小样本验证 Schema。定义好 JavaBean 之后别急着上大批量数据。先拿三五条典型输入跑一下看看模型返回的格式对不对。如果小样本就有问题大批量只会更糟。第二保留原始输出用于排查。entity()解析失败的时候异常信息里往往不包含模型的原始输出。建议在调用的时候把原始响应也记下来方便排查。Spring AI 的ChatResponse对象里有getResult().getOutput().getContent()可以拿到原始文本。第三字段名尽量用英文。虽然 JSON 支持中文 key但模型对中文 key 的处理不如英文稳定。用英文 key 加JsonProperty映射是最稳妥的方案。第四定期回归测试。模型提供方会不定期更新模型版本新版本的行为可能有变化。建议把结构化输出的测试用例纳入 CI每次模型升级后跑一遍及时发现兼容性问题。第五别把entity()当万能药。它解决的是“格式解析”问题不解决“语义理解”问题。如果模型本身就没理解你的意图返回的 JSON 格式再完美也没用。prompt 工程和模型选型该做的功课一样不能少。6. 从 entity() 延伸出去的一些思考entity()这个方法本身不复杂但它背后代表的思路值得琢磨把 AI 的不确定性封装在框架层让业务层尽可能确定。这和传统软件开发里“把变化关进笼子”的原则是一脉相承的。我在实际项目里用entity()做了好几个功能除了信息抽取还有意图分类、情感分析、表单自动填充。意图分类的场景特别有意思——我定义了一个IntentResult类里面有一个intent字段和一个confidence字段模型返回之后直接映射成对象业务层拿到的就是强类型的枚举值不用再做字符串匹配。这个模式还可以继续扩展。比如你可以定义一个ValidationResult类让模型帮你做数据校验返回“是否通过”和“不通过的原因”。或者定义一个TranslationResult类做多语言翻译。核心思路都是一样的定义好目标结构剩下的交给entity()。当然这套方案也有它的边界。如果输出结构极其复杂或者需要模型返回大量自由文本那entity()就不太合适了。工具是死的场景是活的选对工具比用好工具更重要。我在几个项目里反复用下来最大的体会是结构化输出这件事框架能帮你省掉 80% 的脏活但剩下 20% 的调优工作还是得靠你对业务和模型的理解。prompt 怎么写、温度怎么设、重试怎么做这些没有标准答案得根据你的具体场景去试。但至少你不用再手撕 JSON 了这已经是一个巨大的进步。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询