Spring AI入门到实战到原理源码-多模型协作智能客服系统

结构化输出实现投诉客服和地址自动解析

一、概述

本文档基于 TestStructuredOut.java 代码文件,总结了 Spring AI 中结构化输出的实现方式,包括:

  1. 投诉客服判断:使用布尔值结构化输出判断用户是否有投诉意图
  2. 地址自动解析:使用 Record 类型结构化输出自动解析收货地址信息
  3. 底层实现:使用 BeanOutputConverter 实现自定义结构化输出

二、核心概念

2.1 结构化输出(Structured Output)

结构化输出是 Spring AI 提供的一种功能,允许将 AI 模型的文本回复自动转换为 Java 对象。

这样可以:

  • 类型安全:直接获得强类型的 Java 对象
  • 易于处理:无需手动解析 JSON 或文本
  • 业务集成:直接用于业务逻辑处理

2.2 实现方式

Spring AI 提供了两种方式实现结构化输出:

  1. 高级 API :使用 ChatClient.entity() 方法(推荐)
  2. 底层 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进行对话
    }
}
实现原理
  1. 提示词设计

    • 使用 system 提示词明确要求 AI 只返回 true 或 false
    • 要求不要输出多余内容,确保返回的是纯布尔值
  2. 结构化输出

    • 使用 .entity(Boolean.class) 将 AI 回复转换为 Boolean 对象
    • Spring AI 会自动解析 AI 返回的文本,提取布尔值
  3. 业务逻辑

    • 根据返回的布尔值进行分支处理
    • 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);
}
实现原理
  1. 定义数据结构

    • 使用 Java Record 定义地址结构
    • Record 是 Java 14+ 的特性,适合定义不可变的数据传输对象
    • 包含所有需要提取的字段:姓名、电话、省、市、区、详细地址
  2. 提示词设计

    • 使用 system 提示词说明任务:从文本中提取收货信息
    • AI 会根据 Address 类的字段结构自动提取对应信息
  3. 结构化输出

    • 使用 .entity(Address.class) 将 AI 回复转换为 Address 对象
    • Spring AI 会自动:
      • 生成 JSON Schema 描述 Address 结构
      • 要求 AI 按照 Schema 返回 JSON
      • 将 JSON 反序列化为 Address 对象
  4. 自动映射

    • 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);
}
实现原理
  1. BeanOutputConverter

    • 用于将 AI 的文本回复转换为 Java Bean
    • 内部使用 JSON Schema 描述目标对象结构
    • 支持将 JSON 字符串转换为 Java 对象
  2. getFormat() 方法

    • 返回 JSON Schema 格式的字符串
    • 描述目标对象的所有字段和类型
    • 这个格式会被添加到提示词中,指导 AI 返回正确的 JSON
  3. 转换流程

    • 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 提示词设计原则

  1. 明确性:清楚说明需要提取什么信息
  2. 格式要求:明确要求返回格式(JSON、布尔值等)
  3. 示例:可以提供示例帮助 AI 理解
  4. 约束:说明不要输出多余内容

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 提供的强大功能,可以:

  1. 简化开发:无需手动解析 JSON 或文本
  2. 类型安全:直接获得强类型对象
  3. 智能提取:AI 能够理解各种格式的输入
  4. 易于集成:直接用于业务逻辑处理

适用场景:

  • 智能客服路由
  • 信息提取(地址、订单、个人信息等)
  • 数据清洗和标准化
  • 表单自动填充

注意事项:

  • 设计清晰的 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();
}

功能分析

  1. Temperature 设置 (0.4)

    • 作用:控制 AI 输出的随机性
    • 值说明:0.4 是较低的值,表示输出更确定、更一致
    • 适用场景:任务识别需要准确性,避免误判
    • 对比:如果设置为 1.0+,可能导致同样的输入识别出不同的任务类型
  2. System 提示词设计

    markdown 复制代码
    # 票务助手任务拆分规则
    ## 1.要求
    ### 1.1 根据用户内容识别任务
    
    ## 2. 任务
    ### 2.1 JobType:退票(CANCEL) 要求用户提供姓名和预定号, 或者从对话中提取;
    ### 2.2 JobType:查票(QUERY) 要求用户提供预定号, 或者从对话中提取;
    ### 2.3 JobType:其他(OTHER)

    设计要点

    • 使用 Markdown 格式,结构清晰
    • 明确说明三种任务类型
    • 说明每种任务需要的信息
    • 强调可以从对话中提取信息
  3. 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();
}

功能分析

  1. Temperature 设置 (1.2)

    • 作用:使回复更灵活、更自然
    • 值说明:1.2 是较高的值,表示输出更有创造性
    • 适用场景:客服对话需要友好、自然的语气
    • 对比:如果设置为 0.4,回复可能过于机械
  2. System 提示词设计

    • 角色定位:明确 AI 是"XS航空智能客服代理"
    • 语气要求:强调"友好的语气"
    • 简洁性:提示词简洁,给 AI 更多发挥空间
  3. 与 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 // 关键信息映射
)

字段说明

  1. jobType (JobType)

    • 类型:枚举类型
    • 作用:标识任务类型
    • :CANCEL、QUERY、OTHER
    • 用途:用于 switch 语句进行路由
  2. 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():创建多值 Sink
  • unicast():单播模式,一个订阅者
  • 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() 将每条消息推送到 Sink
  • doOnComplete() 完成时关闭流
  • 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 核心设计理念

  1. 职责分离:不同模型负责不同任务
  2. 结构化处理:使用结构化输出,类型安全
  3. 智能路由:根据意图自动路由
  4. 流式响应:实时反馈,提升体验

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();
    }
}

测试效果



相关推荐
沫儿笙18 小时前
CLOOS克鲁斯焊接机器人混合气节气装置
人工智能·机器人
一只落魄的蜂鸟18 小时前
【2026年-01期】AI Agent Trends of 2025
人工智能
Deepoch18 小时前
从“机械臂”到“农艺手”:Deepoc如何让机器人理解果实的生命语言
人工智能·机器人·采摘机器人·农业机器人·具身模型·deepoc
BEOL贝尔科技18 小时前
生物冰箱智能锁如何帮助实验室做好生物样本保存工作的权限管理呢?
人工智能·数据分析
yyy(十一月限定版)18 小时前
c++(3)类和对象(中)
java·开发语言·c++
dundunmm18 小时前
【每天一个知识点】模式识别与群体智慧:AI 如何从“看见数据”走向“理解世界”
人工智能·群体智能·模式识别
hkNaruto18 小时前
【AI】AI学习笔记:关于嵌入模型的切片大小,实际的业务系统中如何选择
人工智能·笔记·学习
华奥系科技18 小时前
老旧社区适老化智能改造,两个系统成社区标配项目
大数据·人工智能