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 格式输出 | 中 | ⭐⭐⭐ |
| 流式解析器 | 实时展示流式输出中的结构化数据 | 高 | ⭐⭐⭐⭐ |
| 带验证解析器 | 需要保证数据完整性和正确性 | 中高 | ⭐⭐⭐⭐⭐ |
最佳实践清单
- 优先使用
BeanOutputConverter:类型安全,易于维护。 - 使用
ParameterizedTypeReference:正确处理泛型。 - 为 POJO 添加验证注解:利用 JSR-303 在解析后立即校验。
- 实现容错重试:网络或 LLM 不稳定时提供重试机制。
- 缓存 Converter 实例:减少重复创建开销。
- 统一异常处理 :定义清晰的异常层次(如
ParsingException)。 - 测试覆盖:为每种解析器编写单元测试,包括异常场景。
- 考虑流式场景:对于长响应,使用流式解析提升用户体验。
- 记录解析失败日志:便于问题定位和监控。
- 版本管理 :POJO 结构变更时,考虑前向兼容性(使用
@JsonIgnoreProperties(ignoreUnknown = true))。
参考资源: