拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring AI 对多模型结构化输出的 Schema 兼容性处理

Spring AI 对多模型结构化输出的 Schema 兼容性处理把 LLM 接入生产环境的 Java 业务系统时最让人头疼的往往不是 Prompt 怎么写而是模型返回数据的稳定性。很多团队在单接 OpenAI 时直接使用 Spring AI 提供的BeanOutputConverter或官方推荐的StructuredOutputConverter定义一个 Java POJO框架自动生成 JSON Schema 注入 Prompt解析顺畅无阻。当业务需要做多模型路由把一部分非核心流量切到通义千问、DeepSeek、Claude 甚至本地私有化部署的 Ollama如 Qwen2.5-7B、Llama3.1时JSON 反序列化异常就会频繁冒出来。各个模型厂商在底层对 JSON Schema 的规范支持程度参差不齐。有的模型强制要求 JSON Schema 的根节点必须包含additionalProperties: false有的模型遇到复杂泛型或递归嵌套对象时会直接忽略required约束本地小参数量模型在生成长 JSON 时经常会夹杂 Markdown 代码块包裹符如json或者输出截断。如果不做统一的 Schema 兼容与解析容错多模型路由就会直接演变成下游业务的崩溃源。Spring AI 默认 Schema 生成机制分析Spring AI 默认使用BeanOutputConverterT来处理结构化输出。深入其内部实现它依赖com.github.victools.jsonschema.generator库通过反射扫描传入的 Java Class并生成符合 Draft 7 或 Draft 2020-12 标准的 JSON Schema。生成的 Schema 会被拼接进系统预设的 Format 提示词中Your response should be in JSON format. Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation. Do not include markdown code blocks in the response. Remove the json markdown from the output. Here is the JSON Schema instance your output must adhere to:这种机制在配合 GPT-4o 这类高级模型时基本能正常工作但在多厂商混用场景下暴露出两个明显缺陷Schema 语法方言不兼容部分中转网关或厂商 API例如某些支持 Function Call / Response Format 的专用 endpoint要求极度精简的 Schema 语法不支持title、description以外的扩展关键字甚至不支持 Java 枚举生成的复杂的anyOf结构。后置解析过于脆弱BeanOutputConverter的convert(String text)方法内部直接调用 Jackson 的objectMapper.readValue(text, this.clazz)。一旦模型在 JSON 前后带有一句“好的这是为您提取的信息”或者尾部多了一个逗号整个调用链条立刻抛出ConversionFailedException。统一结构化输出转换器设计为了适配混合模型路由架构不能完全依赖 Spring AI 默认的转换逻辑需要构建一套兼容各主流厂商 Schema 方言并具备自愈能力的RobustBeanOutputConverter。1. 业务返回模型定义定义一个电商领域标准的工单实体提取 POJO包含基础字段、枚举类型以及集合嵌套package com.example.ai.schema.model; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import lombok.Data; import java.util.List; Data public class OrderTicketAnalysis { JsonProperty(required true) JsonPropertyDescription(工单所属的订单编号) private String orderId; JsonProperty(required true) JsonPropertyDescription(客户核心诉求类别) private TicketCategory category; JsonPropertyDescription(用户情绪评分范围 1 到 55 为极其愤怒) private Integer urgencyLevel; JsonPropertyDescription(从文本中提取的具体商品问题条目列表) private ListIssueItem issues; public enum TicketCategory { LOGISTICS_DELAY, REFUND_REQUEST, PRODUCT_DAMAGE, OTHER } Data public static class IssueItem { JsonProperty(required true) JsonPropertyDescription(涉及的商品名称) private String productName; JsonPropertyDescription(具体损坏或缺失描述) private String problemDetail; } }2. 自定义 Schema 生成器与清洗逻辑针对不同厂商模型精简 Schema剥离导致解析报错的元数据同时增加对输出内容的预清洗Markdown 代码块剥离、前后缀废话剪裁、宽松反序列化配置package com.example.ai.schema.converter; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.victools.jsonschema.generator.Option; import com.github.victools.jsonschema.generator.OptionPreset; import com.github.victools.jsonschema.generator.SchemaGenerator; import com.github.victools.jsonschema.generator.SchemaGeneratorConfig; import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; import com.github.victools.jsonschema.generator.SchemaVersion; import org.springframework.ai.converter.StructuredOutputConverter; import org.springframework.lang.NonNull; import java.util.regex.Matcher; import java.util.regex.Pattern; public class RobustBeanOutputConverterT implements StructuredOutputConverterT { private final ClassT targetClass; private final ObjectMapper objectMapper; private final String jsonSchema; private static final Pattern MARKDOWN_JSON_PATTERN Pattern.compile( (?:json)?\\s*([\\s\\S]*?)\\s*, Pattern.CASE_INSENSITIVE ); public RobustBeanOutputConverter(ClassT targetClass) { this.targetClass targetClass; this.objectMapper createResilientObjectMapper(); this.jsonSchema generateCompatibleSchema(targetClass); } private ObjectMapper createResilientObjectMapper() { ObjectMapper mapper new ObjectMapper(); // 允许非标准 JSON 语法单引号、未引号字段、控制字符等 mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); mapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true); mapper.configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true); // 忽略目标 POJO 中不存在的额外字段 mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 空字符串允许转换为 null mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); return mapper; } private String generateCompatibleSchema(ClassT clazz) { SchemaGeneratorConfigBuilder configBuilder new SchemaGeneratorConfigBuilder( SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON ); // 统一配置展开枚举为简单字符串列表避免生成复杂的 anyOf configBuilder.with(Option.FLATTENED_ENUMS) .without(Option.SCHEMA_VERSION_INDICATOR); SchemaGeneratorConfig config configBuilder.build(); SchemaGenerator generator new SchemaGenerator(config); JsonNode jsonNode generator.generateSchema(clazz); return jsonNode.toString(); } Override public String getFormat() { return String.format( 请严格按照以下 JSON Schema 输出纯 JSON 数据。禁止输出解释性文字禁止添加 markdown 代码块标签。\n JSON Schema:\n%s, this.jsonSchema ); } Override public T convert(NonNull String source) { String cleanedJson extractJsonContent(source); try { return this.objectMapper.readValue(cleanedJson, this.targetClass); } catch (Exception e) { throw new IllegalArgumentException(结构化数据反序列化失败原始内容: source, e); } } /** * 智能提取字符串中的纯 JSON 块 */ private String extractJsonContent(String raw) { if (raw null || raw.trim().isEmpty()) { return {}; } String trimmed raw.trim(); // 1. 优先提取 Markdown 代码块内部内容 Matcher matcher MARKDOWN_JSON_PATTERN.matcher(trimmed); if (matcher.find()) { trimmed matcher.group(1).trim(); } // 2. 如果包含首尾外围文本寻找最外层大括号 int startBrace trimmed.indexOf({); int endBrace trimmed.lastIndexOf(}); if (startBrace ! -1 endBrace ! -1 endBrace startBrace) { trimmed trimmed.substring(startBrace, endBrace 1); } return trimmed; } }多模型路由中的客户端调用与降级策略在业务服务中通过 Spring AI 的ChatClient结合我们实现的RobustBeanOutputConverter。当主模型如公有云高参数量模型调用失败或输出结构断裂时无缝降级至备用模型并执行两次重试package com.example.ai.schema.service; import com.example.ai.schema.converter.RobustBeanOutputConverter; import com.example.ai.schema.model.OrderTicketAnalysis; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import java.util.Map; Slf4j Service RequiredArgsConstructor public class TicketAnalysisService { Qualifier(primaryChatClient) private final ChatClient primaryChatClient; Qualifier(fallbackChatClient) private final ChatClient fallbackChatClient; private final RobustBeanOutputConverterOrderTicketAnalysis converter new RobustBeanOutputConverter(OrderTicketAnalysis.class); public OrderTicketAnalysis analyzeTicket(String userComplaint) { String templateText 请分析以下客户工单反馈内容提取结构化信息\n\n 【客户反馈】{complaint}\n\n {format}; PromptTemplate promptTemplate new PromptTemplate(templateText); Prompt prompt promptTemplate.create(Map.of( complaint, userComplaint, format, converter.getFormat() )); // 优先使用主模型调用 try { String content primaryChatClient.prompt(prompt) .call() .content(); return converter.convert(content); } catch (Exception e) { log.warn(主模型结构化提取失败触发降级模型重试: {}, e.getMessage()); return executeFallback(prompt); } } private OrderTicketAnalysis executeFallback(Prompt prompt) { try { String content fallbackChatClient.prompt(prompt) .call() .content(); return converter.convert(content); } catch (Exception e) { log.error(降级模型调用依然失败进入兜底对象构建, e); OrderTicketAnalysis fallbackObject new OrderTicketAnalysis(); fallbackObject.setCategory(OrderTicketAnalysis.TicketCategory.OTHER); fallbackObject.setUrgencyLevel(3); return fallbackObject; } } }架构演进思考与排坑经验在落地多模型架构时不能只寄希望于大模型具备 100% 的遵循能力。以下是几个经过压测和生产验证的实践准则枚举字段必须声明默认值小模型很容易在枚举匹配上产生微小幻觉如将REFUND_REQUEST写成REFUND_REQUIREMENT。在 Jackson 反序列化时应配合JsonEnumDefaultValue注解或编写自定义反序列化器遇到未知枚举值时兜底为OTHER避免整条链路中断。Schema 深度不宜超过三层超过三层的嵌套数组和对象结构开源 7B / 14B 参数级别模型的失误率呈指数级上升。对于特别复杂的模型应拆解为多次链式调用Chain of Thought每次只提取一个子对象。结合平台原生 JSON 模式如果底层模型支持response_format: { type: json_object }应在 Spring AI 的ChatOptions中显式开启。这能直接从采样层抑制模型吐出 Markdown 标签配合上文的预清洗逻辑双重保障系统的健壮性。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门