大语言模型是文本输入、文本输出的系统。当下游代码需要根据某个字段做分支判断、持久化数据、基于结果执行业务逻辑时,需要将原始文本转换成带类型的对象记录。
结构化输出(Structured Output) 就是用来解决该问题的:引导大模型输出符合指定 Schema 的文本,由应用程序将其反序列化为类型安全的对象,最后让业务代码像使用普通领域对象一样执行操作。
Spring AI 在 ChatClient fluent API中,通过 .entity(…) 方法直接对外暴露结构化输出能力。只需定义期望返回结果的Java类型,Spring AI 就能完成全部工作:根据Java类型自动生成JSON Schema、向模型下发格式约束指令,最后将模型返回的响应反序列化为对应Java对象。
本文主要讲解高层级 ChatClient 使用方式。关于可靠性开关、底层API,请参考:
- Schema校验与自修复 — 使用
validateSchema()检测格式错误输出,并自动重试 - 厂商原生结构化输出 — 使用
useProviderStructuredOutput()在模型API层面强制Schema约束 - 输出转换器 — 底层
StructuredOutputConverterAPI、内置转换器、自定义非JSON转换器
类型化响应(Typed Response)
定义 Java Record 类,描述想让模型返回的数据结构:
|
1 |
record ActorsFilms(String actor, List<String> movies) {} |
调用 ChatClient 填充该对象。不要调用返回原始文本的 .content(),改用 .entity(…) 并传入目标类型:
|
1 2 3 4 |
ActorsFilms films = chatClient.prompt() .user("Generate the filmography for a random actor.") .call() .entity(ActorsFilms.class); |
得到强类型对象,直接交给业务逻辑使用:
|
1 2 |
films.actor(); // "Tom Hanks" films.movies(); // ["Forrest Gump", "Cast Away", ...] |
底层执行三步逻辑:
- Schema生成器,基于
ActorsFilms记录生成JSON Schema; - 将Schema追加到Prompt的系统上下文,告知模型遵循该格式输出;
- 获取模型返回JSON,通过类型转换器解析为Java Record。
注意: 该能力对 Spring AI 支持的全部大模型厂商都生效,不属于某一家厂商特有能力。
⚠️ .entity(…) 仅支持 .call() 同步调用方式。类型解析需要拿到完整响应报文,流式接口 .stream() 返回文本分片,无法转换成类型对象。本文所有API都遵循该限制:普通Class、ParameterizedTypeReference、自定义转换器、可靠性开关均不能用于流式调用。
默认调用 .entity(…) 不提供绝对强制保障:只是请求模型输出符合Schema的JSON,而不是强制。绝大多数情况模型可以遵守;但偶尔会返回多余字段、缺失必填字段,或者在JSON前后附带自然语言描述,此时解析器会抛出异常。下面两个配置项用于解决这类问题。
泛型类型:List、Map 及更多类型
.entity(Class) 适用于普通具体类。如果是泛型类型,例如 List<ActorsFilms>、Map<String, ActorsFilms>,使用 ParameterizedTypeReference:
|
1 2 3 4 |
List<ActorsFilms> films = chatClient.prompt() .user("Generate filmographies for three random actors.") .call() .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {}); |
可靠性配置项:EntityParamSpec
所有 .entity(…)、.responseEntity(…) 的重载方法,都接收可选参数 Consumer<EntityParamSpec>,可以开启两项独立、可组合的行为配置。
1. 输出格式异常不直接报错:validateSchema()
开启 validateSchema() 后会启用自修复重试循环:Spring AI 将响应内容与对象Schema做校验;校验失败时,会把校验错误信息追加到Prompt中重新发起调用;默认最多重试3次。
|
1 2 3 4 |
ActorsFilms films = chatClient.prompt() .user("Generate the filmography for a random actor.") .call() .entity(ActorsFilms.class, spec -> spec.validateSchema()); |
参考文档:模式校验与自修复,了解重试逻辑与自定义重试次数。
2. 上游更强保障:useProviderStructuredOutput()
useProviderStructuredOutput() 将Schema作为API参数直接传给模型厂商,在模型服务端API层面强制格式约束,而不是仅靠Prompt提示词约束。
|
1 2 3 4 |
ActorsFilms films = chatClient.prompt() .user("Generate the filmography for a random actor.") .call() .entity(ActorsFilms.class, spec -> spec.useProviderStructuredOutput()); |
前提: 底层模型必须支持厂商原生结构化输出能力。参考文档:厂商原生结构化输出,查看支持的模型与限制。
3.两项配置组合使用
两个开关解决不同层面问题,可以同时开启:
|
1 2 3 4 5 6 |
ActorsFilms films = chatClient.prompt() .user("Generate the filmography for a random actor.") .call() .entity(ActorsFilms.class, spec -> spec .useProviderStructuredOutput() .validateSchema()); |
解释下核心方法:
useProviderStructuredOutput():API层约束,最大程度减少输出格式异常;validateSchema():兜底防护,处理厂商边界case、推理模型偶尔出现的异常输出,自动重试修复。
注意: 如果下游业务代码完全不能容忍数据结构错乱,建议两项同时开启。
获取完整响应对象
.entity(…) 只会返回解析完成的业务对象。如果你还需要原始的 ChatResponse 对象(获取token消耗、可观测元数据等信息),使用 .responseEntity(…)。
|
1 2 3 4 5 6 7 8 |
ResponseEntity<ChatResponse, ActorsFilms> result = chatClient.prompt() .user("Generate the filmography for a random actor.") .call() .responseEntity(ActorsFilms.class); ActorsFilms films = result.entity(); ChatResponse raw = result.response(); long totalTokens = raw.getMetadata().getUsage().getTotalTokens(); |
.responseEntity(…) 的使用方法与 .entity(…) 几乎完全一致:支持 Class、ParameterizedTypeReference、自定义 StructuredOutputConverter、EntityParamSpec 配置。
自定义和非JSON输出
当内置JSON解析无法满足场景:模型输出被markdown代码块包裹,或者需要解析YAML、CSV等非JSON格式。可以传入自定义实现的 StructuredOutputConverter<T> 给 .entity(…)。
速查表
| 使用场景 | API写法 |
|---|---|
| 默认方案,兼容全部模型厂商 | .entity(Type.class) |
泛型集合 List<T>、Map<K,V> |
.entity(new ParameterizedTypeReference<… >() {}) |
| 格式出错不要直接抛异常,自动重试 | .entity(Type.class, spec -> spec.validateSchema()) |
| 启用模型厂商API原生结构化输出 | .entity(Type.class, spec -> spec.useProviderStructuredOutput()) |
| 双重保障:API约束 + 响应校验重试 | .entity(Type.class, spec -> spec.useProviderStructuredOutput().validateSchema()) |
| 需要同时拿到Token消耗、原始响应元数据 | .responseEntity(…)(重载用法完全一致) |
| JSON被markdown包裹 / 需要YAML/CSV自定义解析 | 实现 StructuredOutputConverter<T>,传入entity方法 |
| 流式stream响应 | ❌不支持;.entity仅可用于 .call(),stream返回文本块,不能转为强类型对象 |
⚠️重要提示: 结构化输出属于尽力而为能力,并不保证模型100%返回期望结构。业务对正确性有要求时,务必开启 validateSchema() 或 useProviderStructuredOutput(),或两者同时开启。
注意:LLM工具调用(Tool Calling)不使用 StructuredOutputConverter,工具调用本身就原生提供结构化输出。