Spring AI 输出解析器进阶

Spring AI 输出解析器进阶

本文深入讲解 Spring AI 中各种输出解析器的使用技巧,从基础 BeanOutputConverter 到自定义复杂结构解析、流式解析以及生产级容错方案。


一、输出解析器概览

1.1 解析器类型

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                     Spring AI 输出解析器体系                    │
│                                                                 │
│  ┌──────────────────────┐    ┌──────────────────────┐          │
│  │  BeanOutputConverter │    │  MapOutputConverter  │          │
│  │  (POJO 转换)         │    │  (Map 转换)          │          │
│  └──────────────────────┘    └──────────────────────┘          │
│  ┌──────────────────────┐    ┌──────────────────────┐          │
│  │ ListOutputConverter  │    │  自定义解析器         │          │
│  │ (列表转换)           │    │  (正则/XML/流式)      │          │
│  └──────────────────────┘    └──────────────────────┘          │
└─────────────────────────────────────────────────────────────────┘

1.2 解析器选择策略

场景 推荐解析器 备注
单对象(固定结构) BeanOutputConverter 最常用,支持嵌套和泛型
动态键值对(未知结构) MapOutputConverter 灵活但类型不安全
简单列表(同质元素) ListOutputConverter 元素类型需一致
复杂嵌套 + 泛型 自定义 + ParameterizedTypeReference 解决类型擦除问题
非 JSON 格式(如 XML) 自定义 XML 解析器 需要额外解析库
流式输出(增量解析) 自定义流式解析器 需要处理不完整的 JSON
需要严格数据验证 带验证的装饰器模式 结合校验框架(如 Jakarta Validation)

1.3 输出解析器的核心工作流程

text 复制代码
┌─────────────┐   ┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│ LLM 原始输出 │──▶│ 格式转换    │──▶│ 类型映射    │──▶│ 验证与清理  │
│ (String)    │   │ (提取 JSON) │   │ (反序列化)  │   │ (Validator) │
└─────────────┘   └─────────────┘   └─────────────┘   └─────────────┘

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、BeanOutputConverter 进阶用法

2.1 嵌套对象解析(完整版)

java 复制代码
package com.example.ai.parser;

import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.ai.chat.client.ChatClient;

import java.util.List;

/**
 * 嵌套对象解析示例
 */
@Service
public class NestedParserDemo {

    private final ChatClient chatClient;

    public NestedParserDemo(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    /**
     * 解析公司信息(嵌套结构)
     */
    public CompanyInfo parseCompany(String userInput) {
        // 使用 ParameterizedTypeReference 保留泛型信息
        var converter = new BeanOutputConverter<CompanyInfo>(
            ParameterizedTypeReference.forType(CompanyInfo.class)
        );

        String format = converter.getFormat();  // 生成 JSON schema 描述
        
        String prompt = """
            从以下文本中提取公司信息,严格按照指定的 JSON 格式返回,不要包含任何额外内容。
            %s
            
            文本:%s
            """.formatted(format, userInput);

        String response = chatClient.prompt(prompt).call().content();
        return converter.convert(response);
    }

    // 内部类定义同原文档,但增加 Lombok 注解简化
    @Data
    public static class CompanyInfo {
        private String name;
        private String industry;
        private Address headquarters;
        private List<Department> departments;
        private Financials financials;
    }

    @Data
    public static class Address {
        private String street;
        private String city;
        private String country;
    }

    @Data
    public static class Department {
        private String name;
        private int employeeCount;
        private String manager;
    }

    @Data
    public static class Financials {
        private double revenue;
        private double profit;
        private int year;
    }
}

嵌套解析的注意事项

  • 使用 ParameterizedTypeReference 是必须的,否则泛型类型信息会被擦除。
  • 嵌套对象的字段名称必须与 JSON 中的 key 匹配(可通过 @JsonProperty 自定义映射)。
  • 如果某些字段可能缺失,使用 @JsonIgnoreProperties(ignoreUnknown = true) 避免反序列化失败。

2.2 泛型集合解析(增强)

java 复制代码
package com.example.ai.parser;

import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 泛型集合解析(支持任意元素类型)
 */
@Service
public class GenericCollectionParser {

    private final ChatClient chatClient;

