前言

最近学习了下使用 Spring AI 进行和大模型的聊天对话,其中涉及到对话记忆,向量数据库,RAG 检索增强。

全部代码已经提交到代码仓库中,具体的可以去gitte中看看。

一、正文

1.1 项目结构

项目继承于 spring-ai-demo项目,父级主要控制依赖版本,子模块用于具体的功能实现。
在这里插入图片描述

1.2 项目环境

本次实践会使用到向量数据库 Qdrant,需要在 Qdrant下载地址 中下载,并安装启动。
另外,对外提供了聊天接口,向量数据库新增和清空文档接口,获取文档接口等。

java 版本选择21,springboot版本是3.4.2

1.3 完整代码

1.3.1 spring-ai-demo的pom文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.pine</groupId>
    <artifactId>spring-ai-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <name>spring-ai-demo</name>
    <url>http://maven.apache.org</url>
    <modules>
        <module>dynamic-datasource-mcp-sse-server</module>
        <module>dynamic-datasource-mcp-stdio-server</module>
        <module>spring-ai-chat-server</module>
    </modules>

    <properties>
        <java.version>21</java.version>
        <spring-boot.version>3.4.2</spring-boot.version>
        <spring-ai.version>1.0.0</spring-ai.version>
        <spring-ai-alibaba.version>1.0.0.1</spring-ai-alibaba.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud.ai</groupId>
                <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
                <version>${spring-ai-alibaba.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>


1.3.2 spring-ai-chat-server 的pom文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.pine</groupId>
        <artifactId>spring-ai-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>spring-ai-chat-server</artifactId>
    <packaging>jar</packaging>

    <name>spring-ai-chat-server</name>
    <url>http://maven.apache.org</url>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.34</version>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-api</artifactId>
            <version>1.65.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
        </dependency>
        <!-- 阿里ai的starter -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <inherited>true</inherited>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <parameters>true</parameters>
                    <showWarnings>true</showWarnings>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>org.feng.MainApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

1.3.3 ChatConfig

package org.feng.config;

import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import io.qdrant.client.QdrantClient;
import lombok.SneakyThrows;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 聊天配置
 *
 * @author pine
 * @version v1.0
 * @since 2025-08-12 20:22
 */
@Configuration
public class ChatConfig {


    /**
     * 配置ChatClient,注册系统指令和工具函数
     */
    @Bean
    @SneakyThrows
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("""
                                 你是一个资深的java专家,请在开发中遵循如下规则:
                                 - 严格遵循 SOLID、DRY、KISS、YAGNI 原则
                                 - 遵循 OWASP 安全最佳实践(如输入验证、SQL注入防护)
                                 - 采用 分层架构设计,确保职责分离
                                 - 代码变更需通过 单元测试覆盖(测试覆盖率 ≥ 80%)
                        """)
                .build();
    }

    /**
     * 内存聊天记忆
     */
    @Bean
    public MessageWindowChatMemory chatMemory() {
        return MessageWindowChatMemory.builder().maxMessages(100).build();
    }


    /**
     * 向量数据库 qdrant
     */
    @Bean
    public VectorStore vectorStore(@Autowired QdrantClient qdrantClient, @Autowired DashScopeEmbeddingModel dashScopeEmbeddingModel) {
        return QdrantVectorStore.builder(qdrantClient, dashScopeEmbeddingModel).build();
    }
}

1.3.4 WebfluxConfig

package org.feng.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

/**
 * webflux配置
 *
 * @author pine
 * @version v1.0
 * @since 2025-08-12 20:27
 */
@Configuration
public class WebfluxConfig {

    @Bean
    public RestClient.Builder restClientBuilder() {
        return RestClient.builder();
    }
}

1.3.5 ChatController

package org.feng.controller;

import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.document.Document;
import org.springframework.ai.rag.Query;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter;
import org.springframework.ai.rag.postretrieval.document.DocumentPostProcessor;
import org.springframework.ai.rag.preretrieval.query.expansion.MultiQueryExpander;
import org.springframework.ai.rag.retrieval.join.ConcatenationDocumentJoiner;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 聊天控制器
 *
 * @author pine
 * @version v1.0
 * @since 2025-08-12 20:28
 */
@Controller
@Slf4j
@RequestMapping("/api")
public class ChatController {
    @Resource
    private ChatClient chatClient;
    @Resource
    private ChatMemory chatMemory;
    @Resource
    private VectorStore vectorStore;

    private final ChatClient.Builder chatBuilder;

    public ChatController(ChatClient.Builder chatBuilder) {
        this.chatBuilder = chatBuilder;
    }


