spring ai1.0实战:ChatClient(2)

目录

Advisor对话拦截

日志拦截:

敏感词拦截:

自定义拦截:

重读拦截器:

原理:

对话记忆

数据库存储对话

多层次记忆架构

结构化输出

类型

基础类型

pojo类型


Advisor对话拦截

Spring AI 利用面向切面的思想提供 Advisors API,它提供了灵活而强大的方法来拦截、修改和增强 Spring 应用程序中的 AI 驱动交互。

Advisor接口提供了CallAdvisor和组成CallAdvisorChain(适用于非流式场景),以及StreamAdvisor和StreamAdvisorChain(适用于流式场景)。它还包括ChatClientRequest,用于表示未密封的 Prompt 请求,以及ChatClientResponse用于表示聊天完成响应。

日志拦截:

由于 AI 对话过程是一个"黑盒",不利于我们调试,可以利用 SimpleLoggerAdvisor 拦截对话记录,帮助观察我们发了什么信息给大模型,便于调试。

java 复制代码
    @Test
    public void testAdvisor(@Autowired ChatClient.Builder chatClientBuilder){
        ChatClient chatClient = chatClientBuilder
                .defaultAdvisors(new SimpleLoggerAdvisor()// SimpleLoggerAdvisor() 系统自带的日志拦截器
             )
                .build();
        String content=chatClient.prompt()
                .user("你好!")
                //.advisors()
                .call()
                .content();
        System.out.println(content);
    }

敏感词拦截:

java 复制代码
 @Test
    public void testAdvisor(@Autowired ChatClient.Builder chatClientBuilder){
        ChatClient chatClient = chatClientBuilder
                .defaultAdvisors(new SimpleLoggerAdvisor(), // SimpleLoggerAdvisor() 系统自带的日志拦截器
                        new SafeGuardAdvisor(List.of("大家")))  //SafeGuardAdvisor()敏感词拦截器
                .build();
        String content=chatClient.prompt()
                .user("你好!大家")
                //.advisors()
                .call()
                .content();
        System.out.println(content);
    }

注:当用户提及与系统存入的敏感词相似,就会被 SafeGuardAdvisor() 拦截。

自定义拦截:


重读拦截器:

重读让 LLMs 重新审视输入问题,通过这种方式让 LLMs 理解问题,发现更复杂的模式。

ReReadingAdvisor:

java 复制代码
public class ReReadingAdvisor implements BaseAdvisor {
    private  static  final  String DEFAULT_USER_TEXT_ADVISE= """
            {re2_input_query}
            Read the question again: {re2_input_query}
            """;

    @Override
    public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
        // todo.. 请求之前  重写提示词
        String contents = chatClientRequest.prompt().getContents();
        String re2InputQuery = PromptTemplate.builder().template(DEFAULT_USER_TEXT_ADVISE).build()
                .render(Map.of("re2_input_query", contents));

        ChatClientRequest clientRequest = chatClientRequest.mutate()
                .prompt(Prompt.builder().content(re2InputQuery).build())
                .build();
        return clientRequest;
    }

    @Override
    public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
        return null;
    }

  
     @Override
    public int getOrder() {
        return 0;
    }
}

测试:

java 复制代码
 @Test
    public void testReAdvisor(@Autowired ChatClient.Builder chatClientBuilder){
        ChatClient chatClient = chatClientBuilder
                .defaultAdvisors(new SimpleLoggerAdvisor(), // SimpleLoggerAdvisor() 系统自带的日志拦截器
                       new ReReadingAdvisor())  //ReReadingAdvisor()自定义重读拦截器
                .build();
        String content=chatClient.prompt()
                .user("王维帅不帅")
                //.advisors()
                .call()
                .content();
        System.out.println(content);
    }

原理:

Advisor 只有结合 ChatClient 才能用!是 Spring AI 上层提供的

对话记忆


大语言模型是无状态的,这就意味着它们不会保留之前交互的信息。

如何让大模型记住我们的对话呢?实际上,每次对话都需要将之前的对话发送给大模型,这种方式称作多轮对话。

Spring AI 提供了一个 ChatMemory 的组件用于存储聊天记录,允许 LLM 跨多个交互存储和检索信息。并且可以为不同用户的多个交互之间维护上下文或状态。

也可以通过 Advisor 对话拦截,Spring AI 为我们提供了ChatMemoryAutoConfiguration自动配置类。

依赖:

java 复制代码
   <!--对话记忆-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId>
        </dependency>

测试:

java 复制代码
@SpringBootTest
public class TestChatMemory {
    ChatClient chatClient;
    @BeforeEach
    public  void init(@Autowired
                      DeepSeekChatModel chatModel,
                      @Autowired
                      ChatMemory chatMemory) {
        chatClient = ChatClient
                .builder(chatModel)
                .defaultAdvisors(
                        PromptChatMemoryAdvisor.builder(chatMemory).build()
                )
                .build();
    }
    @Test
    public void testChatOptions() {
        String content = chatClient.prompt()
                .user("我叫王维 ?")
                .advisors(advisorSpec->advisorSpec.param(ChatMemory.CONVERSATION_ID, "1"))//设置对话唯一标识
                .call()
                .content();
        System.out.println(content);
        System.out.println("--------------------------------------------------------------------------");
        //对话记忆
        content = chatClient.prompt()
                .user("我叫什么 ?")
                .advisors(advisorSpec->advisorSpec.param(ChatMemory.CONVERSATION_ID, "2"))
                .call()
                .content();
        System.out.println(content);

        System.out.println("--------------------------------------------------------------------------");

        content = chatClient.prompt()
                .user("我叫什么 ?")
                .advisors(advisorSpec->advisorSpec.param(ChatMemory.CONVERSATION_ID, "1"))
                .call()
                .content();
        System.out.println(content);
    }

}