    public GenericCollectionParser(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    /**
     * 解析任意类型的列表
     */
    public <T> List<T> parseList(String content, Class<T> elementType) {
        // 使用匿名内部类创建 ParameterizedTypeReference
        var converter = new BeanOutputConverter<List<T>>(
            new ParameterizedTypeReference<List<T>>() {}
        );

        String format = converter.getFormat();
        String prompt = """
            提取数据列表,返回 JSON 数组,每个元素的格式必须符合:
            %s
            
            输入内容:%s
            """.formatted(format, content);

        String response = chatClient.prompt(prompt).call().content();
        return converter.convert(response);
    }

    /**
     * 解析分页结果(泛型版本)
     */
    public <T> PageResult<T> parsePageResult(String content, Class<T> elementType) {
        var converter = new BeanOutputConverter<PageResult<T>>(
            new ParameterizedTypeReference<PageResult<T>>() {}
        );

        String prompt = """
            提取分页数据,返回 JSON 对象,包含以下字段:
            - items: 数组,元素类型为 %s
            - total: int 总数
            - page: int 当前页号
            - pageSize: int 每页大小
            
            输入内容:%s
            """.formatted(elementType.getSimpleName(), content);

        String response = chatClient.prompt(prompt).call().content();
        return converter.convert(response);
    }

    @Data
    public static class PageResult<T> {
        private List<T> items;
        private int total;
        private int page;
        private int pageSize;
    }
}

处理不同类型集合的实用方法

java 复制代码
// 支持 Map 类型的列表解析
public <K, V> List<Map<K, V>> parseMapList(String content, Class<K> keyType, Class<V> valueType) {
    // 由于 Map 本身是泛型,此方法需要更复杂的类型处理,可结合 TypeFactory
    // 实际建议使用 BeanOutputConverter 配合自定义 wrapper 类
    var wrapperType = new ParameterizedTypeReference<List<MapWrapper<K, V>>>() {};
    // ... 类似实现
}

// 或者直接返回 List<Map<String, Object>>,更简单
public List<Map<String, Object>> parseUntypedList(String content) {
    var converter = new BeanOutputConverter<List<Map<String, Object>>>(
        new ParameterizedTypeReference<List<Map<String, Object>>>() {}
    );
    // ...
}

2.3 枚举字段的处理

java 复制代码
public class OrderStatusConverter {
    
    public Order parseOrder(String text) {
        var converter = new BeanOutputConverter<Order>(
            ParameterizedTypeReference.forType(Order.class)
        );
        // 枚举值在 JSON 中可以是字符串或数字,Jackson 默认支持
        return converter.convert(response);
    }

    @Data
    public static class Order {
        private String id;
        private OrderStatus status;  // 枚举
        private PaymentMethod paymentMethod;
    }

    public enum OrderStatus {
        PENDING, PROCESSING, COMPLETED, CANCELLED
    }

    public enum PaymentMethod {
        CREDIT_CARD, PAYPAL, WECHAT_PAY, ALIPAY
    }
}

三、自定义输出解析器

3.1 基于正则的解析器(增强)

支持更复杂的提取和分组映射。

java 复制代码
package com.example.ai.parser;

import org.springframework.ai.converter.OutputConverter;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.function.Function;

/**
 * 正则表达式输出解析器(增强版)
 * 支持多匹配、分组提取和批量转换
 */
public class RegexOutputConverter<T> implements OutputConverter<T> {

    private final Pattern pattern;
    private final Function<MatchResult, T> mapper;
    private final boolean multipleMatches;  // 是否提取所有匹配

    public RegexOutputConverter(String regex, Function<MatchResult, T> mapper) {
        this(regex, mapper, false);
    }

    public RegexOutputConverter(String regex, Function<MatchResult, T> mapper, boolean multipleMatches) {
        this.pattern = Pattern.compile(regex);
        this.mapper = mapper;
        this.multipleMatches = multipleMatches;
    }

    @Override
    public T convert(String text) {
        Matcher matcher = pattern.matcher(text);
        if (multipleMatches) {
            List<Object> results = new ArrayList<>();
            while (matcher.find()) {
                results.add(mapper.apply(matcher.toMatchResult()));
            }
            @SuppressWarnings("unchecked")
            T result = (T) results;
            return result;
        } else {
            if (matcher.find()) {
                return mapper.apply(matcher.toMatchResult());
            }
            throw new IllegalArgumentException("未匹配到任何内容,输入: " + text);
        }
    }

