跳到主要内容

结构化输出

结构化输出是将 AI 模型的响应转换为 Java 对象的关键功能。本章介绍 Spring AI 的结构化输出机制。

概述

传统 AI 模型返回字符串,但应用通常需要结构化数据:

┌─────────────────────────────────────────────────────────────┐
│ 结构化输出流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户请求 ───> AI 模型 ───> JSON 字符串 │
│ │ │
│ ▼ │
│ 输出解析器 │
│ │ │
│ ▼ │
│ Java 对象 (POJO) │
│ │
└─────────────────────────────────────────────────────────────┘

基本使用

输出为简单对象

record Person(String name, int age, String occupation) {}

@GetMapping("/person")
public Person getPerson() {
return chatClient.prompt()
.user("生成一个随机的人物信息")
.call()
.entity(Person.class);
}

输出为 List

record Book(String title, String author, int year) {}

@GetMapping("/books")
public List<Book> getBooks(@RequestParam String topic) {
return chatClient.prompt()
.user("推荐5本关于" + topic + "的经典书籍")
.call()
.entity(new ParameterizedTypeReference<List<Book>>() {});
}

输出为 Map

@GetMapping("/analyze")
public Map<String, Object> analyzeText(@RequestParam String text) {
return chatClient.prompt()
.user("分析以下文本的情感和主题:" + text)
.call()
.entity(new ParameterizedTypeReference<Map<String, Object>>() {});
}

复杂对象结构

嵌套对象

record Address(String city, String street, String zipCode) {}
record Company(String name, Address address, List<String> departments) {}
record Employee(String name, String position, Company company, double salary) {}

@GetMapping("/employee")
public Employee generateEmployee() {
return chatClient.prompt()
.user("生成一个完整的员工信息,包括所属公司")
.call()
.entity(Employee.class);
}

枚举类型

enum Priority { HIGH, MEDIUM, LOW }
enum Status { PENDING, IN_PROGRESS, COMPLETED }

record Task(
String id,
String description,
Priority priority,
Status status,
LocalDateTime deadline
) {}

@GetMapping("/task")
public Task generateTask(@RequestParam String description) {
return chatClient.prompt()
.user("根据描述生成任务:" + description)
.call()
.entity(Task.class);
}

BeanOutputConverter

使用输出转换器

@GetMapping("/convert")
public List<Product> convertProducts() {
// 定义输出结构
record Product(String name, double price, String category) {}

// 创建转换器
BeanOutputConverter<List<Product>> converter =
new BeanOutputConverter<>(new ParameterizedTypeReference<List<Product>>() {});

// 生成提示词
String format = converter.getFormat();
String prompt = """
生成5个电子产品的信息。

{format}
""";

// 获取响应
String response = chatClient.prompt()
.user(u -> u.text(prompt).param("format", format))
.call()
.content();

// 转换为对象
return converter.parse(response);
}

自定义格式指令

BeanOutputConverter<Person> converter = new BeanOutputConverter<>(Person.class);

// 获取格式指令
String formatInstructions = converter.getFormat();
System.out.println(formatInstructions);
// 输出:Your response should be in JSON format...

输出验证

使用验证注解

record User(
@NotBlank String name,
@Email String email,
@Min(0) @Max(150) int age,
@Pattern(regexp = "^1[3-9]\\d{9}$") String phone
) {}

@PostMapping("/user")
public ResponseEntity<?> createUser(@RequestParam String description) {
try {
User user = chatClient.prompt()
.user("根据描述生成用户信息:" + description)
.call()
.entity(User.class);
return ResponseEntity.ok(user);
} catch (Exception e) {
return ResponseEntity.badRequest().body("无法生成有效用户信息");
}
}

自定义验证

@Service
public class OutputService {

public <T> T getValidOutput(String prompt, Class<T> type, Predicate<T> validator) {
int maxAttempts = 3;

for (int i = 0; i < maxAttempts; i++) {
T result = chatClient.prompt()
.user(prompt)
.call()
.entity(type);

if (validator.test(result)) {
return result;
}
}

throw new RuntimeException("无法生成符合要求的输出");
}
}

流式结构化输出

累积流式响应后解析

