SpringAI 使用通义千问进行聊天对话开发

前言

最近学习了下使用 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 来实现。

### 如何将Spring AI与通义进行集成 为了使Java应用程序能够利用通义的强大能力,开发者可以借助于Spring生态系统的扩展——Spring AI来完成这一目标。Spring AI提供了一套简洁而强大的工具集,使得AI服务的接入变得异常简单[^2]。 #### 配置依赖项 首先,在`pom.xml`文件中加入必要的依赖库以便支持Spring AI以及HTTP请求等功能: ```xml <dependencies> <!-- Spring AI --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>${spring-ai.version}</version> </dependency> <!-- Web client for making API calls --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- Other dependencies as needed... --> </dependencies> ``` #### 创建API客户端类 接着定义一个用于访通义的服务接口,并实现具体的调用方法。这里假设已经获取到了合法有效的API密钥和URL地址。 ```java @Service public class QwenApiClient { private final WebClient webClient; @Autowired public QwenApiClient(WebClient.Builder builder, @Value("${qwen.api.url}") String apiUrl, @Value("${qwen.api.key}") String apiKey){ this.webClient = builder.baseUrl(apiUrl).defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer "+apiKey).build(); } /** * 发送对话通义并接收回复. */ public Mono<String> sendMessage(String message){ return webClient.post() .uri("/v1/chat/completions") .body(BodyInserters.fromValue(Map.of( "messages", List.of(Map.of("role","user","content",message)) ))) .retrieve().bodyToMono(new ParameterizedTypeReference<Map<String,Object>>(){}) .map(response -> (String)((List<?>)response.get("choices")).get(0).get("text")); } } ``` 上述代码片段展示了如何创建一个名为`QwenApiClient`的服务组件,该组件负责向通义发送消息并通过异步的方式等待响应结果返回。注意这里的具体路径(如`/v1/chat/completions`)、参数名称可能会有所不同,请参照官方文档调整相应部分[^3]。 #### 使用示例 最后可以在控制器或者其他地方注入这个客户端实例来进行实际交互操作: ```java @RestController @RequestMapping("/chatbot") class ChatBotController { private final QwenApiClient qwenApi; @GetMapping("/{input}") public Mono<ResponseEntity<?>> chat(@PathVariable String input) { return qwenApi.sendMessage(input) .map(ResponseEntity::ok) .defaultIfEmpty(ResponseEntity.status(HttpStatus.NOT_FOUND).body("No response from model.")); } } ``` 这样就完成了基本的功能搭建,当用户通过GET请求访指定端点时,服务器会转发用户的输入到通义处处理,并把得到的回答作为回应传回前端显示出来[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你家宝宝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值