    @Override
    public String getFormat() {
        return "匹配正则表达式: " + pattern.pattern();
    }

    // 预定义工厂方法
    public static RegexOutputConverter<String> emailExtractor() {
        return new RegexOutputConverter<>(
            "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
            MatchResult::group
        );
    }

    public static RegexOutputConverter<List<String>> allEmailExtractor() {
        return new RegexOutputConverter<>(
            "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
            MatchResult::group,
            true  // 提取所有
        );
    }

    public static RegexOutputConverter<Double> priceExtractor(String currencySymbol) {
        String regex = Pattern.quote(currencySymbol) + "?(\\d+(\\.\\d{1,2})?)";
        return new RegexOutputConverter<>(
            regex,
            m -> Double.parseDouble(m.group(1))
        );
    }

    // 提取键值对
    public static RegexOutputConverter<Map<String, String>> keyValueExtractor() {
        return new RegexOutputConverter<>(
            "(\\w+)\\s*[:=]\\s*([^,\\n]+)",
            m -> {
                Map<String, String> map = new LinkedHashMap<>();
                map.put(m.group(1), m.group(2).trim());
                return map;
            },
            true
        ) {
            @Override
            public Map<String, String> convert(String text) {
                // 覆盖以合并多个键值对
                List<Map<String, String>> maps = super.convert(text);
                Map<String, String> merged = new LinkedHashMap<>();
                for (Map<String, String> map : maps) {
                    merged.putAll(map);
                }
                return merged;
            }
        };
    }
}

3.2 XML 输出解析器(增强)

使用 XPath 简化提取,并支持错误恢复。

java 复制代码
package com.example.ai.parser;

import org.springframework.ai.converter.OutputConverter;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import javax.xml.xpath.*;
import java.io.StringReader;
import org.xml.sax.InputSource;

/**
 * 基于 XPath 的 XML 解析器(增强版)
 */
public class XPathXmlOutputConverter<T> implements OutputConverter<T> {

    private final String xpathExpression;
    private final Function<String, T> valueMapper;
    private final String namespaceAware;

    public XPathXmlOutputConverter(String xpathExpression, Function<String, T> valueMapper) {
        this.xpathExpression = xpathExpression;
        this.valueMapper = valueMapper;
    }

    @Override
    public T convert(String text) {
        try {
            // 提取 XML 内容(自动去除 Markdown 代码块等)
            String xml = extractXmlContent(text);
            
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(false);
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document doc = builder.parse(new InputSource(new StringReader(xml)));
            
            XPathFactory xPathFactory = XPathFactory.newInstance();
            XPath xpath = xPathFactory.newXPath();
            
            XPathExpression expr = xpath.compile(xpathExpression);
            String result = expr.evaluate(doc);
            
            return valueMapper.apply(result);
        } catch (Exception e) {
            throw new RuntimeException("XML/XPath 解析失败", e);
        }
    }

    private String extractXmlContent(String text) {
        // 移除 ```xml ... ```标记
        String cleaned = text.replaceAll("(?s)```xml\\s*", "")
                             .replaceAll("```", "")
                             .trim();
        // 如果还包含其他非 XML 内容,尝试找到第一个 < 和最后一个 >
        int start = cleaned.indexOf('<');
        int end = cleaned.lastIndexOf('>');
        if (start != -1 && end != -1 && end > start) {
            return cleaned.substring(start, end + 1);
        }
        return cleaned;
    }

    @Override
    public String getFormat() {
        return "返回 XML 格式,使用 XPath: " + xpathExpression;
    }

    // 工厂方法
    public static XPathXmlOutputConverter<String> stringExtractor(String xpath) {
        return new XPathXmlOutputConverter<>(xpath, Function.identity());
    }

    public static XPathXmlOutputConverter<Double> doubleExtractor(String xpath) {
        return new XPathXmlOutputConverter<>(xpath, Double::parseDouble);
    }
}

3.3 CSV/TSV 解析器

java 复制代码
public class CsvOutputConverter<T> implements OutputConverter<List<T>> {