@GetMapping(value = "/stream-entity", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamEntity(@RequestParam String query) {
AtomicReference<StringBuilder> content = new AtomicReference<>(new StringBuilder());

return chatClient.prompt()
.user(query + " 请以JSON格式输出")
.stream()
.content()
.doOnNext(chunk -> content.get().append(chunk))
.doOnComplete(() -> {
// 流完成后解析完整JSON
String json = content.get().toString();
try {
MyEntity entity = objectMapper.readValue(json, MyEntity.class);
// 处理解析后的对象
} catch (Exception e) {
// 处理解析错误
}
});
}

实际应用示例

1. 文本信息提取

record NewsExtraction(
String title,
String author,
String publishDate,
List<String> keywords,
String summary,
String sentiment
) {}

@PostMapping("/extract-news")
public NewsExtraction extractNews(@RequestBody String newsText) {
return chatClient.prompt()
.system("""
你是一个新闻分析专家。请从新闻文本中提取以下信息:
- 标题
- 作者
- 发布日期
- 关键词(最多5个)
- 摘要(100字以内)
- 情感倾向(正面/负面/中性)
""")
.user("请分析以下新闻:\n" + newsText)
.call()
.entity(NewsExtraction.class);
}

2. 代码分析

record CodeAnalysis(
String language,
String purpose,
int timeComplexity,
int spaceComplexity,
List<String> issues,
List<String> suggestions,
String improvedCode
) {}

@PostMapping("/analyze-code")
public CodeAnalysis analyzeCode(@RequestBody String code) {
return chatClient.prompt()
.system("""
你是一个代码分析专家。请分析代码并提供:
1. 编程语言
2. 代码功能
3. 时间复杂度(仅数字,如O(n)写为n)
4. 空间复杂度
5. 发现的问题
6. 改进建议
7. 改进后的代码
""")
.user("请分析以下代码:\n```\n" + code + "\n```")
.call()
.entity(CodeAnalysis.class);
}

3. 数据转换

record UserDTO(String name, String email, String phone) {}
record UserEntity(String fullName, String emailAddress, String phoneNumber) {}

@PostMapping("/transform")
public UserEntity transformUser(@RequestBody UserDTO dto) {
return chatClient.prompt()
.system("""
你是一个数据转换专家。请将输入的用户数据转换为标准格式:
- 姓名转为首字母大写
- 邮箱转为小写
- 电话号码转为标准格式
""")
.user("转换以下用户数据:" + dto)
.call()
.entity(UserEntity.class);
}

4. 问答对生成

record QAPair(String question, String answer, List<String> tags) {}

@GetMapping("/qa-pairs")
public List<QAPair> generateQAPairs(@RequestParam String topic, @RequestParam int count) {
return chatClient.prompt()
.user(String.format("生成%d个关于'%s'的问答对", count, topic))
.call()
.entity(new ParameterizedTypeReference<List<QAPair>>() {});
}

最佳实践

1. 明确输出格式

// 在提示词中明确指定格式
String prompt = """
请生成产品信息。

要求:
- name: 产品名称,不超过50字符
- price: 价格,正数
- category: 分类,从 [电子产品, 服装, 食品, 书籍] 中选择
- stock: 库存,非负整数
""";

2. 处理解析失败

@GetMapping("/safe-parse")
public ResponseEntity<?> safeParse(@RequestParam String query) {
try {
Product product = chatClient.prompt()
.user(query)
.call()
.entity(Product.class);
return ResponseEntity.ok(product);
} catch (JsonProcessingException e) {
// 解析失败,返回原始响应
String rawResponse = chatClient.prompt()
.user(query)
.call()
.content();
return ResponseEntity.ok(Map.of(
"raw", rawResponse,
"error", "无法解析为结构化数据"
));
}
}

3. 添加默认值

record Config(
String name,
@JsonProperty(defaultValue = "default-value") String value,
boolean enabled
) {}

小结

本章我们学习了:

  1. 基本用法:将 AI 响应转换为 POJO
  2. 复杂结构:嵌套对象、枚举、集合
  3. 输出转换器:BeanOutputConverter 的使用
  4. 输出验证:确保生成数据的正确性
  5. 实际应用:信息提取、代码分析、数据转换

练习

  1. 实现一个简历解析器,将简历文本转换为结构化对象
  2. 创建一个产品描述生成器,输出包含多个字段的产品信息
  3. 实现一个智能表单填充功能

下一步