SpringAI 结构化输出 01:使用说明

大语言模型是文本输入、文本输出的系统。当下游代码需要根据某个字段做分支判断、持久化数据、基于结果执行业务逻辑时,需要将原始文本转换成带类型的对象记录。

结构化输出(Structured Output) 就是用来解决该问题的:引导大模型输出符合指定 Schema 的文本,由应用程序将其反序列化为类型安全的对象,最后让业务代码像使用普通领域对象一样执行操作。

Spring AI 在 ChatClient fluent API中,通过 .entity(…) 方法直接对外暴露结构化输出能力。只需定义期望返回结果的Java类型,Spring AI 就能完成全部工作:根据Java类型自动生成JSON Schema、向模型下发格式约束指令,最后将模型返回的响应反序列化为对应Java对象。

本文主要讲解高层级 ChatClient 使用方式。关于可靠性开关、底层API,请参考:

类型化响应(Typed Response)

定义 Java Record 类,描述想让模型返回的数据结构:

调用 ChatClient 填充该对象。不要调用返回原始文本的 .content(),改用 .entity(…) 并传入目标类型:

得到强类型对象,直接交给业务逻辑使用:

底层执行三步逻辑:

  1. Schema生成器,基于 ActorsFilms 记录生成JSON Schema;
  2. 将Schema追加到Prompt的系统上下文,告知模型遵循该格式输出;
  3. 获取模型返回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

可靠性配置项:EntityParamSpec

所有 .entity(…).responseEntity(…) 的重载方法,都接收可选参数 Consumer<EntityParamSpec>,可以开启两项独立、可组合的行为配置。

1. 输出格式异常不直接报错:validateSchema()

开启 validateSchema() 后会启用自修复重试循环:Spring AI 将响应内容与对象Schema做校验;校验失败时,会把校验错误信息追加到Prompt中重新发起调用;默认最多重试3次

参考文档:模式校验与自修复,了解重试逻辑与自定义重试次数。

2. 上游更强保障:useProviderStructuredOutput()

useProviderStructuredOutput() 将Schema作为API参数直接传给模型厂商,在模型服务端API层面强制格式约束,而不是仅靠Prompt提示词约束

前提: 底层模型必须支持厂商原生结构化输出能力。参考文档:厂商原生结构化输出,查看支持的模型与限制。

3.两项配置组合使用

两个开关解决不同层面问题,可以同时开启:

解释下核心方法:

  • useProviderStructuredOutput():API层约束,最大程度减少输出格式异常;
  • validateSchema():兜底防护,处理厂商边界case、推理模型偶尔出现的异常输出,自动重试修复。

注意: 如果下游业务代码完全不能容忍数据结构错乱,建议两项同时开启。

获取完整响应对象

.entity(…) 只会返回解析完成的业务对象。如果你还需要原始的 ChatResponse 对象(获取token消耗、可观测元数据等信息),使用 .responseEntity(…)

.responseEntity(…) 的使用方法与 .entity(…) 几乎完全一致:支持 Class、ParameterizedTypeReference、自定义 StructuredOutputConverterEntityParamSpec 配置。

自定义和非JSON输出

当内置JSON解析无法满足场景:模型输出被markdown代码块包裹,或者需要解析YAML、CSV等非JSON格式。可以传入自定义实现的 StructuredOutputConverter<T> 给 .entity(…)

详见文档:输出转换器 Output Converters

速查表

使用场景 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,工具调用本身就原生提供结构化输出。

原文:Structured Output