主题
结构化输出
大语言模型是文本进、文本出的系统。 一旦下游代码需要依据某个字段做路由、持久化某个值、或根据某个结果做分支,这段文本就必须变成一个有类型的记录。 结构化输出弥合了这一鸿沟:模型被引导产出符合某个模式的文本,应用再把它解析回一个有类型的对象,代码库的其他部分可以像对待任何其他领域类型一样对待它。
Spring AI 通过 .entity(...) 在 ChatClient 流式 API 上直接暴露结构化输出。 你为你想要的返回形状定义一个 Java 类型,其余交给 Spring AI:JSON 模式由你的类型生成、模型被指示遵循该模式、响应再被反序列化为你的类型。
本页介绍高层的 ChatClient 路径。 关于可靠性开关和更底层 API,参见:
- 模式校验与自我纠正 —— 用
validateSchema()检测格式错误的输出并自动重试。 - 服务商原生的结构化输出 —— 用
useProviderStructuredOutput()在服务商 API 层面强制模式。 - 输出转换器 —— 更底层的
StructuredOutputConverterAPI、内置转换器,以及自定义/非 JSON 转换器。
有类型的响应
为你想要的返回形状定义一个 Java record:
java
record ActorsFilms(String actor, List<String> movies) {}让 ChatClient 来填充它。 不要用 .content()(它返回原始文本回复)来结束调用,而是用 .entity(...) 结束并传入你的目标类型:
java
ActorsFilms films = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorsFilms.class);结果是一个有类型的 ActorsFilms,你可以把它传给代码库的其他部分:
java
films.actor(); // "Tom Hanks"
films.movies(); // ["Forrest Gump", "Cast Away", ...]在幕后,Spring AI 做了三件事:一个模式生成器把你的 ActorsFilms record 转换为一个 JSON 模式、该模式被追加到提示词的系统上下文中、而模型的 JSON 回答则交给一个类型转换器,把它解析回你的 record。
这对 Spring AI 支持的每一个模型都有效——它不带有特定服务商的耦合。
NOTE
.entity(...) 仅适用于 .call()。 有类型的解析需要完整的响应,因此它在流式路径上不可用(.stream() 返回的是文本块,而非有类型的对象)。 这适用于本页的每一种变体——Class、ParameterizedTypeReference、自定义转换器,无论是否启用可靠性开关。
默认的 .entity(...) 调用没有任何保证。 模型是被要求产出匹配该模式的 JSON,而非被强制。 多数时候它会照做;但有时它会多返回一个字段、漏掉一个必填字段、或把 JSON 包裹在文字里,于是解析器抛出异常。 下面的两个开关正是为解决这个问题而生。
泛型类型:列表、映射及更多
.entity(Class) 用于具体类。 对于泛型类型——List<ActorsFilms>、Map<String, ActorsFilms>——请使用 ParameterizedTypeReference:
java
List<ActorsFilms> films = chatClient.prompt()
.user("Generate filmographies for three random actors.")
.call()
.entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});可靠性开关:EntityParamSpec
所有 .entity(...)(以及 .responseEntity(...))重载都接受一个可选的 Consumer<EntityParamSpec>,用于启用两个相互独立、可组合的行为。
遇到格式错误的输出不失败:validateSchema()
validateSchema() 会开启一个自我纠正的重试循环:Spring AI 依据实体的模式校验响应,如果校验失败,具体的错误会被追加到提示词中并重新发起调用——默认最多 3 次尝试。
java
ActorsFilms films = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorsFilms.class, spec -> spec.validateSchema());关于重试循环如何工作以及如何定制,参见 模式校验与自我纠正。
更强的上游保证:useProviderStructuredOutput()
useProviderStructuredOutput() 把模式作为 API 层面的约束发送给服务商,从而由服务商的运行时来强制符合性,而不是依赖提示词里的指示。
java
ActorsFilms films = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorsFilms.class, spec -> spec.useProviderStructuredOutput());这要求底层模型支持原生的结构化输出。 关于受支持的服务商与限制,参见 服务商原生的结构化输出。
两者结合
这两个开关解决的是不同的问题,并且能自然地组合:
java
ActorsFilms films = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorsFilms.class, spec -> spec
.useProviderStructuredOutput()
.validateSchema());useProviderStructuredOutput() 通过在 API 层面约束模型,把输出格式错误的几率降到最低。 validateSchema() 则捕获剩下的情形——服务商的边缘情况、推理模型的怪癖——并自动纠正它们。 当下游代码无法容忍形状漂移时,两者都用上。
获取完整响应
.entity(...) 只返回解析后的对象。 如果你还需要底层的 ChatResponse——用于令牌用量、可观测性元数据,或实体之外的任何信息——请使用 .responseEntity(...):
java
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();它与 .entity(...) 拥有一组相同的重载——Class、ParameterizedTypeReference、自定义 StructuredOutputConverter,以及 EntityParamSpec 消费者均适用。
自定义与非 JSON 输出
当内置的 JSON 解析不够用时——模型把 JSON 包裹在 markdown 围栏里,或者你需要 YAML、CSV 这类非 JSON 格式——就把你自己的 StructuredOutputConverter<T> 传给 .entity(...)。 参见 输出转换器。
速查表
| 你需要 | 用法 |
|---|---|
| 默认——适用于每一个服务商 | .entity(Type.class) |
泛型类型如 List<T>、Map<K,V> | .entity(new ParameterizedTypeReference<...>() {}) |
| 遇到格式错误的输出不失败 | .entity(Type.class, spec -> spec.validateSchema()) |
| 来自服务商的更强上游保证 | .entity(Type.class, spec -> spec.useProviderStructuredOutput()) |
| 两者都用——请求约束 + 响应重试 | .entity(Type.class, spec -> spec.useProviderStructuredOutput().validateSchema()) |
| 连同实体一起获取令牌用量/元数据 | .responseEntity(...)(同样的重载) |
| 模型把 JSON 包裹在 markdown 围栏里,或非 JSON 格式 | 实现 StructuredOutputConverter<T> 并把它传给 .entity(...) |
| 流式响应 | 不支持——.entity(...) 仅适用于 .call();.stream() 返回文本块而非有类型的对象 |
TIP
结构化输出是把模型输出转换为结构化类型的一种尽力而为的做法。
不保证模型一定会返回所请求的结构。 当正确性很重要时,请使用 validateSchema() 和/或 useProviderStructuredOutput()。
NOTE
StructuredOutputConverter 不用于 LLM 工具调用,因为该特性本身就默认提供结构化输出。