    private final char delimiter;
    private final Function<Map<String, String>, T> rowMapper;
    private final List<String> headers;

    public CsvOutputConverter(char delimiter, 
                              List<String> headers,
                              Function<Map<String, String>, T> rowMapper) {
        this.delimiter = delimiter;
        this.headers = headers;
        this.rowMapper = rowMapper;
    }

    @Override
    public List<T> convert(String text) {
        String[] lines = text.split("\\r?\\n");
        List<T> results = new ArrayList<>();
        
        // 跳过表头行(如果有)
        int startIndex = 0;
        if (headers != null && !headers.isEmpty()) {
            // 假设第一行是表头,跳过
            startIndex = 1;
        }

        for (int i = startIndex; i < lines.length; i++) {
            String line = lines[i].trim();
            if (line.isEmpty()) continue;
            
            String[] fields = line.split(Pattern.quote(String.valueOf(delimiter)));
            Map<String, String> rowData = new LinkedHashMap<>();
            for (int j = 0; j < fields.length && j < headers.size(); j++) {
                rowData.put(headers.get(j), fields[j]);
            }
            results.add(rowMapper.apply(rowData));
        }
        return results;
    }

    @Override
    public String getFormat() {
        return "CSV 格式,分隔符: " + delimiter + ",表头: " + headers;
    }
}

四、带验证的容错解析器(增强)

4.1 综合验证器(支持 JSR-303 校验)

java 复制代码
package com.example.ai.parser;

import jakarta.validation.*;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.stereotype.Component;

import jakarta.validation.ConstraintViolation;
import java.util.Set;
import java.util.function.Function;

/**
 * 带 JSR-303 验证的输出解析器
 */
@Component
public class ValidatingOutputParser<T> {

    private final Validator validator;

    public ValidatingOutputParser() {
        try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
            this.validator = factory.getValidator();
        }
    }

    /**
     * 解析并执行 Bean 验证
     */
    public T parseAndValidate(BeanOutputConverter<T> converter,
                              String response,
                              Function<String, T> fallback) {
        try {
            T obj = converter.convert(response);
            Set<ConstraintViolation<T>> violations = validator.validate(obj);
            if (violations.isEmpty()) {
                return obj;
            } else {
                // 收集错误信息
                String errors = violations.stream()
                    .map(v -> v.getPropertyPath() + " " + v.getMessage())
                    .collect(Collectors.joining(", "));
                throw new IllegalArgumentException("验证失败: " + errors);
            }
        } catch (Exception e) {
            // 如果提供了 fallback,则调用
            if (fallback != null) {
                return fallback.apply(response);
            }
            throw e;
        }
    }

    /**
     * 带重试的版本
     */
    public T parseWithRetry(BeanOutputConverter<T> converter,
                            String initialPrompt,
                            Function<String, String> llmCaller,
                            int maxRetries) {
        int attempts = 0;
        String currentPrompt = initialPrompt;
        while (attempts < maxRetries) {
            String response = llmCaller.apply(currentPrompt);
            try {
                T obj = converter.convert(response);
                Set<ConstraintViolation<T>> violations = validator.validate(obj);
                if (violations.isEmpty()) {
                    return obj;
                }
                // 验证失败,追加反馈
                currentPrompt = initialPrompt + "\n\n之前输出不符合要求: " + response +
                    "\n错误: " + violations.stream()
                        .map(v -> v.getPropertyPath() + " " + v.getMessage())
                        .collect(Collectors.joining(", ")) +
                    "\n请修正后重新输出。";
                attempts++;
            } catch (Exception e) {
                currentPrompt = initialPrompt + "\n\n解析失败,错误: " + e.getMessage() +
                    ",请重新输出有效的 JSON。";
                attempts++;
            }
        }
        throw new RuntimeException("达到最大重试次数,解析失败");
    }
}

4.2 使用带注解的 POJO

java 复制代码
import jakarta.validation.constraints.*;

@Data
public class OrderInfo {
    @NotNull(message = "订单ID不能为空")
    private String orderId;

    @NotBlank(message = "客户姓名不能为空")
    @Size(max = 50, message = "客户姓名长度不超过50")
    private String customerName;

