结构化输出实现投诉客服和地址自动解析
一、概述
本文档基于 TestStructuredOut.java 代码文件,总结了 Spring AI 中结构化输出的实现方式,包括:
- 投诉客服判断:使用布尔值结构化输出判断用户是否有投诉意图
- 地址自动解析:使用 Record 类型结构化输出自动解析收货地址信息
- 底层实现:使用 BeanOutputConverter 实现自定义结构化输出
二、核心概念
2.1 结构化输出(Structured Output)
结构化输出是 Spring AI 提供的一种功能,允许将 AI 模型的文本回复自动转换为 Java 对象。
这样可以:
- 类型安全:直接获得强类型的 Java 对象
- 易于处理:无需手动解析 JSON 或文本
- 业务集成:直接用于业务逻辑处理
2.2 实现方式
Spring AI 提供了两种方式实现结构化输出:
- 高级 API :使用
ChatClient.entity()方法(推荐) - 底层 API :使用
BeanOutputConverter手动转换
三、功能实现详解
3.1 投诉客服判断(testBoolOut)
功能说明
判断用户输入是否包含投诉意图,返回布尔值,用于路由到不同的客服处理流程。
代码实现
java
@Test
public void testBoolOut() {
Boolean isComplain = chatClient
.prompt()
.system("""
请判断用户信息是否表达了投诉意图?
只能用 true 或 false 回答,不要输出多余内容
""")
.user("你好!")
.call()
.entity(Boolean.class);
// 分支逻辑
if (Boolean.TRUE.equals(isComplain)) {
System.out.println("用户是投诉,转接人工客服!");
} else {
System.out.println("用户不是投诉,自动流转客服机器人。");
// todo 继续调用 客服ChatClient进行对话
}
}
实现原理
-
提示词设计:
- 使用 system 提示词明确要求 AI 只返回 true 或 false
- 要求不要输出多余内容,确保返回的是纯布尔值
-
结构化输出:
- 使用
.entity(Boolean.class)将 AI 回复转换为 Boolean 对象 - Spring AI 会自动解析 AI 返回的文本,提取布尔值
- 使用
-
业务逻辑:
- 根据返回的布尔值进行分支处理
- true:转接人工客服
- false:继续使用客服机器人
使用场景
- 智能客服路由:自动判断用户意图,分流到不同处理流程
- 情感分析:判断用户情绪(正面/负面)
- 二分类任务:任何需要是/否判断的场景
注意事项
- 提示词要明确要求只返回布尔值
- 建议在提示词中强调"不要输出多余内容"
- 对于边界情况,AI 可能返回其他值,需要做好异常处理
3.2 地址自动解析(testEntityOut)
功能说明
从用户输入的文本中自动提取收货地址信息,包括姓名、电话、省市区和详细地址。
代码实现
java
// 定义地址 Record
public record Address(
String name, // 收件人姓名
String phone, // 联系电话
String province, // 省
String city, // 市
String district, // 区/县
String detail // 详细地址
) {}
@Test
public void testEntityOut() {
Address address = chatClient.prompt()
.system("""
请从下面这条文本中提取收货信息,
""")
.user("收货人:张三,电话13588888888,地址:浙江省杭州市西湖区文一西路100号8幢202室")
.call()
.entity(Address.class);
System.out.println(address);
}
实现原理
-
定义数据结构:
- 使用 Java Record 定义地址结构
- Record 是 Java 14+ 的特性,适合定义不可变的数据传输对象
- 包含所有需要提取的字段:姓名、电话、省、市、区、详细地址
-
提示词设计:
- 使用 system 提示词说明任务:从文本中提取收货信息
- AI 会根据 Address 类的字段结构自动提取对应信息
-
结构化输出:
- 使用
.entity(Address.class)将 AI 回复转换为 Address 对象 - Spring AI 会自动:
- 生成 JSON Schema 描述 Address 结构
- 要求 AI 按照 Schema 返回 JSON
- 将 JSON 反序列化为 Address 对象
- 使用
-
自动映射:
- AI 会智能识别文本中的各个字段
- 自动映射到对应的 Java 字段
- 例如:"浙江省" → province,"杭州市" → city
使用场景
- 电商订单:自动解析用户输入的收货地址
- 物流系统:从文本中提取地址信息
- 表单填写:智能填充地址表单
- 数据清洗:从非结构化文本中提取结构化数据
优势
- 智能识别:AI 能够理解各种地址格式
- 容错性强:即使格式不规范,也能提取关键信息
- 类型安全:直接获得强类型对象,无需手动解析
注意事项
- Record 字段名要清晰,便于 AI 理解
- 提示词可以更详细,说明提取规则
- 对于复杂地址,可能需要更精确的提示词
3.3 底层实现(testLowEntityOut)
功能说明
演示如何使用 BeanOutputConverter 手动实现结构化输出,这是 ChatClient.entity() 的底层实现方式。
代码实现
java
public record ActorsFilms(
String actor,
String film1,
String film2,
String film3,
String film4,
String film5
) {}
@Test
public void testLowEntityOut(@Autowired DashScopeChatModel chatModel) {
// 1. 创建 BeanOutputConverter
BeanOutputConverter<ActorsFilms> beanOutputConverter =
new BeanOutputConverter<>(ActorsFilms.class);
// 2. 获取格式说明(JSON Schema)
String format = beanOutputConverter.getFormat();
String actor = "周星驰";
// 3. 构建提示词,包含格式说明
String template = """
提供5部{actor}导演的电影.
{format}
""";
PromptTemplate promptTemplate = PromptTemplate.builder()
.template(template)
.variables(Map.of("actor", actor, "format", format))
.build();
// 4. 调用 ChatModel
ChatResponse response = chatModel.call(promptTemplate.create());
// 5. 手动转换 AI 回复为对象
ActorsFilms actorsFilms = beanOutputConverter.convert(
response.getResult().getOutput().getText()
);
System.out.println(actorsFilms);
}
实现原理
-
BeanOutputConverter:
- 用于将 AI 的文本回复转换为 Java Bean
- 内部使用 JSON Schema 描述目标对象结构
- 支持将 JSON 字符串转换为 Java 对象
-
getFormat() 方法:
- 返回 JSON Schema 格式的字符串
- 描述目标对象的所有字段和类型
- 这个格式会被添加到提示词中,指导 AI 返回正确的 JSON
-
转换流程:
- AI 返回 JSON 格式的文本
- BeanOutputConverter.convert() 将 JSON 解析为 Java 对象
- 使用 Jackson 或其他 JSON 库进行反序列化
与高级 API 的对比
| 特性 | ChatClient.entity() | BeanOutputConverter |
|---|---|---|
| 使用方式 | 简单,一行代码 | 需要多步操作 |
| 灵活性 | 较低 | 较高,可以自定义 |
| 适用场景 | 大多数场景 | 需要精细控制的场景 |
| 代码量 | 少 | 多 |
使用场景
- 需要自定义提示词格式
- 需要处理复杂的转换逻辑
- 需要与现有的 PromptTemplate 集成
- 需要更精细的控制
四、技术要点
4.1 Record 类型的使用
Java Record 是定义结构化输出的理想选择:
- 简洁:自动生成构造函数、getter、equals、hashCode、toString
- 不可变:所有字段都是 final,保证数据安全
- 类型安全:编译时检查类型
4.2 提示词设计原则
- 明确性:清楚说明需要提取什么信息
- 格式要求:明确要求返回格式(JSON、布尔值等)
- 示例:可以提供示例帮助 AI 理解
- 约束:说明不要输出多余内容
4.3 错误处理
结构化输出可能失败的情况:
- AI 返回的格式不符合预期
- 字段缺失或类型不匹配
- 网络或 API 调用失败
建议:
- 使用 try-catch 捕获异常
- 对返回的对象进行空值检查
- 提供默认值或降级方案
五、实际应用场景
5.1 智能客服系统
java
// 1. 判断用户意图
Boolean isComplain = chatClient.prompt()
.system("判断用户是否有投诉意图,只返回 true 或 false")
.user(userMessage)
.call()
.entity(Boolean.class);
// 2. 根据意图路由
if (Boolean.TRUE.equals(isComplain)) {
// 转接人工客服
transferToHumanAgent(userMessage);
} else {
// 继续机器人对话
continueWithBot(userMessage);
}
5.2 地址解析系统
java
// 从用户输入提取地址
Address address = chatClient.prompt()
.system("从文本中提取收货地址信息")
.user("收货人:张三,电话13588888888,地址:浙江省杭州市西湖区文一西路100号")
.call()
.entity(Address.class);
// 验证地址
if (isValidAddress(address)) {
// 保存到数据库
saveAddress(address);
} else {
// 提示用户补充信息
askForMoreInfo();
}
5.3 数据提取系统
可以提取各种结构化信息:
- 订单信息(订单号、商品、金额等)
- 个人信息(姓名、身份证、联系方式等)
- 事件信息(时间、地点、人物等)
六、最佳实践
6.1 Record 设计
java
// 好的设计:字段清晰,类型明确
public record OrderInfo(
String orderId,
List<String> products,
BigDecimal amount,
LocalDateTime orderTime
) {}
// 避免:字段名不清晰
public record Data(
String f1, // 不清晰
String f2 // 不清晰
) {}
6.2 提示词优化
java
// 好的提示词:明确、具体
.system("""
请从用户输入中提取订单信息。
必须包含以下字段:
- orderId: 订单号
- products: 商品列表
- amount: 订单金额
只返回 JSON 格式,不要输出其他内容。
""")
// 避免:过于简单
.system("提取订单信息")
6.3 错误处理
java
try {
Address address = chatClient.prompt()
.user(userInput)
.call()
.entity(Address.class);
// 验证必填字段
if (address.name() == null || address.phone() == null) {
throw new IllegalArgumentException("地址信息不完整");
}
return address;
} catch (Exception e) {
// 降级处理:返回默认值或提示用户
log.error("地址解析失败", e);
return getDefaultAddress();
}
七、扩展应用
7.1 多级结构化输出
可以嵌套使用 Record:
java
public record Person(
String name,
Contact contact,
Address address
) {}
public record Contact(
String phone,
String email
) {}
public record Address(
String province,
String city,
String detail
) {}
7.2 列表结构化输出
java
public record MovieList(
List<Movie> movies
) {}
public record Movie(
String title,
Integer year,
String director
) {}
7.3 条件结构化输出
根据不同的输入,返回不同的结构:
java
// 先判断类型
String type = chatClient.prompt()
.user(input)
.call()
.entity(String.class); // "order" 或 "complaint"
// 根据类型提取不同结构
if ("order".equals(type)) {
OrderInfo order = chatClient.prompt()
.user(input)
.call()
.entity(OrderInfo.class);
} else {
ComplaintInfo complaint = chatClient.prompt()
.user(input)
.call()
.entity(ComplaintInfo.class);
}
八、性能考虑
8.1 缓存策略
对于相同的输入,可以缓存结构化输出结果:
java
@Cacheable("addressCache")
public Address parseAddress(String text) {
return chatClient.prompt()
.user(text)
.call()
.entity(Address.class);
}
8.2 批量处理
对于大量数据,考虑批量处理:
java
List<Address> addresses = texts.stream()
.map(text -> chatClient.prompt()
.user(text)
.call()
.entity(Address.class))
.toList();
九、总结
结构化输出是 Spring AI 提供的强大功能,可以:
- 简化开发:无需手动解析 JSON 或文本
- 类型安全:直接获得强类型对象
- 智能提取:AI 能够理解各种格式的输入
- 易于集成:直接用于业务逻辑处理
适用场景:
- 智能客服路由
- 信息提取(地址、订单、个人信息等)
- 数据清洗和标准化
- 表单自动填充
注意事项:
- 设计清晰的 Record 结构
- 编写明确的提示词
- 做好错误处理和验证
- 考虑性能和缓存
原理