注:若有多个用户在进行对话,将需要进行对话隔离,利用 ChatMemory.CONVERSATION_ID 可以将用户对话隔离。

配置聊天记录最大存储数量:

大模型的 Token 是有上限的,如果你发送过多的聊天记录,可能就会导致 Token 过长。DEFAULT_MAX_MESSAGES 默认 20 即 10 次对话。

java 复制代码
    @Bean
        ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository){
            return MessageWindowChatMemory.builder()
                    .maxMessages(10)
                    .build();
        }

数据库存储对话

默认情况下,对话内容会存在 JVM 内存会导致:

  1. 一直存最终只会撑爆 JVM 导致 OOM。

  2. 重启就会丢,可以存储到第三方进行持久化。

Spring AI 提供了几种方式(Cassandra、JDBC、Neo4j、Redis),我们以 JDBC 为例:

a. 依赖:

XML 复制代码
 <!--存储方式-->

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
        </dependency>

        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>


        <!--mysql驱动-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

b. 配置

java 复制代码
spring.ai.chat.memory.repository.jdbc.initialize-schema=always
spring.ai.chat.memory.repository.jdbc.schema=classpath:/sql/schema-mysql.sql
XML 复制代码
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/springai?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver

c. SQL:

sql 复制代码
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
                                                     `conversation_id` VARCHAR(36) NOT NULL,
    `content` TEXT NOT NULL,
    `type` VARCHAR(10) NOT NULL,
    `timestamp` TIMESTAMP NOT NULL,

    INDEX `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`, `timestamp`)
    );

注:测试类用对话记忆的测试类,最终发现在配置类中设置的最大对话为几,数据库中就会存储几条该用户的消息,会更新数据库中的信息。

多层次记忆架构

对话记忆是基于对话上下文的存储,但记忆一旦多了,就会触发 Token 上限,这时候就需要多层次记忆架构,它是模仿人类的记忆类型。

  • 近期记忆:保留在上下文窗口中的最近几轮对话,每轮对话完成后立即存储(可通过ChatMemory); 10 条
  • 中期记忆:通过RAG检索的相关历史对话(每轮对话完成后,异步将对话内容转换为向量并存入向量数据库) 5条
  • 长期记忆:关键信息的固化总结

方式一:定时批处理

  • 通过定时任务(如每天或每周)对积累的对话进行总结和提炼
  • 提取关键信息、用户偏好、重要事实等
  • 批处理方式降低计算成本,适合大规模处理

方式二:关键点实时处理

  • 在对话中识别出关键信息点时立即提取并存储
  • 例如,当用户明确表达偏好、提供个人信息或设置持久性指令时
  • 采用"写入触发器"机制,在特定条件下自动更新长期记忆

结构化输出


类型


基础类型

在agent中可以用于判断内容的两个分支,不同分支走不同的逻辑:

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("用户不是投诉,自动转客服机器人");

        }
    }

pojo类型

让大模型从一段话中提取信息,例如用购物app复制一个地址,会自动为你填入对应的输入框。

java 复制代码
    ChatClient chatClient;
    @BeforeEach
    public void  init(@Autowired DashScopeChatModel chatModel){
        chatClient = ChatClient.builder(chatModel).build();

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

原理:spring ai底层写了一个FormatProvider的类,为大模型举例子如何填入对应的对象。

相关推荐
实名上网宋凯宣9 小时前
Django
后端·python·django
天河归来10 小时前
第三方接口不稳定,如何让核心任务继续完成
java·大数据
Knight_AL11 小时前
Java 文件下载时文件名中的 `#` 被转换成 `%23`?一次 URL 编码与解码问题排查
java·开发语言
利威尔xu11 小时前
Go IM 源码实战(14):本地缓存快 3000 倍,失效却难 10 倍
后端·缓存·微服务·面试·golang
逃逸线LOF12 小时前
Ribbon负载均衡原理(Irule组件、源码分析)
后端·spring cloud·ribbon
砚底藏山河12 小时前
python量化自定义周期 K 线合成
java·开发语言·数据库·python·金融
慢云智慧空间12 小时前
从设备智能到空间理解,空间智能终端解决方案商如何重新定义行业标准
后端
利威尔xu12 小时前
Go IM 源码实战(13):30 个会话只有 2 个红点,未读数错在三条路径
redis·后端·微服务·面试·golang
猿究院_夏燊13 小时前
MySQL 的四种隔离级别
java·数据库·mysql
薛定谔的猫-菜鸟程序员13 小时前
软件架构设计师开发资料大全(从入门到精通)
后端·python·系统架构·flask·架构设计师