    @NotNull(message = "总金额不能为空")
    @Positive(message = "总金额必须为正数")
    private Double totalAmount;

    @NotEmpty(message = "订单项不能为空")
    @Valid  // 级联验证
    private List<OrderItem> items;

    @Min(value = 1, message = "至少1个订单项")
    private int itemCount;
}

@Data
public class OrderItem {
    @NotBlank(message = "产品名称不能为空")
    private String productName;

    @Min(value = 1, message = "数量至少为1")
    private int quantity;

    @Positive(message = "价格必须为正")
    private double price;
}

五、流式输出解析(高级)

当 LLM 流式返回时,输出是分块的,需要特殊的增量解析策略。

5.1 流式 JSON 解析器

java 复制代码
package com.example.ai.parser;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Flux;

import java.util.function.Consumer;

/**
 * 流式 JSON 解析器
 * 从 Flux<String> 中增量提取完整的 JSON 对象
 */
public class StreamingJsonParser<T> {

    private final ObjectMapper mapper = new ObjectMapper();
    private final Class<T> targetClass;
    private final StringBuilder buffer = new StringBuilder();

    public StreamingJsonParser(Class<T> targetClass) {
        this.targetClass = targetClass;
    }

    /**
     * 处理流式内容,当检测到完整的 JSON 对象时触发回调
     */
    public void parseStreaming(Flux<String> chunks, Consumer<T> onComplete) {
        chunks.subscribe(
            chunk -> {
                buffer.append(chunk);
                // 尝试解析当前缓冲区
                tryExtractJson(onComplete);
            },
            error -> {
                // 错误处理
            },
            () -> {
                // 完成时尝试最后一次解析
                tryExtractJson(onComplete);
            }
        );
    }

    private void tryExtractJson(Consumer<T> consumer) {
        String content = buffer.toString();
        // 简单检测:寻找最外层 '{' 和 '}'
        int start = content.indexOf('{');
        int depth = 0;
        int end = -1;
        for (int i = start; i < content.length(); i++) {
            char c = content.charAt(i);
            if (c == '{') depth++;
            else if (c == '}') {
                depth--;
                if (depth == 0) {
                    end = i;
                    break;
                }
            }
        }
        if (start != -1 && end != -1 && end > start) {
            String json = content.substring(start, end + 1);
            try {
                T obj = mapper.readValue(json, targetClass);
                consumer.accept(obj);
                // 移除已处理的部分,保留后续内容
                buffer.delete(0, end + 1);
            } catch (Exception e) {
                // JSON 不完整,保留缓冲区继续等待
            }
        }
    }

    /**
     * 返回解析结果 Flux
     */
    public Flux<T> parseToFlux(Flux<String> chunks) {
        return chunks
            .scan("", String::concat)  // 累积字符串
            .map(this::tryExtractFullJson)
            .filter(Optional::isPresent)
            .map(Optional::get);
    }

    private Optional<T> tryExtractFullJson(String accumulated) {
        // 类似上述逻辑,返回 Optional
        // 实现略
        return Optional.empty();
    }
}

5.2 使用流式解析器与 ChatClient

java 复制代码
@Service
public class StreamingParsingService {

    private final ChatClient chatClient;

    public StreamingParsingService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    /**
     * 流式输出并实时解析成对象
     */
    public Flux<OrderInfo> streamAndParseOrders(String userPrompt) {
        return chatClient.prompt()
            .user(userPrompt)
            .stream()
            .content()
            .transform(new StreamingJsonParser<>(OrderInfo.class)::parseToFlux);
    }
}

六、性能与最佳实践

6.1 解析器性能对比

解析器类型 平均耗时 (μs) 内存占用 适用场景
BeanOutputConverter ~50-100 标准 POJO
MapOutputConverter ~30-60 动态结构
正则解析器 ~10-30 极低 简单模式提取
XML 解析器 ~100-200 XML 格式
自定义流式解析器 取决于分块 长输出、实时展示

6.2 解析器缓存策略

对于相同的 JSON Schema,可以缓存 BeanOutputConverter 的格式字符串和反序列化配置,避免重复创建。

java 复制代码
@Component
public class ParserCache {
    private final Map<Class<?>, BeanOutputConverter<?>> converterCache = new ConcurrentHashMap<>();