java
public record ActorsFilms(
String actor,
String film1,
String film2,
String film3,
String film4,
String film5
) {}
@Test
public void testLowEntityOut(@Autowired DashScopeChatModel chatModel) {
BeanOutputConverter<ActorsFilms> beanOutputConverter = new BeanOutputConverter<>(ActorsFilms.class);
String format = beanOutputConverter.getFormat();
String actor = "周星驰";
String template = """
提供5部{actor}导演的电影.
{format}
""";
PromptTemplate promptTemplate = PromptTemplate.builder().template(template).variables(Map.of("actor", actor, "format", format)).build();
ChatResponse response = chatModel.call(
promptTemplate.create()
);
ActorsFilms actorsFilms = beanOutputConverter.convert(response.getResult().getOutput().getText());
System.out.println(actorsFilms);
}


结构化输出agent实战(手动实现tools)
链接多个模型协调工作实战-初代tools:
背景:
大模型如果它无法和企业API互联那将毫无意义!比如我们开发一个智能票务助手,当用户需要退票, 基础大模型它肯定做不到,因为票务信息都存在了我们系统中,必须通过我们系统的业务方法才能进行退票。那怎么能让大模型"调用"我们自己系统的业务方法呢?今天叫大家通过结构化输入连接多个模型一起协同完成这个任务:

项目结构

多模型协作智能客服系统 - 代码详细分析
一、项目结构
com.xushu.springai.chain
├── Application.java # Spring Boot 启动类
├── AiConfig.java # AI 模型配置类
├── AiJob.java # 任务定义类
└── MultiModelsController.java # 多模型协作控制器
二、类详细分析
2.1 Application.java
代码
java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
分析
- 作用:Spring Boot 应用入口
- 功能:启动 Spring Boot 应用,初始化 Spring 容器
- 特点:标准的 Spring Boot 启动类,无特殊配置
2.2 AiConfig.java
代码结构
java
@Configuration
public class AiConfig {
@Bean
public ChatClient planningChatClient(...) { }
@Bean
public ChatClient botChatClient(...) { }
}
详细分析
2.2.1 planningChatClient - 任务规划模型
完整代码:
java
@Bean
public ChatClient planningChatClient(DashScopeChatModel chatModel,
DashScopeChatProperties options,
ChatMemory chatMemory) {
DashScopeChatOptions dashScopeChatOptions = DashScopeChatOptions.fromOptions(options.getOptions());
dashScopeChatOptions.setTemperature(0.4);
return ChatClient.builder(chatModel)
.defaultSystem("""
# 票务助手任务拆分规则
## 1.要求
### 1.1 根据用户内容识别任务
## 2. 任务
### 2.1 JobType:退票(CANCEL) 要求用户提供姓名和预定号, 或者从对话中提取;
### 2.2 JobType:查票(QUERY) 要求用户提供预定号, 或者从对话中提取;
### 2.3 JobType:其他(OTHER)
""")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.defaultOptions(dashScopeChatOptions)
.build();
}
功能分析:
-
Temperature 设置 (0.4)
- 作用:控制 AI 输出的随机性
- 值说明:0.4 是较低的值,表示输出更确定、更一致
- 适用场景:任务识别需要准确性,避免误判
- 对比:如果设置为 1.0+,可能导致同样的输入识别出不同的任务类型
-
System 提示词设计
markdown# 票务助手任务拆分规则 ## 1.要求 ### 1.1 根据用户内容识别任务 ## 2. 任务 ### 2.1 JobType:退票(CANCEL) 要求用户提供姓名和预定号, 或者从对话中提取; ### 2.2 JobType:查票(QUERY) 要求用户提供预定号, 或者从对话中提取; ### 2.3 JobType:其他(OTHER)设计要点:
- 使用 Markdown 格式,结构清晰
- 明确说明三种任务类型
- 说明每种任务需要的信息
- 强调可以从对话中提取信息
-
MessageChatMemoryAdvisor
- 作用:自动管理对话记忆
- 功能:在多轮对话中保持上下文
- 重要性:支持从历史对话中提取信息
工作原理:
用户输入 → planningChatClient
↓
System 提示词指导 AI 识别任务类型
↓
AI 分析用户输入,识别意图
↓
提取关键信息(姓名、订单号等)
↓
返回结构化的 Job 对象
2.2.2 botChatClient - 智能客服模型
完整代码:
java
@Bean
public ChatClient botChatClient(DashScopeChatModel chatModel,
DashScopeChatProperties options,
ChatMemory chatMemory) {
DashScopeChatOptions dashScopeChatOptions = DashScopeChatOptions.fromOptions(options.getOptions());
dashScopeChatOptions.setTemperature(1.2);
return ChatClient.builder(chatModel)
.defaultSystem("""
你是XS航空智能客服代理, 请以友好的语气服务用户。
""")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.defaultOptions(dashScopeChatOptions)
.build();
}
功能分析:
-
Temperature 设置 (1.2)
- 作用:使回复更灵活、更自然
- 值说明:1.2 是较高的值,表示输出更有创造性
- 适用场景:客服对话需要友好、自然的语气
- 对比:如果设置为 0.4,回复可能过于机械
-
System 提示词设计
- 角色定位:明确 AI 是"XS航空智能客服代理"
- 语气要求:强调"友好的语气"
- 简洁性:提示词简洁,给 AI 更多发挥空间
-
与 planningChatClient 的对比
| 特性 | planningChatClient | botChatClient |
|---|---|---|
| Temperature | 0.4(确定性) | 1.2(灵活性) |
| 主要功能 | 任务识别 | 对话服务 |
| 输出格式 | 结构化(Job对象) | 文本(自然语言) |
| 提示词风格 | 规则明确 | 角色定位 |
| 使用场景 | 意图识别 | 友好对话 |
2.3 AiJob.java
代码
java
package com.xushu.springai.chain;
import java.util.Map;
public class AiJob {
record Job(JobType jobType, Map<String,String> keyInfos) {
}
public enum JobType{
CANCEL,
QUERY,
OTHER,
}
}
详细分析
2.3.1 Job Record
结构说明:
java
record Job(
JobType jobType, // 任务类型
Map<String,String> keyInfos // 关键信息映射
)
字段说明:
-
jobType (JobType)
- 类型:枚举类型
- 作用:标识任务类型
- 值:CANCEL、QUERY、OTHER
- 用途:用于 switch 语句进行路由
-
keyInfos (Map<String, String>)
-
类型:字符串键值对映射
-
作用:存储从用户输入中提取的关键信息
-
示例 :
java// 退票任务 { "name": "张三", "orderId": "123456" } // 查票任务 { "orderId": "123456" } -
灵活性:使用 Map 可以存储任意数量的信息
-
Record 的优势:
- 简洁:自动生成构造函数、getter、equals、hashCode、toString
- 不可变:所有字段都是 final,保证数据安全
- 类型安全:编译时检查类型
2.3.2 JobType 枚举
定义:
java
public enum JobType {
CANCEL, // 退票
QUERY, // 查票
OTHER // 其他
}
设计说明:
- CANCEL:退票任务,需要姓名和订单号
- QUERY:查票任务,需要订单号
- OTHER:其他任务,使用智能客服处理
扩展性 :
可以轻松添加新的任务类型:
java
public enum JobType {
CANCEL, // 退票
QUERY, // 查票
CHANGE, // 改签(新增)
REFUND, // 退款(新增)
OTHER // 其他
}
2.4 MultiModelsController.java
代码结构
java
@RestController
public class MultiModelsController {
@Autowired
ChatClient planningChatClient;
@Autowired
ChatClient botChatClient;
@GetMapping("/stream")
Flux<String> stream(@RequestParam String message) {
// 实现逻辑
}
}
详细分析
2.4.1 依赖注入
java
@Autowired
ChatClient planningChatClient; // 任务规划模型
@Autowired
ChatClient botChatClient; // 智能客服模型
说明:
- 两个 ChatClient 由
AiConfig配置类提供 - Spring 自动注入到控制器
- 可以在控制器中直接使用
2.4.2 stream 方法 - 核心处理逻辑
完整流程分析:
步骤1:创建流式响应
java
Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();
sink.tryEmitNext("正在计划任务...<br/>");
说明:
Sinks.many():创建多值 Sinkunicast():单播模式,一个订阅者onBackpressureBuffer():背压缓冲,处理速度不匹配tryEmitNext():发送消息到流
步骤2:异步任务规划
java
new Thread(() -> {
AiJob.Job job = planningChatClient.prompt()
.user(message)
.call()
.entity(AiJob.Job.class);
// ...
}).start();
说明:
- 使用新线程避免阻塞
- 调用
planningChatClient识别任务 - 使用
.entity(AiJob.Job.class)进行结构化输出 - 返回
Job对象,包含任务类型和关键信息
步骤3:任务路由处理
退票处理 (CANCEL):
java
case CANCEL -> {
System.out.println(job);
if(job.keyInfos().size() == 0) {
sink.tryEmitNext("请输入姓名和订单号.");
} else {
// todo.. 执行业务 ticketService.cancel
// --->springai --->json
sink.tryEmitNext("退票成功!");
}
}
分析:
- 检查
keyInfos是否为空 - 如果为空,提示用户输入
- 如果不为空,执行退票业务逻辑
- 注释说明可以集成业务服务
查票处理 (QUERY):
java
case QUERY -> {
System.out.println(job);
if(job.keyInfos().size() == 0) {
sink.tryEmitNext("请输入订单号.");
}
// todo.. 执行业务 ticketService.query()
sink.tryEmitNext("查询预定信息:xxxx");
}
分析:
- 检查是否有订单号
- 如果没有,提示用户输入
- 执行查票业务逻辑
- 返回查询结果
其他处理 (OTHER):
java
case OTHER -> {
Flux<String> content = botChatClient.prompt()
.user(message)
.stream()
.content();
content.doOnNext(sink::tryEmitNext) // 推送每条AI流内容
.doOnComplete(() -> sink.tryEmitComplete())
.subscribe();
}
分析:
- 使用
botChatClient进行对话 .stream()启用流式输出.content()获取内容流doOnNext()将每条消息推送到 SinkdoOnComplete()完成时关闭流subscribe()订阅并开始处理
步骤4:返回流式响应
java
return sink.asFlux();
说明:
- 将 Sink 转换为 Flux
- 返回给客户端
- 支持 Server-Sent Events (SSE) 或 WebFlux
三、技术原理深度解析
3.1 结构化输出原理
3.1.1 Spring AI 如何实现结构化输出
底层流程:
1. 分析目标类结构
↓
2. 生成 JSON Schema
↓
3. 将 Schema 添加到提示词
↓
4. AI 返回 JSON
↓
5. 反序列化为 Java 对象
示例:AiJob.Job 的 Schema 生成
json
{
"type": "object",
"properties": {
"jobType": {
"type": "string",
"enum": ["CANCEL", "QUERY", "OTHER"]
},
"keyInfos": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": ["jobType", "keyInfos"]
}
AI 收到的提示词:
# 票务助手任务拆分规则
...
请返回以下 JSON 格式:
{
"jobType": "CANCEL|QUERY|OTHER",
"keyInfos": {
"key1": "value1",
"key2": "value2"
}
}
3.2 多模型协作原理
3.2.1 模型分工策略
规划模型 (planningChatClient):
- 职责:意图识别、信息提取
- 特点:低 Temperature,高确定性
- 输出:结构化数据
客服模型 (botChatClient):
- 职责:自然对话、友好服务
- 特点:高 Temperature,高灵活性
- 输出:自然语言文本
3.2.2 协作流程
用户输入
↓
规划模型分析
↓
判断任务类型
├─ 结构化任务 → 业务逻辑处理
└─ 对话任务 → 客服模型处理
↓
返回结果
3.3 流式响应原理
3.3.1 Reactor 流式处理
Sinks 的作用:
- 创建 :
Sinks.many().unicast()创建单播 Sink - 发送 :
tryEmitNext()发送消息 - 完成 :
tryEmitComplete()标记完成 - 转换 :
asFlux()转换为响应式流
Flux 的作用:
- 订阅:客户端订阅 Flux
- 推送:服务端推送消息
- 背压:自动处理速度不匹配
3.3.2 异步处理
为什么使用新线程:
java
new Thread(() -> {
// 处理逻辑
}).start();
原因:
- 任务规划可能需要较长时间
- 避免阻塞主线程
- 保持流式响应的实时性
改进建议:
java
@Autowired
ExecutorService taskExecutor;
taskExecutor.submit(() -> {
// 处理逻辑
});
四、实际应用场景
4.1 智能客服系统
完整流程:
用户: "我要退票"
↓
系统: "正在计划任务..."
↓
规划模型识别: CANCEL
↓
检查信息: keyInfos 为空
↓
系统: "请输入姓名和订单号."
↓
用户: "张三,订单号123456"
↓
规划模型识别: CANCEL, keyInfos={name: "张三", orderId: "123456"}
↓
执行退票逻辑
↓
系统: "退票成功!"
4.2 对话场景
完整流程:
用户: "你们公司的服务怎么样?"
↓
系统: "正在计划任务..."
↓
规划模型识别: OTHER
↓
路由到客服模型
↓
客服模型: "我们公司一直致力于提供优质的服务..."
↓
流式返回给用户
五、代码优化建议
5.1 线程管理优化
当前实现:
java
new Thread(() -> {
// 处理逻辑
}).start();
优化方案:
java
@Configuration
public class ThreadConfig {
@Bean
public ExecutorService taskExecutor() {
return Executors.newFixedThreadPool(10);
}
}
// 使用
@Autowired
ExecutorService taskExecutor;
taskExecutor.submit(() -> {
// 处理逻辑
});
5.2 异常处理优化
当前实现:缺少异常处理
优化方案:
java
new Thread(() -> {
try {
AiJob.Job job = planningChatClient.prompt()
.user(message)
.call()
.entity(AiJob.Job.class);
// 处理逻辑
} catch (Exception e) {
log.error("处理失败", e);
sink.tryEmitNext("系统错误,请稍后重试。");
sink.tryEmitComplete();
}
}).start();
5.3 业务逻辑完善
当前实现:使用 TODO 注释
优化方案:
java
case CANCEL -> {
if(job.keyInfos().size() == 0) {
sink.tryEmitNext("请输入姓名和订单号.");
} else {
try {
String name = job.keyInfos().get("name");
String orderId = job.keyInfos().get("orderId");
// 验证信息
if (name == null || orderId == null) {
sink.tryEmitNext("信息不完整,请提供姓名和订单号。");
return;
}
// 执行业务逻辑
CancelResult result = ticketService.cancel(name, orderId);
if (result.isSuccess()) {
sink.tryEmitNext("退票成功!退款金额:" + result.getAmount());
} else {
sink.tryEmitNext("退票失败:" + result.getReason());
}
} catch (Exception e) {
log.error("退票失败", e);
sink.tryEmitNext("退票处理失败,请稍后重试。");
}
}
sink.tryEmitComplete();
}
六、总结
6.1 核心设计理念
- 职责分离:不同模型负责不同任务
- 结构化处理:使用结构化输出,类型安全
- 智能路由:根据意图自动路由
- 流式响应:实时反馈,提升体验
6.2 技术亮点
- Spring AI 结构化输出
- 多模型配置和管理
- Reactor 流式响应
- 责任链模式应用
6.3 适用场景
- 智能客服系统
- 任务自动化系统
- 意图识别系统
- 多步骤工作流系统
关键代码 :
java
package com.xushu.springai.chain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;
@RestController
public class MultiModelsController {
private static final Logger log = LoggerFactory.getLogger(MultiModelsController.class);
@Autowired
ChatClient planningChatClient;
@Autowired
ChatClient botChatClient;
/**
* 流式处理用户请求
*
* <p>该方法实现多模型协作的智能客服系统:
* 1. 使用 planningChatClient 识别用户意图(结构化输出)
* 2. 根据任务类型路由到不同的处理逻辑
* 3. 支持流式响应,实时返回结果
*
* <p><b>执行流程:</b>
* <ol>
* <li>创建流式响应 Sink</li>
* <li>发送初始消息:"正在计划任务..."</li>
* <li>异步调用 planningChatClient 识别任务</li>
* <li>根据任务类型路由处理:
* <ul>
* <li>CANCEL:退票处理</li>
* <li>QUERY:查票处理</li>
* <li>OTHER:智能客服对话</li>
* </ul>
* </li>
* <li>流式返回结果</li>
* </ol>
*
* <p><b>使用示例:</b>
* <pre>
* GET http://localhost:8080/stream?message=我要退票
* GET http://localhost:8080/stream?message=查询我的订单
* GET http://localhost:8080/stream?message=你好
* </pre>
*
* <p><b>响应格式:</b>
* Content-Type: text/stream;charset=UTF8
*
* <p><b>注意事项:</b>
* <ul>
* <li>使用异步处理,避免阻塞</li>
* <li>所有分支都需要调用 sink.tryEmitComplete() 完成流</li>
* <li>异常处理确保流能够正常完成</li>
* </ul>
*
* @param message 用户输入的消息内容
* @return Flux<String> 流式响应,包含处理过程中的所有消息
*
* @see com.xushu.springai.chain.AiJob
* @see com.xushu.springai.chain.AiConfig
*/
// http://localhost:8080/stream?message=你好
@GetMapping(value = "/stream")
Flux<String> stream(@RequestParam("message") String message) {
// 参数验证
if (message == null || message.trim().isEmpty()) {
return Flux.just("错误:消息内容不能为空");
}
// 创建一个用于接收多条消息的 Sink
// unicast(): 单播模式,一个订阅者
// onBackpressureBuffer(): 背压缓冲,处理生产速度 > 消费速度的情况
Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();
// 推送初始消息
sink.tryEmitNext("正在计划任务...<br/>");
// 异步处理,避免阻塞主线程
// 注意:生产环境建议使用线程池而不是直接创建线程
new Thread(() -> {
try {
// 使用任务规划模型识别用户意图
// entity(AiJob.Job.class) 将 AI 回复转换为结构化的 Job 对象
AiJob.Job job = planningChatClient.prompt()
.user(message)
.call()
.entity(AiJob.Job.class);
// 空值检查
if (job == null) {
log.warn("任务识别返回 null,用户消息: {}", message);
sink.tryEmitNext("任务识别失败,请重试。");
sink.tryEmitComplete();
return;
}
// 根据任务类型路由到不同的处理逻辑
switch (job.jobType()) {
case CANCEL -> {
log.info("识别到退票任务: {}", job);
if (job.keyInfos() == null || job.keyInfos().isEmpty()) {
// 信息不完整,提示用户输入
sink.tryEmitNext("请输入姓名和订单号.");
} else {
// 信息完整,执行退票逻辑
// todo.. 执行业务 ticketService.cancel
// --->springai --->json
String name = job.keyInfos().get("name");
String orderId = job.keyInfos().get("orderId");
log.info("退票请求 - 姓名: {}, 订单号: {}", name, orderId);
sink.tryEmitNext("退票成功!");
}
// 完成流
sink.tryEmitComplete();
}
case QUERY -> {
log.info("识别到查票任务: {}", job);
if (job.keyInfos() == null || job.keyInfos().isEmpty()) {
// 信息不完整,提示用户输入
sink.tryEmitNext("请输入订单号.");
} else {
// 信息完整,执行查票逻辑
// todo.. 执行业务 ticketService.query()
String orderId = job.keyInfos().get("orderId");
log.info("查票请求 - 订单号: {}", orderId);
sink.tryEmitNext("查询预定信息:xxxx");
}
// 完成流
sink.tryEmitComplete();
}
case OTHER -> {
log.info("识别到其他任务,使用智能客服处理: {}", job);
// 使用智能客服模型进行流式对话
// stream() 启用流式输出
// content() 获取内容流
Flux<String> content = botChatClient.prompt()
.user(message)
.stream()
.content();
// 订阅流,将每条消息推送到 Sink
content.doOnNext(msg -> {
// 推送每条AI流内容
sink.tryEmitNext(msg);
})
.doOnError(error -> {
// 错误处理
log.error("智能客服处理失败", error);
sink.tryEmitNext("<br/>抱歉,处理过程中发生错误,请稍后重试。");
sink.tryEmitComplete();
})
.doOnComplete(() -> {
// 流完成
sink.tryEmitComplete();
})
.subscribe();
}
default -> {
log.warn("未知任务类型: {}", job.jobType());
sink.tryEmitNext("解析失败:未知任务类型");
sink.tryEmitComplete();
}
}
} catch (Exception e) {
// 异常处理:确保流能够正常完成
log.error("处理用户请求失败,用户消息: " + message, e);
sink.tryEmitNext("系统错误,请稍后重试。错误信息: " + e.getMessage());
sink.tryEmitComplete();
}
}).start();
// 返回流式响应
return sink.asFlux();
}
}
测试效果