    /**
     * 聊天
     */
    @GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    @SneakyThrows
    @ResponseBody
    public Flux<String> chat(@RequestParam(name="conversationId", required = false, defaultValue = "feng123") String conversationId, @RequestParam(name="message", defaultValue = "你好,你有什么功能") String message) {
        log.info("chat(), conversationId:{},message:{}", conversationId, message);

        // 查询扩展
        MultiQueryExpander queryExpander = MultiQueryExpander.builder()
                // 扩展数量:3
                .numberOfQueries(3)
                .chatClientBuilder(chatBuilder)
                // 包括源查询条件
                .includeOriginal(true)
                .build();

        // 增加上下文的信息
        ContextualQueryAugmenter contextualQueryAugmenter = ContextualQueryAugmenter.builder()
                .allowEmptyContext(true)
                .build();


        // RAG
        RetrievalAugmentationAdvisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
                // 使用向量数据库作为文档源
                .documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).build())
                // 扩充查询条件
                .queryExpander(queryExpander)
                // 拼接查询到的文档
                .documentJoiner(new ConcatenationDocumentJoiner())
                // 取多个doc中的第一个
                .documentPostProcessors(new SelectedFirstDocumentPostProcessor())
                // 对生成的查询增强上下文
                .queryAugmenter(contextualQueryAugmenter)
                .build();


        return chatClient.prompt(message)
                // 日志
                .advisors(new SimpleLoggerAdvisor())
                // 聊天记忆(暂时使用内存,后续支持jdbc)
                .advisors(MessageChatMemoryAdvisor.builder(chatMemory).conversationId(conversationId).build())
                // RAG
                .advisors(retrievalAugmentationAdvisor)
                .stream()
                .content();
    }

    /**
     * 聊天记录
     */
    @GetMapping(value = "/messages")
    @SneakyThrows
    @ResponseBody
    public List<Message> messages(@RequestParam(name="conversationId", defaultValue = "feng123") String conversationId) {
        return chatMemory.get(conversationId);
    }

    /**
     * 清空聊天记录
     */
    @GetMapping(value = "/clearMessages")
    @SneakyThrows
    @ResponseBody
    public String clearMessages(@RequestParam(name="conversationId", defaultValue = "feng123") String conversationId) {
        chatMemory.clear(conversationId);
        return "success";
    }

    /**
     * 向量数据库增加数据
     */
    @ResponseBody
    @SneakyThrows
    @PostMapping(value = "/addDocuments")
    public String addDocuments(@RequestParam(name = "documents") List<String> documents) {
        List<Document> collected = documents.stream().map(Document::new).collect(Collectors.toList());
        vectorStore.add(collected);
        return "success";
    }

    @SuppressWarnings("all")
    private static class SelectedFirstDocumentPostProcessor implements DocumentPostProcessor {
        @Override
        public List<Document> process(Query query, List<Document> documents) {
            if (documents.isEmpty()) {
                return Collections.emptyList();
            }
            return Collections.singletonList(documents.get(0));
        }
    }
}

1.3.6 MainApplication

package org.feng;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.web.bind.annotation.CrossOrigin;
/**
 * 启动类
 *
 * @author pine
 * @version v1.0
 * @since 2025-08-12 20:23
 */
@SpringBootApplication
@CrossOrigin(
        origins = "*",
        allowedHeaders = "*",
        exposedHeaders = {"Cache-Control", "Connection"}  // 暴露必要头
)
@ConfigurationPropertiesScan
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(MainApplication.class);
        springApplication.setWebApplicationType(WebApplicationType.REACTIVE);
        springApplication.run(args);
    }
}

1.4 完整配置

1.4.1 application.yaml

server:
  port: 8181
  tomcat:
    uri-encoding: UTF-8
    keep-alive-timeout: 30000
    max-connections: 100
  servlet:
    encoding:
      charset: UTF-8
      force: true
      enabled: true
  compression:
    enabled: false # 禁用压缩(否则流式数据可能被缓冲)

spring:
  main:
    allow-bean-definition-overriding: true
    web-application-type: reactive
    banner-mode: console
  application:
    name: spring-ai-chat-demo

  ai:
    # 配置阿里的密钥,模型
    dashscope:
      api-key: sk#你自己的key
      chat:
        options:
          model: qwen-plus
    # 向量数据库
    vectorstore:
      qdrant:
        host: localhost
        port: 6334
        collection-name: vector-store
        use-tls: false
        initialize-schema: true

# 日志增强
logging:
  level:
    org:
      springframework:
        ai:
          chat:
            client:
              advisor: DEBUG

1.5 调用效果

使用get请求,进行聊天对话。
在这里插入图片描述

二、附录

2.1 参考文档

  • https://docs.spring.io/spring-ai/reference/api/chatclient.html
  • https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html
  • https://docs.spring.io/spring-ai/reference/api/vectordbs/qdrant.html
  • https://springdoc.cn/spring-ai/index.html

2.2 建议

https://element-plus-x.com/zh/

前端页面可以采取 element-plus-x 来实现。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