    @SuppressWarnings("unchecked")
    public <T> BeanOutputConverter<T> getConverter(Class<T> clazz) {
        return (BeanOutputConverter<T>) converterCache.computeIfAbsent(clazz,
            k -> new BeanOutputConverter<>(ParameterizedTypeReference.forType(k))
        );
    }
}

6.3 错误恢复策略

  • 重试机制:最多重试 2-3 次,每次增加更具体的提示。
  • 默认值回退:为每个字段提供合理的默认值,避免完全失败。
  • 人工干预:对关键业务,记录失败日志,支持人工修正。

6.4 测试建议

java 复制代码
@SpringBootTest
class OutputParserTest {
    
    @Autowired
    private NestedParserDemo parser;

    @Test
    void testParseCompany() {
        String input = "TechCorp is a software company headquartered in Beijing...";
        CompanyInfo info = parser.parseCompany(input);
        assertNotNull(info);
        assertEquals("TechCorp", info.getName());
        assertNotNull(info.getHeadquarters());
        assertEquals("Beijing", info.getHeadquarters().getCity());
    }

    @Test
    void testInvalidJsonFallback() {
        // 模拟 LLM 返回错误 JSON,测试容错逻辑
        String badInput = "```json\n{\"name\":\"TechCorp\",\"industry\":\"Software\"\n```";
        CompanyInfo info = parser.parseCompany(badInput); // 应能处理或抛明确异常
        // 验证异常或默认值
    }
}

七、总结

解析器类型 适用场景 复杂度 推荐度
BeanOutputConverter 结构化 POJO 转换(最常用) ⭐⭐⭐⭐⭐
MapOutputConverter 动态键值对,无需预定义类 ⭐⭐⭐
正则解析器 从非结构化文本中提取特定模式 ⭐⭐⭐⭐
XML 解析器 遗留系统接口或 XML 格式输出 ⭐⭐⭐
流式解析器 实时展示流式输出中的结构化数据 ⭐⭐⭐⭐
带验证解析器 需要保证数据完整性和正确性 中高 ⭐⭐⭐⭐⭐

最佳实践清单

  1. 优先使用 BeanOutputConverter:类型安全,易于维护。
  2. 使用 ParameterizedTypeReference:正确处理泛型。
  3. 为 POJO 添加验证注解:利用 JSR-303 在解析后立即校验。
  4. 实现容错重试:网络或 LLM 不稳定时提供重试机制。
  5. 缓存 Converter 实例:减少重复创建开销。
  6. 统一异常处理 :定义清晰的异常层次(如 ParsingException)。
  7. 测试覆盖:为每种解析器编写单元测试,包括异常场景。
  8. 考虑流式场景:对于长响应,使用流式解析提升用户体验。
  9. 记录解析失败日志:便于问题定位和监控。
  10. 版本管理 :POJO 结构变更时,考虑前向兼容性(使用 @JsonIgnoreProperties(ignoreUnknown = true))。

参考资源:

相关推荐
天天代码码天天44 分钟前
纯 C OCR 又补齐 Java 生态了!lw.PPOCR.C v0.1.0-preview.7 发布
人工智能
实验室管理云平台1 小时前
LIMS 系统常见问题(技术向):从数据采集到系统集成,开发与运维视角的踩坑记录
人工智能
程序员-李俞1 小时前
RelayRouter大模型 API 接入实践:用统一路由降低多模型调用成本与维护复杂度
人工智能
Agudamu11611 小时前
AI 视频学习笔记工具拆解:4款工具谁能把视频变成复习资料
人工智能·学习·音视频
2601_967659911 小时前
2026年江西GEO系统贴牌服务商怎么选才稳妥
人工智能
外域速览1 小时前
2026世界动力电池大会今日宜宾启幕:固态电池从实验室走向量产,IEC 国际标准终结概念乱象
大数据·人工智能·microsoft
金智维科技官方2 小时前
财务核算如何提效?这三条核心链路正在加速自动化
运维·人工智能·ai·自动化·财务·智能体
Csvn2 小时前
第 16 章 多智能体 Multi-Agent
人工智能·aigc·agent
xsd202411182 小时前
从BEV感知透视3D检测
人工智能