文章目录1. 前言2. 单个对象2.1 定义 Java 对象2.2 开启原生结构化输出2.2.1 模型参数2.2.2 ENABLE_NATIVE_STRUCTURED_OUTPUT 属性2.3 调用 entity 方法3. 泛型集合类型3.1 List3.2 Map4. responseEntity()5. 自定义 StructuredOutputConverter1. 前言在之前我们提到过想要实现结构化输出需要向提示词追加格式要求指令将模型输出转换为结构化类型实例分为两种模式非原生模式追加格式指令文本、Schema要用户提示词中原生模式需要模型支持原生结构化输出通过ChatOptions传递Schema更加可靠稳定接下来我们使用Spring AI提供的结构化输出功能进行案例演示。2. 单个对象演示需求提取合同中的关键信息为Java对象。2.1 定义 Java 对象定义合同信息record类/** * 合同信息实体类适配 Spring AI 原生结构化输出 */publicrecordContractInfo(/** * 合同编号必填格式HT开头8位数字 */JsonProperty(contractNo)// 指定 JSON 字段名确保和大模型输出映射StringcontractNo,/** * 合同名称必填 */JsonProperty(contractName)StringcontractName,/** * 签订日期必填ISO-8601格式yyyy-MM-dd */JsonProperty(signDate)StringsignDate,/** * 甲方名称必填 */JsonProperty(partyA)StringpartyA,/** * 乙方名称必填 */JsonProperty(partyB)StringpartyB,/** * 合同金额必填单位元保留2位小数 */JsonProperty(amount)BigDecimalamount,// 用BigDecimal避免浮点精度问题比Double更适合金额/** * 付款方式可选仅支持一次性付清/分3期/分12期/月结 */JsonProperty(valuepaymentMethod,requiredfalse)StringpaymentMethod,/** * 合同备注可选 */JsonProperty(valueremark,requiredfalse)Stringremark,/** * 有效期至可选ISO-8601格式yyyy-MM-dd */JsonProperty(valuevalidUntil,requiredfalse)StringvalidUntil){}支持通过JsonPropertyOrder注解指定属性的精确顺序例如// 在 AI 模型输出内容中contractName 始终在最前面JsonPropertyOrder({contractName,contractNo})recordActorsFilms(Stringactor,ListStringmovies){}注意事项使用BeanOutputConverter时生效适用于record类和普通Java类。2.2 开启原生结构化输出2.2.1 模型参数对于支持原生结构化输出的AI模型需要配置输出格式化模式相比基于提示词的方式更可靠。注意智谱AI只支持配置response-format实际并非完整的原生结构化输出。可以在application.yml中全局默认配置spring:application:name:ai-chat-demoai:# 智谱 GLM 配置zhipuai:api-key:f9d9e7c26bd24406ad# 替换为你的智谱 API Keybase-url:https://open.bigmodel.cn/api/paas# 可选默认值可省略chat:options:model:glm-4.7# 智谱模型名称如 glm-4、glm-4-flash 等response-format:json_object# text也可以在构建ChatClient时全局配置ZhiPuAiChatOptionschatOptionsZhiPuAiChatOptions.builder().responseFormat(ZhiPuAiApi.ChatCompletionRequest.ResponseFormat.jsonObject()).build();ChatClientclientChatClient.builder(zhiPuAiChatModel).defaultOptions(chatOptions).build();不过一般全局默认应该是默认的text文件输出在运行时配置json输出// 动态设置 response-formatStringresultclient.prompt().user(你的问题).options(ZhiPuAiChatOptions.builder().responseFormat(ZhiPuAiApi.ChatCompletionRequest.ResponseFormat.jsonObject())// 或 ResponseFormat.text().build()).call().content();2.2.2 ENABLE_NATIVE_STRUCTURED_OUTPUT 属性需要设置ENABLE_NATIVE_STRUCTURED_OUTPUT属性用于自动生成output.schema。ChatClient可配置以下属性publicenumChatClientAttributes{//formatter:off// 格式指令文本追加到用户 prompt告诉模型输出格式OUTPUT_FORMAT(spring.ai.chat.client.output.format),// JSON Schema 字串从 Java 类生成传给模型原生 APISTRUCTURED_OUTPUT_SCHEMA(spring.ai.chat.client.structured.output.schema),// 原生模式开关启用后 schema 直接传模型 API 而非注入 promptSTRUCTURED_OUTPUT_NATIVE(spring.ai.chat.client.structured.output.native);//formatter:onprivatefinalStringkey;ChatClientAttributes(Stringkey){this.keykey;}publicStringgetKey(){returnthis.key;}}配置如下ChatClient.CallResponseSpeccallResponseSpecclient.prompt().advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT).user(userInput).call();2.3 调用 entity 方法直接调用entity方法将输出转换为合同信息类StringuserInput 2026年度办公设备采购合同 合同编号HT20260313 甲方北京科技有限公司 乙方上海办公设备销售有限公司 甲乙双方经友好协商就办公设备采购事宜达成如下协议 第一条 采购内容 甲方向乙方采购办公设备一批具体型号及数量详见附件清单。 第二条 合同金额 合同总金额为人民币贰万伍仟捌佰元整¥25,800.00元。 第三条 付款方式 合同签订后分3期支付首付款30%于签约时支付二期款40%于设备到货时支付尾款30%于验收合格后支付。 第四条 交货期限 乙方须于2026年04月01日前完成全部设备交付及安装调试。 第五条 质量保证 乙方保证所供设备为全新正品符合国家质量标准提供一年免费保修服务。 第六条 违约责任 任何一方违约应向守约方支付合同金额10%的违约金。 第七条 本合同一式两份双方各执一份自签订之日起生效有效期至2027年03月12日。 甲方盖章 北京科技有限公司 乙方盖章 上海办公设备销售有限公司 签订日期 2026年03月13日 ;ContractInfocontractInfoclient.prompt().advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT).user(userInput).call()// 直接映射为ContractInfo实体无需手动解析JSON.entity(ContractInfo.class);运行后可以看到在模型请求中包含了格式要求指令以及自动生成的输出JSON Schema打印信息中可以看到成功被转换为 Java 对象3. 泛型集合类型3.1 List对于泛型集合类型需要使用Spring Framework提供的ParameterizedTypeReference解决Java泛型类型擦除问题。入口方法OverrideNullablepublicTTentity(ParameterizedTypeReferenceTtype){Assert.notNull(type,type cannot be null);returndoSingleWithBeanOutputConverter(newBeanOutputConverter(type));}在BeanOutputConverter会保留类型简单示例ListContractInfocontractInfoListclient.prompt().advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT).user(userInput).call().entity(newParameterizedTypeReferenceListContractInfo(){});生成的schema{$schema:https://json-schema.org/draft/2020-12/schema,type:array,items:{type:object,properties:{contractName:{type:string},contractNo:{type:string},signDate:{type:string},partyA:{type:string},partyB:{type:string},amount:{type:number},paymentMethod:{type:string},remark:{type:string},validUntil:{type:string}},required:[contractName,contractNo,signDate,partyA,partyB,amount,paymentMethod,remark,validUntil],additionalProperties:false}}返回结果3.2 MapMap类型也是一样简单示例MapString,ObjectobjectMapclient.prompt().advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT).user(userInput).call().entity(newParameterizedTypeReferenceMapString,Object(){});生成的schema{$schema:https://json-schema.org/draft/2020-12/schema,type:object,additionalProperties:false}4. responseEntity()responseEntity()可以返回结构化的ResponseEntity适用于需要同时访问结构化实体和响应元数据双返回值response (R)原始ChatResponse包含所有元数据entity (E)转换后的目标类型实体publicrecordResponseEntityR,E(NullableRresponse,NullableEentity){NullablepublicRgetResponse(){returnthis.response;}NullablepublicEgetEntity(){returnthis.entity;}}三个重载方法OverridepublicTResponseEntityChatResponse,TresponseEntity(ClassTtype){Assert.notNull(type,type cannot be null);returndoResponseEntity(newBeanOutputConverter(type));}OverridepublicTResponseEntityChatResponse,TresponseEntity(ParameterizedTypeReferenceTtype){Assert.notNull(type,type cannot be null);returndoResponseEntity(newBeanOutputConverter(type));}OverridepublicTResponseEntityChatResponse,TresponseEntity(StructuredOutputConverterTstructuredOutputConverter){Assert.notNull(structuredOutputConverter,structuredOutputConverter cannot be null);returndoResponseEntity(structuredOutputConverter);}使用方式和执行逻辑和entity方法基本一致只是返回值多了一个ChatResponse/** * 执行结构化输出转换的核心方法将 LLM 响应转换为目标类型。 * * p处理流程 * ol * li将格式指令存入 context供后续 Advisor 使用/li * li如果是原生结构化输出模式将 JSON Schema 存入 context/li * li调用模型获取响应/li * li使用转换器将响应文本转换为目标类型/li * /ol * * param outputConverter 结构化输出转换器包含格式指令和转换逻辑 * param T 目标类型 * return 包含 ChatResponse 和转换后实体的 ResponseEntity */protectedTResponseEntityChatResponse,TdoResponseEntity(StructuredOutputConverterToutputConverter){Assert.notNull(outputConverter,structuredOutputConverter cannot be null);// 将格式指令存入 contextChatModelCallAdvisor 会将其注入 prompt 或 ChatOptionsthis.request.context().put(ChatClientAttributes.OUTPUT_FORMAT.getKey(),outputConverter.getFormat());// 原生结构化输出模式将 JSON Schema 存入 context// ChatModelCallAdvisor 会将其设置到 StructuredOutputChatOptions 中if(Boolean.TRUE.equals(this.request.context().get(ChatClientAttributes.STRUCTURED_OUTPUT_NATIVE.getKey()))outputConverterinstanceofBeanOutputConverterbeanOutputConverter){this.request.context().put(ChatClientAttributes.STRUCTURED_OUTPUT_SCHEMA.getKey(),beanOutputConverter.getJsonSchema());}// 执行请求并获取响应varchatResponsedoGetObservableChatClientResponse(this.request).chatResponse();// 提取响应文本内容varresponseContentgetContentFromChatResponse(chatResponse);if(responseContentnull){returnnewResponseEntity(chatResponse,null);}// 将响应文本转换为目标类型TentityoutputConverter.convert(responseContent);returnnewResponseEntity(chatResponse,entity);}简单示例ResponseEntityChatResponse,ContractInfochatResponseContractInfoResponseEntityclient.prompt().advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT).user(userInput).call().responseEntity(ContractInfo.class);返回结果5. 自定义 StructuredOutputConverter我们可以自定义实现StructuredOutputConverter以适配更多的场景比如实现中文的格式指令支持更多的schema简单示例publicclassChineseBeanOutputConverterTimplementsStructuredOutputConverterT{privatefinalTypetype;privatefinalObjectMapperobjectMapper;privatefinalStringjsonSchema;privatefinalResponseTextCleanertextCleaner;publicChineseBeanOutputConverter(ClassTclazz){this(ParameterizedTypeReference.forType(clazz),newObjectMapper(),null);}publicChineseBeanOutputConverter(ClassTclazz,ObjectMapperobjectMapper){this(ParameterizedTypeReference.forType(clazz),objectMapper,null);}publicChineseBeanOutputConverter(ClassTclazz,ObjectMapperobjectMapper,ResponseTextCleanertextCleaner){this(ParameterizedTypeReference.forType(clazz),objectMapper,textCleaner);}publicChineseBeanOutputConverter(ParameterizedTypeReferenceTtypeRef){this(typeRef,newObjectMapper(),null);}publicChineseBeanOutputConverter(ParameterizedTypeReferenceTtypeRef,ObjectMapperobjectMapper,ResponseTextCleanertextCleaner){this.typetypeRef.getType();this.objectMapperobjectMapper;this.textCleanertextCleaner;this.jsonSchemagenerateSchema();}/** * 生成 JSON Schema */privateStringgenerateSchema(){JacksonModulejacksonModulenewJacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED,JacksonOption.RESPECT_JSONPROPERTY_ORDER);SchemaGeneratorConfigBuilderconfigBuildernewSchemaGeneratorConfigBuilder(com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12,com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON).with(jacksonModule).with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT);// 所有字段必填configBuilder.forFields().withRequiredCheck(f-true);SchemaGeneratorConfigconfigconfigBuilder.build();SchemaGeneratorgeneratornewSchemaGenerator(config);JsonNodejsonNodegenerator.generateSchema(this.type);returnjsonNode.toPrettyString();}/** * 中文格式指令 */OverridepublicStringgetFormat(){Stringtemplate 你的回答必须是 JSON 格式。 不要包含任何解释说明只提供符合 RFC8259 标准的 JSON 响应严格按照以下格式输出不得有任何偏差。 不要在回答中包含 markdown 代码块标记。 请移除输出中的 json markdown 标记。 以下是你的输出必须遵循的 JSON Schema %s ;returnString.format(template,this.jsonSchema);}SuppressWarnings(unchecked)OverridepublicTconvert(Stringtext){try{// 如果有文本清理器先清理文本if(this.textCleaner!null){textthis.textCleaner.clean(text);}return(T)this.objectMapper.readValue(text,this.objectMapper.constructType(this.type));}catch(JsonProcessingExceptione){thrownewRuntimeException(无法将文本转换为目标类型: this.type,e);}}publicStringgetJsonSchema(){returnthis.jsonSchema;}}使用示例// 使用自定义转换器ChineseBeanOutputConverterContractInfoconverternewChineseBeanOutputConverter(ContractInfo.class);ContractInfocontractInfoclient.prompt().advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT).user(userInput).call().entity(converter);生成的schema