【从零搭建】SpringAI Alibaba + RAG + Milvus + Qwen 项目实战

目录

一、大模型爆发后的新瓶颈

二、玩转阿里云百炼与云模型调试

[2.1 初识阿里云百炼-你的云端模型炼丹炉](#2.1 初识阿里云百炼-你的云端模型炼丹炉)

阿里云百炼是什么?

访问阿里云百炼

体验云模型

[获取你的神兵利器 - API Key](#获取你的神兵利器 - API Key)

[2.2 理解通讯协议-大模型OpenAI接口规范](#2.2 理解通讯协议-大模型OpenAI接口规范)

[OpenAI API 接口规范](#OpenAI API 接口规范)

JSON格式

流式支持

函数调用

[API 基础 URL](#API 基础 URL)

认证方式

三、掌握使用Ollama本地部署大模型Qwen3

[3.1 讲解Ollama框架及其安装](#3.1 讲解Ollama框架及其安装)

Ollama简介

安装

[3.2 如何召唤你的第一个本地模型-qwen3](#3.2 如何召唤你的第一个本地模型-qwen3)

下载并运行qwen3:1.7b模型

[四、带你快速入门SpringAI Alibaba](#四、带你快速入门SpringAI Alibaba)

[4.1 讲解SpringAI Alibaba及其核心价值和与SpringAI的区别](#4.1 讲解SpringAI Alibaba及其核心价值和与SpringAI的区别)

[SpringAI Alibaba简介](#SpringAI Alibaba简介)

核心价值

[SpringAI Alibaba官方文档](#SpringAI Alibaba官方文档)

与SpringAI的区别

[4.2 SpringAI Alibaba如何配置你的双模型战略](#4.2 SpringAI Alibaba如何配置你的双模型战略)

[如何在Spring Boot中配置云端和本地大模型](#如何在Spring Boot中配置云端和本地大模型)

创建SpringBoot工程

添加依赖

[配置多模型 application.yml](#配置多模型 application.yml)

配置ChatClient

[4.3 编写第一个使用SpringAI Alibaba进行同步AI对话的接口](#4.3 编写第一个使用SpringAI Alibaba进行同步AI对话的接口)

[编写第一个AI对话程序(阿里云百炼云模型 + ollama本地大模型)](#编写第一个AI对话程序(阿里云百炼云模型 + ollama本地大模型))

添加日志记录

[4.4 感受SpringAI Alibaba双模型一字一句的回应(流式输出)](#4.4 感受SpringAI Alibaba双模型一字一句的回应(流式输出))

[五、提示词工程和SpringAI Alibaba进阶](#五、提示词工程和SpringAI Alibaba进阶)

[5.1 如何使用SpringAI Alibaba为你的AI赋予默认人设](#5.1 如何使用SpringAI Alibaba为你的AI赋予默认人设)

提示词工程概念

系统提示词配置

[5.2 SpringAI Alibaba集成Redis让AI记住上下文-上](#5.2 SpringAI Alibaba集成Redis让AI记住上下文-上)

为什么要记住上下文?

实现方案

pom依赖

yml配置文件

配置类

ChatConfig

RedisSessionController

[5.3 SpringAI Alibaba集成Redis让AI记住上下文-下](#5.3 SpringAI Alibaba集成Redis让AI记住上下文-下)

获取指定会话记忆

清除指定会话记忆

获取指定用户所有会话ID

[六、使用SpringAI Alibaba Tools实现智能体](#六、使用SpringAI Alibaba Tools实现智能体)

[6.1 给你的AI装上眼睛-实现课程查询智能助手-上](#6.1 给你的AI装上眼睛-实现课程查询智能助手-上)

课程查询案例

代码实例

[6.2 给你的AI装上眼睛-实现课程查询智能助手-下](#6.2 给你的AI装上眼睛-实现课程查询智能助手-下)

[6.3 给AI装上双手-实现一对一辅导智能预约助手](#6.3 给AI装上双手-实现一对一辅导智能预约助手)

[七、SpringAI Alibaba+RAG+Milvus不迷路](#七、SpringAI Alibaba+RAG+Milvus不迷路)

[7.1 AI技术困局-为什么传统技术跟不上AI的脑回路](#7.1 AI技术困局-为什么传统技术跟不上AI的脑回路)

[7.2 什么是RAG](#7.2 什么是RAG)

[7.3 你知道Embedding吗 为什么选择Qwen-embedding-v4](#7.3 你知道Embedding吗 为什么选择Qwen-embedding-v4)

核心概念

百炼平台模型选择

测试用例

高频面试题

[7.4 向量数据库到底怎么选 Milvus又该怎么部署](#7.4 向量数据库到底怎么选 Milvus又该怎么部署)

怎么选?

Milvus核心概念:Collection、Partition、Entity、Field的"四世同堂"

Docker安装:你的代码"集装箱"

Milvus单机版启动:一行命令,立等可取

[下载官方 docker-compose.yml](#下载官方 docker-compose.yml)

[离线版 docker-compose.yml](#离线版 docker-compose.yml)

一键启动

关键端口说明

[可视化工具:Attu - 数据库的"美颜相机"](#可视化工具:Attu - 数据库的“美颜相机”)

[7.5 高频面试题](#7.5 高频面试题)

[7.6 SpringAI Alibaba一键接入Milvus实现RAG-上](#7.6 SpringAI Alibaba一键接入Milvus实现RAG-上)

[7.7 SpringAI Alibaba一键接入Milvus实现RAG-下](#7.7 SpringAI Alibaba一键接入Milvus实现RAG-下)

[7.8 玩转RAG进阶-构建知识库让AI读懂PDF和MD](#7.8 玩转RAG进阶-构建知识库让AI读懂PDF和MD)


一、大模型爆发后的新瓶颈

  • ChatGPT等大模型虽然"聪明"

    • 但有"金鱼记忆"(知识截止)

    • "幻觉问题"(胡说八道)

  • 企业需要AI理解私有知识

    • 公司文档

    • 产品资料

    • 客户数据

  • 传统搜索引擎只能关键词匹配,无法语义理解

二、玩转阿里云百炼与云模型调试

2.1 初识阿里云百炼-你的云端模型炼丹炉

阿里云百炼是什么?

  • 阿里云提供的一站式大模型服务平台

  • 你可以理解为:

    • 一个汇集了多种顶级大模型的"模型超市"和"调优工厂"
  • 核心价值:

    • 免运维

    • 易集成

    • 提供丰富的工具链

      • 提示词工程

      • 评估

      • 部署

    • 在本课程中我们主要使用其 "模型服务" 能力、API文档参考

访问阿里云百炼

https://bailian.console.aliyun.com/cn-beijing/#/homehttps://bailian.console.aliyun.com/cn-beijing/#/home

体验云模型

获取你的神兵利器 - API Key

2.2 理解通讯协议-大模型OpenAI接口规范

OpenAI API 接口规范

  • RESTful API:

    • 基于HTTPS的RESTful架构。
  • 资源导向: 核心资源

    • ChatCompletion

    • Completion

    • Embedding

JSON格式

请求和响应体均为JSON

  • 请求体

    curl -X POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
    -H "Authorization: Bearer $DASHSCOPE_API_KEY"
    -H "Content-Type: application/json"
    -d '{
    "model": "qwen-plus",
    "messages": [
    {
    "role": "system",
    "content": "You are a helpful assistant."
    },
    {
    "role": "user",
    "content": "你是谁?"
    }
    ]
    }'

  • 响应体

    {
    "choices": [
    {
    "message": {
    "role": "assistant",
    "content": "我是阿里云开发的一款超大规模语言模型,我叫通义千问。"
    },
    "finish_reason": "stop",
    "index": 0,
    "logprobs": null
    }
    ],
    "object": "chat.completion",
    "usage": {
    "prompt_tokens": 3019,
    "completion_tokens": 104,
    "total_tokens": 3123,
    "prompt_tokens_details": {
    "cached_tokens": 2048
    }
    },
    "created": 1735120033,
    "system_fingerprint": null,
    "model": "qwen-plus",
    "id": "chatcmpl-6ada9ed2-7f33-9de2-8bb0-78bd4035025a"
    }

流式支持

  • 对文本生成类接口支持服务器发送事件流式传输

函数调用

  • 原生支持模型决定调用用户定义函数的能力

API 基础 URL

复制代码
POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions

认证方式

Bearer Token,使用在OpenAI平台获取的API密钥

复制代码
Authorization: Bearer sk-xxx...xxx

三、掌握使用Ollama本地部署大模型Qwen3

3.1 讲解Ollama框架及其安装

Ollama简介

  • Ollama是一个用于在本地运行、管理和发布大模型的开源框架

  • 它让下载和Qwen等模型变得像 docker run 一样简单

安装

验证安装:

  • 打开终端

  • 输入 ollama --version

3.2 如何召唤你的第一个本地模型-qwen3

下载并运行qwen3:1.7b模型

  • Ollama官网搜索模型
  • 直接点击复制

  • 在终端中执行一条命令:

    ollama run qwen3:1.7b

  • Ollama会自动下载这个约1.4GB(Size)参数的模型

    • 喝杯咖啡,等待下载完成
  • 下载完成Ollama会自动运行模型服务

  • 也可以通过Ollama自带的客户端下载启动
    • 右键Ollama任务图表:open Ollama

    • 搜索需要的模型,直接下载!

测试本地模型

  • 两种测试方式:

    • ollama run 的交互界面中

      • 直接输入信息,看它的回复
    • Ollama客户端界面中

      • 直接输入信息,看它的回复

四、带你快速入门SpringAI Alibaba

4.1 讲解SpringAI Alibaba及其核心价值和与SpringAI的区别

SpringAI Alibaba简介

  • 它是Spring AI项目的一个实现

  • 由阿里云贡献

  • 深度集成阿里云百炼(DashScope)

  • 并支持OpenAI兼容接口

核心价值

  • 提供统一的

    • AiClient

    • AiStreamClient

    • ChatClient 等接口

    • 通过配置切换模型

      • 极大降低集成复杂度

SpringAI Alibaba官方文档

与SpringAI的区别

  • SpringAI

    • 是国外AI接口标准

    • 百炼和其他国内AI不是完全兼容

    • 通过SpringAI访问国内模型会有诸多麻烦

  • SpringAI Alibaba

    • 不仅兼容SpringAI的标准

    • 阿里云百炼所有AI完美兼容

    • 方便快捷

4.2 SpringAI Alibaba如何配置你的双模型战略

如何在Spring Boot中配置云端和本地大模型

创建SpringBoot工程
添加依赖
XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.clay</groupId>
    <artifactId>SpringAI-Alibaba-RAG-Milvus</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringAI-Alibaba-RAG-Milvus</name>
    <description>SpringAI-Alibaba-RAG-Milvus</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <!--
       Spring AI Alibaba	Spring AI	Spring Boot
       1.0.0.2	            1.0.0	    3.4.5
       -->
    <properties>
        <java.version>21</java.version>
        <spring-ai.version>1.0.0</spring-ai.version>
        <spring-ai-alibaba.version>1.0.0.2</spring-ai-alibaba.version>
        <spring-boot.version>3.4.5</spring-boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud.ai</groupId>
                <artifactId>spring-ai-alibaba-bom</artifactId>
                <version>${spring-ai-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <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>

        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-ollama</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-memory-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>5.2.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
配置多模型 application.yml
XML 复制代码
spring:
  application:
    name: SpringAIAlibaba-RAG-Milvus
  # DashScope 配置
  ai:
    dashscope:
      api-key: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      chat:
        model: qwen-plus
        options:
          temperature: 0.7
    # Ollama 配置
    ollama:
      enabled: true
      base-url: http://localhost:11434
      chat:
        model: qwen3:1.7b
        options:
          temperature: 0.7
# 服务器端口
server:
  port: 8080
配置ChatClient
java 复制代码
package com.clay.springaialibabaragmilvus.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * ChatClient 配置类
 */
@Configuration
public class ChatConfig {

    @Bean
    public ChatClient dashscopeChatClient(@Qualifier("dashscopeChatModel") ChatModel dashscopeChatModel) {
        return ChatClient.builder(dashscopeChatModel)
                .build();
    }

    @Bean
    public ChatClient ollamaChatClient(@Qualifier("ollamaChatModel") ChatModel ollamaChatModel) {
        return ChatClient.builder(ollamaChatModel)
                .build();
    }
}

4.3 编写第一个使用SpringAI Alibaba进行同步AI对话的接口

编写第一个AI对话程序(阿里云百炼云模型 + ollama本地大模型)

java 复制代码
package com.clay.springaialibabaragmilvus.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import jakarta.servlet.http.HttpServletResponse;

/**
 * 简单的AI控制器
 */
@RestController
@RequestMapping("/api/ai")
@RequiredArgsConstructor
public class SimpleAiController {

    // 阿里云百炼模型
    private final ChatClient dashscopeChatClient;
    // 本地大模型
    private final ChatClient ollamaChatClient;

    /**
     * 同步响应
     */
    @GetMapping(value = "/simple/chat", produces = "text/html;charset=utf-8")
    public String simpleChat(@RequestParam("question") String question) {
        // response.setCharacterEncoding("utf-8");
        return dashscopeChatClient.prompt(question).call().content();
    }

    /**
     * Ollama
     */
    @GetMapping(value = "/ollamaSimpleChat/chat", produces = "text/html;charset=utf-8")
    public String ollamaSimpleChat(@RequestParam("question") String question, HttpServletResponse response) {
        return ollamaChatClient.prompt(question).call().content();
    }
}

添加日志记录

使用SpringAI内置的 Advisor 为AI对话添加日志记录。

  • Advisor模式:

    • SpringAI中的拦截器模式

    • 可在AI请求前后自动执行特定逻辑(如日志、监控、审计)。

4.4 感受SpringAI Alibaba双模型一字一句的回应(流式输出)

java 复制代码
    /**
     * 流式聊天接口
     */
    @GetMapping(value = "/stream/chat", produces = "text/html;charset=utf-8")
    public Flux<String> streamChat(@RequestParam("question") String question, HttpServletResponse response) {
        return dashscopeChatClient.prompt(question).stream().content();
    }

    /**
     * Ollama 流式聊天接口
     */
    @GetMapping(value = "/ollamaStreamChat/chat", produces = "text/html;charset=utf-8")
    public Flux<String> ollamaStreamChat(@RequestParam("question") String question, HttpServletResponse response) {
        return ollamaChatClient.prompt(question).stream().content();
    }

五、提示词工程和SpringAI Alibaba进阶

5.1 如何使用SpringAI Alibaba为你的AI赋予默认人设

提示词工程概念

java 复制代码
1. **身份设定** (Role/Identity)
   - "你是一位资深软件架构师"
   - "你是一个10年经验的营销专家"

2. **任务目标** (Task Objective)
   - "请分析以下代码的性能瓶颈"
   - "为新产品设计营销方案"

3. **上下文信息** (Context)
   - 背景知识
   - 相关数据
   - 用户信息

4. **指令说明** (Instructions)
   - 具体步骤
   - 注意事项
   - 处理逻辑

5. **输出格式** (Output Format)
   - JSON/XML/表格
   - Markdown格式
   - 分点/分段要求

6. **约束条件** (Constraints)
   - 字数限制
   - 语言风格
   - 禁止内容

7. **示例参考** (Examples)
   - 输入-输出范例

系统提示词配置

每次通过这个客户端发起对话,AI都会默认扮演这个角色

java 复制代码
package com.clay.springaialibabaragmilvus.config;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * ChatClient 配置类
 */
@Configuration
public class ChatConfig {

    private static final String DEFAULT_PROMPT = """
            你是一个专业、耐心且富有启发性的 AI 学习助手,名字叫CLAY。
            你的目标是帮助用户深入理解知识点,而不仅仅是提供标准答案。
                    请遵循以下原则进行回复:
                    1. 角色设定:你是用户的导师,用鼓励性的语气交流。
                    2. 循序渐进:如果用户问一个复杂问题,先解释基础概念,再逐步深入。
                    3. 举例说明:尽量使用生活中的例子或代码示例来解释抽象概念。
                    4. 启发思考:在给出答案后,提出一个相关的问题,引导用户进一步思考。
                    5. 结构清晰:使用 Markdown 格式(如标题、列表、加粗)使内容易于阅读。
                    6. 准确严谨:确保提供的信息是最新且准确的,如果遇到不确定的领域,请诚实告知。
            """;

    @Bean
    public ChatClient dashscopeChatClient(@Qualifier("dashscopeChatModel") ChatModel dashscopeChatModel) {
        return ChatClient.builder(dashscopeChatModel)
                //覆盖配置文件配置
                .defaultSystem(DEFAULT_PROMPT)
                .defaultOptions(DashScopeChatOptions.builder()
                        //指定模型 优先级高于 配置文件
                        .withModel("qwen-plus")
                        //指定温度 优先级高于 配置文件
                        .withTemperature(0.7)
                        .build())
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }

    @Bean
    public ChatClient ollamaChatClient(@Qualifier("ollamaChatModel") ChatModel ollamaChatModel) {
        return ChatClient.builder(ollamaChatModel)
                .defaultSystem("你是一个博学的本地大模型,名字叫CLAY")
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}

也可以在controller中构建Prompt-优先级更高

java 复制代码
    /**
     * 带系统提示词的聊天接口
     */
    @GetMapping(value = "/session/chat-with-prompt", produces = "text/html;charset=utf-8")
    public Flux<String> chatWithSystemPrompt(
            @RequestParam("question") String question) {

        Prompt prompt = new Prompt(List.of(
                new SystemMessage("""
                    你是一个专业、耐心且富有启发性的 AI 学习助手,名字叫CLAY。
                    你的目标是帮助用户深入理解知识点,而不仅仅是提供标准答案。
                    请遵循以下原则进行回复:
                    1. 角色设定:你是用户的导师,用鼓励性的语气交流。
                    2. 循序渐进:如果用户问一个复杂问题,先解释基础概念,再逐步深入。
                    3. 举例说明:尽量使用生活中的例子或代码示例来解释抽象概念。
                    4. 启发思考:在给出答案后,提出一个相关的问题,引导用户进一步思考。
                    5. 结构清晰:使用 Markdown 格式(如标题、列表、加粗)使内容易于阅读。
                    6. 准确严谨:确保提供的信息是最新且准确的,如果遇到不确定的领域,请诚实告知。
                    """),
                new UserMessage(question)
        ));

        // 使用构造好的 Prompt
        return dashscopeChatClient.prompt(prompt).stream().content();
        //使用配置中的系统提示词
//        return dashscopeChatClient.prompt(question).stream().content();

    }

5.2 SpringAI Alibaba集成Redis让AI记住上下文-上

为什么要记住上下文?

首先,我们要知道一点,大模型本身是无状态的,也就是无法记住之前的对话内容。比如我们跟大模型说我现有10个苹果,给了弟弟5个,还剩多少个,这时大模型肯定回复还剩5个。但如果你再问,我把剩下的苹果再给妹妹2个,还剩几个,大模型肯定就不知道了,因为他根本不记得第一个问题是什么。

我们这里使用Redis进行持久化存储上下文。

实现方案

pom依赖
XML 复制代码
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-memory-redis</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>5.2.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yml配置文件
XML 复制代码
    # Redis 配置(用于内存管理)
    memory:
      redis:
        host: localhost
        port: 6379
        password: ""
        timeout: 5000
配置类
java 复制代码
package com.clay.springaialibabaragmilvus.config;

import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Redis 记忆配置类 - 用于 Spring AI Alibaba 的会话记忆功能
 */
@Configuration
public class RedisMemoryConfig {

    @Value("${spring.ai.memory.redis.host:localhost}")
    private String redisHost;

    @Value("${spring.ai.memory.redis.port:6379}")
    private int redisPort;

    @Value("${spring.ai.memory.redis.password:}")
    private String redisPassword;

    @Value("${spring.ai.memory.redis.timeout:5000}")
    private int redisTimeout;

    /**
     * Redis 聊天记忆仓库配置

     */
    @Bean
    public RedisChatMemoryRepository redisChatMemoryRepository() {
        return RedisChatMemoryRepository.builder()
                .host(redisHost)
                .port(redisPort)
                .password(redisPassword)
                .timeout(redisTimeout)
                .build();
    }
}
ChatConfig
java 复制代码
package com.clay.springaialibabaragmilvus.config;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository;
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.MessageWindowChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * ChatClient 配置类
 */
@Configuration
public class ChatConfig {

    private static final String DEFAULT_PROMPT = """
            你是一个专业、耐心且富有启发性的 AI 学习助手,名字叫CLAY。
            你的目标是帮助用户深入理解知识点,而不仅仅是提供标准答案。
                    请遵循以下原则进行回复:
                    1. 角色设定:你是用户的导师,用鼓励性的语气交流。
                    2. 循序渐进:如果用户问一个复杂问题,先解释基础概念,再逐步深入。
                    3. 举例说明:尽量使用生活中的例子或代码示例来解释抽象概念。
                    4. 启发思考:在给出答案后,提出一个相关的问题,引导用户进一步思考。
                    5. 结构清晰:使用 Markdown 格式(如标题、列表、加粗)使内容易于阅读。
                    6. 准确严谨:确保提供的信息是最新且准确的,如果遇到不确定的领域,请诚实告知。
            """;

    @Bean
    public ChatClient dashscopeChatClient(@Qualifier("dashscopeChatModel") ChatModel dashscopeChatModel, RedisChatMemoryRepository redisChatMemoryRepository) {
        return ChatClient.builder(dashscopeChatModel)
                //覆盖配置文件配置
                .defaultSystem(DEFAULT_PROMPT)
                .defaultOptions(DashScopeChatOptions.builder()
                        //指定模型 优先级高于 配置文件
                        .withModel("qwen-plus")
                        //指定温度 优先级高于 配置文件
                        .withTemperature(0.7)
                        .build())
                .defaultAdvisors(
                        // 添加日志顾问
                        new SimpleLoggerAdvisor(),
                        // 添加聊天记忆顾问
                        MessageChatMemoryAdvisor.builder(
                                // 指定聊天记忆
                                MessageWindowChatMemory.builder()
                                        //指定聊天记忆仓库
                                        .chatMemoryRepository(redisChatMemoryRepository)
                                        //指定最大消息数
                                        .maxMessages(50)
                                        .build()).build()
                )
                .build();
    }

    @Bean
    public ChatClient ollamaChatClient(@Qualifier("ollamaChatModel") ChatModel ollamaChatModel) {
        return ChatClient.builder(ollamaChatModel)
                .defaultSystem("你是一个博学的本地大模型,名字叫CLAY")
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}
RedisSessionController
java 复制代码
package com.clay.springaialibabaragmilvus.controller;

import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

import java.util.List;

import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;

/**
 * Redis 会话记忆控制器
 */
@RestController
@RequestMapping("/api/redis-session")
@RequiredArgsConstructor
public class RedisSessionController {

    private final ChatClient dashscopeChatClient;
    private final RedisChatMemoryRepository redisChatMemoryRepository;

    private final RedisTemplate redisTemplate;


    /**
     * 带会话记忆的聊天接口
     */
    @GetMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> chatWithMemory(
            @RequestParam("question") String question,
            @RequestParam(value = "sessionId", defaultValue = "student_session") String sessionId,
            @RequestParam(value = "userId", defaultValue = "default_userId") String userId) {

        // 将 userId 和 sessionId 拼接作为 Redis key
        String redisKey = userId + ":" + sessionId;

        MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
                .chatMemoryRepository(redisChatMemoryRepository)
                .maxMessages(Integer.MAX_VALUE)
                .build();
        // 如果是新的会话 将 sessionId 存入 Redis list类型 userId为key sessionId为值
        if (messageWindowChatMemory.get(redisKey).size() <1){
            redisTemplate.setKeySerializer(RedisSerializer.string());
            redisTemplate.setValueSerializer(RedisSerializer.json());
            BoundListOperations boundListOperations = redisTemplate.boundListOps("history:"+userId);
            boundListOperations.leftPush(sessionId);
        }

        // 使用已配置的 dashscopeChatClient,Redis 记忆已在配置中启用
        return dashscopeChatClient.prompt(question)
                .advisors(
                        a -> a.param(CONVERSATION_ID, redisKey)
                )
                .stream().content();
    }

}

5.3 SpringAI Alibaba集成Redis让AI记住上下文-下

获取指定会话记忆

java 复制代码
/**
 * 获取指定会话的历史消息
 */
@GetMapping("/history/{userId}/{sessionId}")
public List<Message> getSessionHistory(
        @PathVariable String userId,
        @PathVariable String sessionId) {

    String redisKey = userId + ":" + sessionId;

    MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
            .chatMemoryRepository(redisChatMemoryRepository)
            .maxMessages(Integer.MAX_VALUE)
            .build();

    return messageWindowChatMemory.get(redisKey);
}

清除指定会话记忆

java 复制代码
/**
 * 清除指定会话的记忆
 */
@DeleteMapping("/clear/{userId}/{sessionId}")
public String clearSession(
        @PathVariable String userId, 
        @PathVariable String sessionId) {
    
    String redisKey = userId + ":" + sessionId;
    
    MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
            .chatMemoryRepository(redisChatMemoryRepository)
            .maxMessages(Integer.MAX_VALUE)
            .build();
    
    messageWindowChatMemory.clear(redisKey);
    
    // 从 Redis 会话列表中删除对应的 sessionId
    redisTemplate.setKeySerializer(RedisSerializer.string());
    redisTemplate.setValueSerializer(RedisSerializer.json());
    BoundListOperations<String, String> boundListOperations = redisTemplate.boundListOps("history:" + userId);
    boundListOperations.remove(0, sessionId);
    
    return "用户 " + userId + " 的会话 " + sessionId + " 记忆已清除";
}   

获取指定用户所有会话ID

java 复制代码
/**
 * 获取指定用户的所有会话ID
 */
@GetMapping("/sessions/{userId}")
public List<String> getUserSessions(@PathVariable String userId) {
    redisTemplate.setKeySerializer(RedisSerializer.string());
    redisTemplate.setValueSerializer(RedisSerializer.json());
    BoundListOperations<String, String> boundListOperations = redisTemplate.boundListOps("history:"+userId);
    return boundListOperations.range(0, -1);
}

六、使用SpringAI Alibaba Tools实现智能体

6.1 给你的AI装上眼睛-实现课程查询智能助手-上

课程查询案例

  • 场景

    • 用户对AI说"你们有哪些课程?"
  • 目标

    • AI自动理解意图,并调用后端服务操作数据库,完成查询

代码实例

添加mybatis-plus依赖

XML 复制代码
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.7</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>

yml配置

XML 复制代码
spring:
  application:
    name: SpringAIAlibaba-RAG-Milvus

  ai:
    dashscope:
      api-key: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      chat:
        model: qwen-plus
        options:
          temperature: 0.7

    ollama:
      enabled: true
      base-url: http://localhost:11434
      chat:
        model: qwen3:1.7b
        options:
          temperature: 0.7

    # Redis 配置(用于内存管理)
    memory:
      redis:
        host: localhost
        port: 6379
        password: ""
        timeout: 5000
  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/learn_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

  # MyBatis Plus 配置
  mybatis-plus:
    configuration:
      map-underscore-to-camel-case: true
      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

logging:
  level:
    org.springframework.ai: debug
    com.clay: debug

server:
  port: 8080

表创建

sql 复制代码
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(100) NOT NULL COMMENT '用户名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

INSERT INTO `user` VALUES
(1,'admin'),
(2,'teacher1'),
(3,'teacher2'),
(4,'teacher3');

-- ----------------------------
-- Table structure for course_type
-- ----------------------------
DROP TABLE IF EXISTS `course_type`;
CREATE TABLE `course_type` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '课程类型ID',
  `name` varchar(100) NOT NULL COMMENT '课程类型名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程类型表';

INSERT INTO `course_type` VALUES
(1,'编程语言'),
(2,'数据库'),
(3,'前端开发'),
(4,'后端开发'),
(5,'移动开发'),
(6,'人工智能');

-- ----------------------------
-- Table structure for course
-- ----------------------------
DROP TABLE IF EXISTS `course`;
CREATE TABLE `course`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '课程ID',
  `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程标题',
  `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '课程描述',
  `cover_image` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '封面图片URL',
  `teacher_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '教师姓名',
  `price` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '价格',
  `rating` decimal(2, 1) NULL DEFAULT 0.0 COMMENT '评分',
  `student_count` bigint NULL DEFAULT 0 COMMENT '学生人数',
  `type_id` bigint NOT NULL COMMENT '课程类型ID',
  `creator_id` bigint NOT NULL COMMENT '创建者ID',
  `created_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `type_id`(`type_id` ASC) USING BTREE,
  INDEX `creator_id`(`creator_id` ASC) USING BTREE,
  CONSTRAINT `course_ibfk_1` FOREIGN KEY (`type_id`) REFERENCES `course_type` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `course_ibfk_2` FOREIGN KEY (`creator_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '课程表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of course
-- ----------------------------
INSERT INTO `course` VALUES (1, 'Java基础教程', 'Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。', 'https://example.com/java-cover.jpg', '张老师', 99.00, 4.8, 1200, 1, 1, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `course` VALUES (2, 'MySQL数据库实战', 'MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发,目前属于Oracle旗下产品。', 'https://example.com/mysql-cover.jpg', '李老师', 199.00, 4.6, 800, 2, 1, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `course` VALUES (3, 'Vue.js从入门到精通', 'Vue.js是一套用于构建用户界面的渐进式JavaScript框架。与其它大型框架不同的是,Vue被设计为可以自底向上逐层应用。', 'https://example.com/vue-cover.jpg', '王老师', 149.00, 4.7, 1500, 3, 2, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `course` VALUES (4, 'Spring Boot实战', 'Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。', 'https://example.com/springboot-cover.jpg', '赵老师', 299.00, 4.9, 900, 4, 2, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `course` VALUES (5, 'Android开发详解', 'Android是一种基于Linux的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑。', 'https://example.com/android-cover.jpg', '孙老师', 249.00, 4.5, 700, 5, 3, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `course` VALUES (6, '机器学习入门', '机器学习是人工智能的一个分支。人工智能的研究历史有着一条从以"推理"为重点,到以"知识"为重点,再到以"学习"为重点的自然、清晰的脉络。', 'https://example.com/ml-cover.jpg', '周老师', 399.00, 4.8, 1100, 6, 4, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `course` VALUES (7, 'Python高级编程', '深入学习Python高级特性,包括装饰器、生成器、元类、并发编程等', 'https://example.com/python-advanced-cover.jpg', '陈老师', 159.00, 4.7, 1300, 1, 1, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `course` VALUES (8, 'PostgreSQL数据库优化', 'PostgreSQL数据库的高级使用技巧和性能优化方法', 'https://example.com/postgresql-cover.jpg', '刘老师', 189.00, 4.6, 750, 2, 2, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `course` VALUES (9, 'React全家桶实战', 'React核心概念及周边生态系统的综合应用', 'https://example.com/react-cover.jpg', '杨老师', 179.00, 4.8, 1600, 3, 3, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `course` VALUES (10, '微服务架构设计', '微服务架构的设计原则、实现方式和最佳实践', 'https://example.com/microservices-cover.jpg', '黄老师', 269.00, 4.9, 950, 4, 4, '2025-12-19 20:23:32', '2025-12-19 20:23:32');

-- ----------------------------
-- Table structure for tutor
-- ----------------------------
DROP TABLE IF EXISTS `tutor`;
CREATE TABLE `tutor`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '辅导老师ID',
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '老师姓名',
  `specialty` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '专长领域',
  `introduction` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '老师简介',
  `avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像URL',
  `hourly_rate` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '每小时费用',
  `rating` decimal(2, 1) NULL DEFAULT 0.0 COMMENT '评分',
  `student_count` bigint NULL DEFAULT 0 COMMENT '辅导学生数',
  `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(1:可用 0:不可用)',
  `created_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '辅导老师表' ROW_FORMAT = Dynamic;

INSERT INTO `tutor` VALUES (1, '张老师', 'Java全栈开发', '拥有10年Java开发经验,擅长Spring生态和微服务架构', 'https://example.com/tutor-zhang-avatar.jpg', 200.00, 4.9, 150, 1, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `tutor` VALUES (2, '李老师', '数据库专家', '资深DBA,精通MySQL、PostgreSQL等关系型数据库优化', 'https://example.com/tutor-li-avatar.jpg', 180.00, 4.8, 120, 1, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `tutor` VALUES (3, '王老师', '前端架构师', 'Vue.js核心贡献者,专注于前端工程化和性能优化', 'https://example.com/tutor-wang-avatar.jpg', 220.00, 4.9, 200, 1, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `tutor` VALUES (4, '赵老师', 'Python数据科学', '数据科学家,擅长机器学习和数据分析', 'https://example.com/tutor-zhao-avatar.jpg', 250.00, 4.7, 80, 1, '2025-12-19 20:23:32', '2025-12-19 20:23:32');
INSERT INTO `tutor` VALUES (5, '陈老师', 'Android高级开发', 'Android开发专家,熟悉各种移动开发技术', 'https://example.com/tutor-chen-avatar.jpg', 190.00, 4.8, 100, 1, '2025-12-19 20:23:32', '2025-12-19 20:23:32');

-- ----------------------------
-- Table structure for appointment
-- ----------------------------
DROP TABLE IF EXISTS `appointment`;
CREATE TABLE `appointment`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '预约ID',
  `student_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '学生名称',
  `tutoring_content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '辅导内容',
  `tutor_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '老师名称',
  `phone_number` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '手机号码',
  `notes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '预约备注',
  `created_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '预约表' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

实体类

java 复制代码
package com.oracle.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("appointment")
public class Appointment {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String studentName;
    
    private String tutoringContent;
    
    private String tutorName;
    
    private String phoneNumber;
    
    private String notes;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedTime;
}



package com.oracle.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("course")
public class Course {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String title;
    
    private String description;
    
    private String coverImage;
    
    private String teacherName;
    
    private BigDecimal price;
    
    private BigDecimal rating;
    
    private Long studentCount;
    
    private Long typeId;
    
    private Long creatorId;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedTime;
}






package com.oracle.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("tutor")
public class Tutor {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String name;
    
    private String specialty;
    
    private String introduction;
    
    private String avatar;
    
    private BigDecimal hourlyRate;
    
    private BigDecimal rating;
    
    private Long studentCount;
    
    private Integer status;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedTime;
}

mapper

java 复制代码
package com.clay.springaialibabaragmilvus.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.clay.springaialibabaragmilvus.entity.Appointment;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface AppointmentMapper extends BaseMapper<Appointment> {
}
java 复制代码
package com.clay.springaialibabaragmilvus.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.clay.springaialibabaragmilvus.entity.Course;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface CourseMapper extends BaseMapper<Course> {
}
java 复制代码
package com.clay.springaialibabaragmilvus.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.clay.springaialibabaragmilvus.entity.Tutor;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface TutorMapper extends BaseMapper<Tutor> {
}

service接口

java 复制代码
import com.baomidou.mybatisplus.extension.service.IService;
import com.clay.springaialibabaragmilvus.entity.Appointment;

public interface IAppointmentService extends IService<Appointment> {
}
java 复制代码
import com.baomidou.mybatisplus.extension.service.IService;
import com.clay.springaialibabaragmilvus.entity.Course;

import java.util.List;

public interface ICourseService extends IService<Course> {

    /**
     * 搜索课程
     *
     * @param keyword 关键词
     * @param category 分类
     * @param minPrice 最低价格
     * @param maxPrice 最高价格
     * @return 课程列表
     */
    List<Course> searchCourses(String keyword, String category, Double minPrice, Double maxPrice);

}
java 复制代码
import com.baomidou.mybatisplus.extension.service.IService;
import com.clay.springaialibabaragmilvus.entity.Tutor;

public interface ITutorService extends IService<Tutor> {
}

实现类

java 复制代码
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.clay.springaialibabaragmilvus.entity.Course;
import com.clay.springaialibabaragmilvus.mapper.CourseMapper;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;

@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements ICourseService {

    @Override
    public List<Course> searchCourses(String keyword, String category, Double minPrice, Double maxPrice) {
        return lambdaQuery()
                .like(StringUtils.hasText(keyword), Course::getTitle, keyword)
                .or()
                .like(StringUtils.hasText(keyword), Course::getDescription, keyword)
                .like(StringUtils.hasText(category), Course::getTeacherName, category)
                .ge(minPrice != null, Course::getPrice, minPrice)
                .le(maxPrice != null, Course::getPrice, maxPrice)
                .orderByDesc(Course::getStudentCount)
                .list();
    }
}

6.2 给你的AI装上眼睛-实现课程查询智能助手-下

tools

java 复制代码
package com.clay.springaialibabaragmilvus.tools;

import com.clay.springaialibabaragmilvus.entity.Course;
import com.clay.springaialibabaragmilvus.service.ICourseService;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@RequiredArgsConstructor
public class LearningAssistantTools {

    private final ICourseService courseService;

    /**
     * 根据条件查询课程
     * @param keyword 课程名称
     * @return 课程信息
     */
    @Tool(description = "查询课程")
    public String searchCourses(
            @ToolParam(description = "课程名称") String keyword) {

        List<Course> courses = courseService.searchCourses(keyword, null, null, null);
        StringBuilder result = new StringBuilder("符合条件的课程:\n");

        if (courses.isEmpty()) {
            result.append("暂无符合条件的课程");
        } else {
            for (Course course : courses) {
                result.append(String.format("- %s (讲师:%s) - 评分:%s - 价格:¥%s - 学生数:%d人\n",
                        course.getTitle(), course.getTeacherName(), course.getRating(), course.getPrice(), course.getStudentCount()));
            }
        }

        return result.toString();
    }

}

controller

java 复制代码
package com.clay.springaialibabaragmilvus.controller;

import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository;
import com.clay.springaialibabaragmilvus.tools.LearningAssistantTools;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

import java.util.Map;

import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;

/**
 * 智能体控制器 - 课程查询和辅导预约
 */
@RestController
@RequestMapping("/api/agent")
@RequiredArgsConstructor
public class LearningAgentController {

    private final ChatClient dashscopeChatClient;
    private final RedisChatMemoryRepository redisChatMemoryRepository;
    private final LearningAssistantTools learningAssistantTools;
    private final RedisTemplate redisTemplate;


    /**
     * 智能体对话接口 - 处理课程查询和辅导预约请求
     */
    @GetMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> agentChat(
            @RequestParam("question") String question,
            @RequestParam(value = "sessionId", defaultValue = "agent_session") String sessionId,
            @RequestParam(value = "userId", defaultValue = "default_userId") String userId) {

        String redisKey = userId + ":" + sessionId;

        MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
                .chatMemoryRepository(redisChatMemoryRepository)
                .maxMessages(Integer.MAX_VALUE)
                .build();

        // 如果是新的会话 将 sessionId 存入 Redis list类型 userId为key sessionId为值
        if (messageWindowChatMemory.get(redisKey).size() < 1){
            redisTemplate.setKeySerializer(RedisSerializer.string());
            redisTemplate.setValueSerializer(RedisSerializer.json());
            BoundListOperations boundListOperations = redisTemplate.boundListOps("history:"+userId);
            boundListOperations.leftPush(sessionId);
        }

        // 集成学习助手工具
        return dashscopeChatClient.prompt()
                .system("""
                    你是一个智能学习助手,可以帮助用户查询课程和预约辅导老师。
                    当用户询问课程相关信息时,请使用search_courses工具进行查询,只需提供关键词参数。
                    请根据用户的具体需求选择合适的工具并提供准确的信息。
                    """)
                .user(question)
                .advisors(
                        a -> a.param(CONVERSATION_ID, redisKey)
                )
                .tools(learningAssistantTools)
                .stream().content();
    }

}

6.3 给AI装上双手-实现一对一辅导智能预约助手

service

java 复制代码
package com.clay.springaialibabaragmilvus.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.clay.springaialibabaragmilvus.entity.Appointment;

public interface IAppointmentService extends IService<Appointment> {

    /**
     * 预约课程
     * @param studentName 学生姓名
     * @param tutoringContent 辅导内容
     * @param tutorName 导师名称
     * @param phoneNumber 手机号码
     * @param notes 备注
     * @return 是否预约成功
     */
    boolean bookAppointment(String studentName, String tutoringContent, String tutorName, String phoneNumber, String notes);

}
java 复制代码
package com.clay.springaialibabaragmilvus.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.clay.springaialibabaragmilvus.entity.Appointment;
import com.clay.springaialibabaragmilvus.mapper.AppointmentMapper;
import com.clay.springaialibabaragmilvus.service.IAppointmentService;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
public class AppointmentServiceImpl extends ServiceImpl<AppointmentMapper, Appointment> implements IAppointmentService {

    @Override
    public boolean bookAppointment(String studentName, String tutoringContent, String tutorName, String phoneNumber, String notes) {
        Appointment appointment = new Appointment();
        appointment.setStudentName(studentName);
        appointment.setTutoringContent(tutoringContent);
        appointment.setTutorName(tutorName);
        appointment.setPhoneNumber(phoneNumber);
        appointment.setNotes(notes);
        appointment.setCreatedTime(LocalDateTime.now());
        appointment.setUpdatedTime(LocalDateTime.now());
        return save(appointment);
    }

}
java 复制代码
package com.clay.springaialibabaragmilvus.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.clay.springaialibabaragmilvus.entity.Tutor;

import java.util.List;

public interface ITutorService extends IService<Tutor> {
    /**
     * 获取所有可用的导师信息
     * @return
     */
    List<Tutor> getAvailableTutors();

    /**
     * 根据专业搜索导师信息
     * @param specialty
     * @return
     */
    List<Tutor> searchTutorsBySpecialty(String specialty);

}
java 复制代码
package com.clay.springaialibabaragmilvus.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.clay.springaialibabaragmilvus.entity.Tutor;
import com.clay.springaialibabaragmilvus.mapper.TutorMapper;
import com.clay.springaialibabaragmilvus.service.ITutorService;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class TutorServiceImpl extends ServiceImpl<TutorMapper, Tutor> implements ITutorService {

    @Override
    public List<Tutor> getAvailableTutors() {
        return lambdaQuery()
                .eq(Tutor::getStatus, 1)
                .orderByDesc(Tutor::getRating)
                .list();
    }

    @Override
    public List<Tutor> searchTutorsBySpecialty(String specialty) {
        return lambdaQuery()
                .eq(Tutor::getStatus, 1)
                .like(Tutor::getSpecialty, specialty)
                .orderByDesc(Tutor::getRating)
                .list();
    }
}

tools

java 复制代码
package com.clay.springaialibabaragmilvus.tools;

import com.clay.springaialibabaragmilvus.entity.Course;
import com.clay.springaialibabaragmilvus.entity.Tutor;
import com.clay.springaialibabaragmilvus.service.IAppointmentService;
import com.clay.springaialibabaragmilvus.service.ICourseService;
import com.clay.springaialibabaragmilvus.service.ITutorService;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@RequiredArgsConstructor
public class LearningAssistantTools {

    private final ICourseService courseService;
    private final ITutorService tutorService;
    private final IAppointmentService appointmentService;


    /**
     * 根据条件查询课程
     * @param keyword 课程名称
     * @return 课程信息
     */
    @Tool(description = "查询课程")
    public String searchCourses(
            @ToolParam(description = "课程名称") String keyword) {

        List<Course> courses = courseService.searchCourses(keyword, null, null, null);
        StringBuilder result = new StringBuilder("符合条件的课程:\n");

        if (courses.isEmpty()) {
            result.append("暂无符合条件的课程");
        } else {
            for (Course course : courses) {
                result.append(String.format("- %s (讲师:%s) - 评分:%s - 价格:¥%s - 学生数:%d人\n",
                        course.getTitle(), course.getTeacherName(), course.getRating(), course.getPrice(), course.getStudentCount()));
            }
        }

        return result.toString();
    }


    /**
     * 查询所有一对一的辅导老师
     * @return 辅导老师列表
     */
    @Tool(description = "获取辅导老师信息")
    public String getAllTutors() {
        List<Tutor> tutors = tutorService.getAvailableTutors();
        StringBuilder result = new StringBuilder("可用的辅导老师:\n");

        for (Tutor tutor : tutors) {
            result.append(String.format("- %s (%s) - 评分:%s - 费用:¥%s/小时\n",
                    tutor.getName(), tutor.getSpecialty(), tutor.getRating(), tutor.getHourlyRate()));
        }

        return result.toString();
    }

    /**
     * 根据专长查询辅导老师
     * @param specialty 专长领域
     * @return 辅导老师列表
     */
    @Tool(description = "查询辅导老师")
    public String searchTutorsBySpecialty(@ToolParam(description = "老师专长领域") String specialty) {
        List<Tutor> tutors = tutorService.searchTutorsBySpecialty(specialty);
        StringBuilder result = new StringBuilder("专长为\"" + specialty + "\"的辅导老师:\n");

        for (Tutor tutor : tutors) {
            result.append(String.format("- %s - 评分:%s - 费用:¥%s/小时\n",
                    tutor.getName(), tutor.getRating(), tutor.getHourlyRate()));
        }

        return result.toString();
    }

    /**
     * 预约指定辅导老师一对一辅导
     * @param studentName 学生姓名
     * @param tutoringContent 辅导内容
     * @param tutorName 老师姓名
     * @param phoneNumber 手机号码
     * @param notes 备注
     * @return 预约结果
     */
    @Tool(description = "预约一对一辅导服务")
    public String bookTutor(
            @ToolParam(description = "学生姓名") String studentName,
            @ToolParam(description = "辅导内容") String tutoringContent,
            @ToolParam(description = "老师姓名") String tutorName,
            @ToolParam(description = "手机号码") String phoneNumber,
            @ToolParam(description = "预约备注") String notes) {
        try {
            boolean success = appointmentService.bookAppointment(studentName, tutoringContent, tutorName, phoneNumber, notes);

            if (success) {
                return "预约成功!";
            } else {
                return "预约失败";
            }
        } catch (Exception e) {
            return "预约失败";
        }
    }

}

controller

java 复制代码
package com.clay.springaialibabaragmilvus.controller;

import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository;
import com.clay.springaialibabaragmilvus.tools.LearningAssistantTools;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

import java.util.Map;

import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;

/**
 * 智能体控制器 - 课程查询和辅导预约
 */
@RestController
@RequestMapping("/api/agent")
@RequiredArgsConstructor
public class LearningAgentController {

    private final ChatClient dashscopeChatClient;
    private final RedisChatMemoryRepository redisChatMemoryRepository;
    private final LearningAssistantTools learningAssistantTools;
    private final RedisTemplate redisTemplate;


    /**
     * 智能体对话接口 - 处理课程查询和辅导预约请求
     */
    @GetMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> agentChat(
            @RequestParam("question") String question,
            @RequestParam(value = "sessionId", defaultValue = "agent_session") String sessionId,
            @RequestParam(value = "userId", defaultValue = "default_userId") String userId) {

        String redisKey = userId + ":" + sessionId;

        MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
                .chatMemoryRepository(redisChatMemoryRepository)
                .maxMessages(Integer.MAX_VALUE)
                .build();

        // 如果是新的会话 将 sessionId 存入 Redis list类型 userId为key sessionId为值
        if (messageWindowChatMemory.get(redisKey).size() <1){
            redisTemplate.setKeySerializer(RedisSerializer.string());
            redisTemplate.setValueSerializer(RedisSerializer.json());
            BoundListOperations boundListOperations = redisTemplate.boundListOps("history:"+userId);
            boundListOperations.leftPush(sessionId);
        }

        // 集成学习助手工具(自动识别,可以不写提示词)
        return dashscopeChatClient.prompt()
                .system("""
                    你是一个智能学习助手,可以帮助用户查询课程和预约辅导老师。
                    当用户询问课程相关信息时,请使用search_courses工具进行查询,只需提供关键词参数。
                    当用户询问辅导老师时,请使用get_all_tutor工具。
                    当用户想要预约辅导时,请使用book_tutor工具。
                    请根据用户的具体需求选择合适的工具并提供准确的信息。
                    """)
                .user(question)
                .advisors(
                        a -> a.param(CONVERSATION_ID, redisKey)
                )
                //可以定义多个
                .tools(learningAssistantTools)
                .stream().content();
    }

}

七、SpringAI Alibaba+RAG+Milvus不迷路

7.1 AI技术困局-为什么传统技术跟不上AI的脑回路

传统技术栈的"中年危机":MySQL、Redis、Elasticsearch为何力不从心?

向量数据让AI理解"像什么"

7.2 什么是RAG

7.3 你知道Embedding吗 为什么选择Qwen-embedding-v4

核心概念

离散的文本数据 转换为连续的数值向量的数学过程。

百炼平台模型选择

测试用例

application.yml

java 复制代码
spring:
  application:
    name: SpringAIAlibaba-RAG-Milvus
  # DashScope 配置
  ai:
    dashscope:
      api-key: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
      chat:
        model: qwen-plus
        options:
          temperature: 0.7
      embedding:
        options:
          model: text-embedding-v4

测试代码

java 复制代码
package com.clay.springaialibabaragmilvus.controller;

import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;
import java.util.List;


@RestController
@RequestMapping("/embedding")
public class EmbeddingController {

    @Autowired
    private EmbeddingModel embeddingModel;

    /**
     * 生成单个文本的embedding向量
     */
    @GetMapping("/single")
    public String embedSingle(@RequestParam(defaultValue = "你好世界") String text) {
        float[] embedding = embeddingModel.embed(text);
        return String.format("向量维度: %d, 样本值: %s",
                embedding.length,
                Arrays.toString(
                        // Arrays.copyOf(embedding, Math.min(5, embedding.length))
                        Arrays.copyOf(embedding, embedding.length)

                )
        );
    }

    /**
     * 计算两个文本之间的相似度(余弦距离)
     */
    @GetMapping("/similarity")
    public String calculateSimilarity(
            @RequestParam(defaultValue = "你好世界") String text1,
            @RequestParam(defaultValue = "您好") String text2) {

        float[] vec1 = embeddingModel.embed(text1);
        float[] vec2 = embeddingModel.embed(text2);

        double similarity = calculateCosineSimilarity(vec1, vec2);

        return String.format("'%s' 与 '%s' 的相似度: %.4f", text1, text2, similarity);
    }

    /**
     * 计算两个向量的余弦相似度
     */
    private double calculateCosineSimilarity(float[] vector1, float[] vector2) {
        if (vector1.length != vector2.length) {
            throw new IllegalArgumentException("向量长度不匹配");
        }

        double dotProduct = 0.0;
        double norm1 = 0.0;
        double norm2 = 0.0;

        for (int i = 0; i < vector1.length; i++) {
            dotProduct += vector1[i] * vector2[i];
            norm1 += Math.pow(vector1[i], 2);
            norm2 += Math.pow(vector2[i], 2);
        }

        if (norm1 == 0 || norm2 == 0) {
            return 0.0; // 如果任一向量为零向量,则相似度为0
        }

        return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
    }



    /**
     * 计算两个文本之间的欧式距离
     */
    @GetMapping("/euclidean")
    public String calculateEuclideanDistance(
            @RequestParam(defaultValue = "你好世界") String text1,
            @RequestParam(defaultValue = "您好") String text2) {

        float[] vec1 = embeddingModel.embed(text1);
        float[] vec2 = embeddingModel.embed(text2);

        double euclideanDistance = calculateEuclideanDistance(vec1, vec2);

        return String.format("'%s' 与 '%s' 的欧式距离: %.4f", text1, text2, euclideanDistance);
    }

    /**
     * 计算两个向量的欧式距离
     */
    private double calculateEuclideanDistance(float[] vector1, float[] vector2) {
        if (vector1.length != vector2.length) {
            throw new IllegalArgumentException("向量长度不匹配");
        }

        double sumSquaredDifferences = 0.0;

        for (int i = 0; i < vector1.length; i++) {
            double difference = vector1[i] - vector2[i];
            sumSquaredDifferences += difference * difference;
        }

        return Math.sqrt(sumSquaredDifferences);
    }

}

高频面试题

  • Embedding向量的维度一般是多少?维度高低对搜索有什么影响?

    • 答:

      • 常见维度为384、512、768、1024、1536等

      • 维度越高,表达能力越强,但存储和计算成本也越高

      • 需要在精度和性能之间权衡

  • 相似度搜索常用的距离度量有哪些?

    • 答:

      • 余弦相似度(Cosine Similarity):

        • 衡量向量方向差异
      • 欧氏距离(Euclidean Distance):

        • 衡量向量绝对距离
      • 内积(Inner Product)

      • 不同场景选择不同度量方式。

7.4 向量数据库到底怎么选 Milvus又该怎么部署

怎么选?

Milvus核心概念:Collection、Partition、Entity、Field的"四世同堂"

Docker安装:你的代码"集装箱"

  • 为什么需要Docker?

    • 传统安装:

      • 需要手动安装依赖、配置环境、解决版本冲突...费时费力
    • Docker方式:

      • 一次配置,到处运行

      • 隔离环境,纯净无污染。

  • 一键安装Docker

    • Windows/Mac

      • 下载Docker Desktop,可视化安装,自带Kubernetes。
    • Linux:一行命令搞定:

bash 复制代码
curl -fsSL https://get.docker.com | bash -s docker
sudo systemctl start docker
sudo systemctl enable docker
  • 验证安装
bash 复制代码
docker --version
docker run hello-world  # 运行测试容器

Milvus单机版启动:一行命令,立等可取

下载官方 docker-compose.yml
bash 复制代码
# 创建项目目录
mkdir milvus-demo && cd milvus-demo

# 下载最新的docker-compose文件
wget https://github.com/milvus-io/milvus/releases/download/v2.4.0/milvus-standalone-docker-compose.yml -O docker-compose.yml
离线版 docker-compose.yml
bash 复制代码
# 创建项目目录
mkdir -p ~/my_milvus && cd ~/my_milvus

#创建文件
cat > docker-compose.yml << 'EOF'
version: '3.5'
services:
  etcd:
    image: quay.io/coreos/etcd:v3.5.18
    container_name: milvus-etcd
    environment:
      - ETCD_AUTO_COMPACTION_MODE=revision
      - ETCD_AUTO_COMPACTION_RETENTION=1000
    volumes:
      - ./etcd-data:/etcd
    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
  minio:
    image: minio/minio:RELEASE.2023-03-20T20-16-18Z
    container_name: milvus-minio
    environment:
      MINIO_ACCESS_KEY: minioadmin
      MINIO_SECRET_KEY: minioadmin
    volumes:
      - ./minio-data:/minio_data
    command: minio server /minio_data --console-address ":9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3
  standalone:
    image: milvusdb/milvus:v2.4.0-rc.1
    container_name: milvus-standalone
    command: ["milvus", "run", "standalone"]
    environment:
      ETCD_ENDPOINTS: etcd:2379
      MINIO_ADDRESS: minio:9000
    volumes:
      - ./standalone-data:/var/lib/milvus
    ports:
      - "19530:19530"
      - "9091:9091"
    depends_on:
      etcd:
        condition: service_started
      minio:
        condition: service_healthy
  attu:
    image: zilliz/attu:latest
    container_name: milvus-attu
    environment:
      MILVUS_URL: standalone:19530
    ports:
      - "8000:3000"
    depends_on:
      - standalone
EOF
一键启动
bash 复制代码
# 后台启动所有服务
docker-compose up -d

# 查看运行状态
docker-compose ps

# 应该看到4个服务运行中:
# 1. milvus-etcd (存储元数据)
# 2. milvus-minio (存储向量数据) 
# 3. milvus-standalone (主服务)
# 4. milvus-attu(可视化操作界面)
关键端口说明
  • 19530:Milvus服务端口(Java程序连接这个)

  • 9091:监控指标端口

  • 2379:etcd端口(内部使用)

  • 9000:MinIO端口(内部使用)

可视化工具:Attu - 数据库的"美颜相机"
  • 安装Milvus时自带

    • 访问与使用

    • 浏览器打开:http://localhost:8000

    • 连接Milvus:地址填localhost,端口19530

    • 零配置直接连接(单机版默认无密码)

    • Attu核心功能速览

      • Collection管理:可视化创建、删除集合

      • 数据操作:上传数据、执行搜索

      • 监控面板:查看系统状态、性能指标

      • 查询控制台:执行类SQL查询

7.5 高频面试题

  • Milvus的架构有什么特点?为什么能支持海量向量数据?

    • 答:

      • Milvus采用存储计算分离架构:

        • 对象存储(MinIO/S3)存向量数据

        • 查询节点负责计算

        • 协调服务管理元数据。

        • 这种架构易于水平扩展,支持十亿级向量

  • 为什么选择Java而不是Python开发AI应用?

    • 答:

      • Java适合构建高并发、稳定可靠的生产系统

      • JDK21虚拟线程显著提升IO密集型AI应用的性能

      • 企业现有技术栈多为Java,集成成本低

  • Milvus与其他向量数据库相比,在Java生态中的支持如何?

    • 答:

      • Milvus提供完整的Java SDK,文档详尽

        • 与Spring Boot等框架集成良好
      • 相比Chroma(Python为主)、Pinecone(HTTP API)

        • Milvus的Java支持更专业
  • Docker安装Milvus相比传统安装有什么优势?

    • 答:

      • 环境隔离,避免依赖冲突

      • 一键部署,节省配置时间

      • 版本管理方便,可快速切换版本

      • 便于团队共享和环境一致性

  • Milvus单机版启动失败,可能是什么原因?

    • 答:

      • 端口冲突(19530已被占用)

      • 内存不足(至少4GB)

      • Docker未启动

      • 磁盘空间不足

      • 可通过docker-compose logs查看具体错误。

  • Attu连接不上Milvus怎么办?

    • 答:

      • Milvus服务是否运行(docker-compose ps

      • 网络是否互通(docker network ls

      • 地址端口是否正确(localhost:19530)

      • 防火墙是否放行端口。

7.6 SpringAI Alibaba一键接入Milvus实现RAG-上

添加依赖

XML 复制代码
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-milvus-store</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-autoconfigure-vector-store-milvus</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-advisors-vector-store</artifactId>
        </dependency>

application.yml

XML 复制代码
spring:
  application:
    name: SpringAIAlibaba-RAG-Milvus

  ai:
    dashscope:
      api-key: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      chat:
        model: qwen-plus
        options:
          temperature: 0.7
      embedding:
        options:
          model: text-embedding-v4

    ollama:
      enabled: true
      base-url: http://localhost:11434
      chat:
        model: qwen3:1.7b
        options:
          temperature: 0.7

    # Redis 配置(用于内存管理)
    memory:
      redis:
        host: localhost
        port: 6379
        password: ""
        timeout: 5000

    # Milvus Vector Store 配置
    vectorstore:
      milvus:
        enabled: true  # 启用Milvus配置
        initialize-schema: true  # 启用schema初始化,自动创建集合
        client:
          host: "xxxxxxxxxxxxxxx"  # 云服务器地址
          port: 19530
          username: "minioadmin"
          password: "minioadmin"
        databaseName: "default"
        collectionName: "vector_store"
        embeddingDimension: 1024 # default: 1536
        metricType: COSINE # default: COSINE


  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/learn_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

  # MyBatis Plus 配置
  mybatis-plus:
    configuration:
      map-underscore-to-camel-case: true
      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

logging:
  level:
    org.springframework.ai: debug
    com.clay: debug

server:
  port: 8080

Milvus向量数据初始化

java 复制代码
package com.clay.springaialibabaragmilvus.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
 * @author Milvus初始化
 */
@Configuration
public class MilvusInitializer {

    private static final Logger logger = LoggerFactory.getLogger(MilvusInitializer.class);

    @Bean("milvusDataInitializer")
    public ApplicationRunner milvusInitializer(
            VectorStore vectorStore,
            @Value("${spring.ai.vectorstore.milvus.initialize-demo-data:true}") boolean initializeDemoData) {
        return args -> {
            if (initializeDemoData) {
                logger.info("开始初始化Milvus数据...");

                // 插入一些演示文档以确保集合被创建
                List<Document> demoDocs = List.of(
                        new Document("苹果是一种常见的水果,富含维生素C和纤维素"),
                        new Document("香蕉是热带水果,含有丰富的钾元素"),
                        new Document("橙子味道酸甜,富含维生素C"),
                        new Document("草莓外观鲜红,口感甜美,富含抗氧化物质"),
                        new Document("葡萄可以制作葡萄酒,含有多种有益成分")
                );

                try {
                    // 分批添加文档,每批最多10个,以避免超出DashScope API限制
                    int batchSize = 10;
                    for (int i = 0; i < demoDocs.size(); i += batchSize) {
                        int endIndex = Math.min(i + batchSize, demoDocs.size());
                        List<Document> batch = demoDocs.subList(i, endIndex);
                        vectorStore.add(batch);
                        logger.info("已添加批次文档到Milvus,批次范围: {}-{}", i, endIndex-1);
                    }
                    logger.info("成功添加 {} 个文档到Milvus", demoDocs.size());
                } catch (Exception e) {
                    logger.warn("添加数据到Milvus时出现错误,由于集合尚未完全准备好,错误信息: {}", e.getMessage());
                }
            }
        };
    }
}

启动项目,看Milvus是否创建了新的演示文档:

初始化成功!!!

7.7 SpringAI Alibaba一键接入Milvus实现RAG-下

ChatClient中添加QuestionAnswerAdvisor

java 复制代码
    @Bean
    public ChatClient milvusRagChatClient(@Qualifier("dashscopeChatModel") ChatModel dashscopeChatModel,
                                          RedisChatMemoryRepository redisChatMemoryRepository,
                                          VectorStore vectorStore) {
        return ChatClient.builder(dashscopeChatModel)
                .defaultSystem("""
                        你是一个专业的知识库问答助手,叫CLAY,只能基于从Milvus向量数据库中检索到的信息来回答用户问题。
                        请遵循以下原则进行回复:
                        1. 只能使用从知识库中检索到的信息来回答问题,不得凭空创造信息。
                        2. 如果检索到的相关信息不足以回答问题,请明确告知用户"根据现有知识库信息,无法回答该问题"。
                        3. 回答要简洁明了,重点突出,结构清晰。
                        4. 如果检索到的信息与用户问题相关,请结合这些信息给出准确的回答。
                        5. 请保持专业、客观的语气,不要添加个人意见。
                        """ )
                .defaultOptions(DashScopeChatOptions.builder()
                        .withModel("qwen-plus")
                        //指定温度
                        .withTemperature(0.7)
                        .build())
                .defaultAdvisors(
                        // 添加日志顾问
                        new SimpleLoggerAdvisor(),
                        // 添加聊天记忆顾问
                        MessageChatMemoryAdvisor.builder(
                                // 指定聊天记忆
                                MessageWindowChatMemory.builder()
                                        //指定聊天记忆仓库
                                        .chatMemoryRepository(redisChatMemoryRepository)
                                        //指定最大消息数
                                        .maxMessages(Integer.MAX_VALUE)
                                        .build()).build(),
                        // 添加向量存储问答顾问
                        QuestionAnswerAdvisor
                                .builder(vectorStore)
                                .searchRequest(SearchRequest.builder()
                                        .topK(5)  // 设置返回最相似的5个文档
                                        .similarityThreshold(0.7)  // 设置相似度阈值为0.7
                                        .build())
                                .build()
                )
                .build();
    }

添加向量数据和检索向量数据

java 复制代码
package com.clay.springaialibabaragmilvus.controller;

import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

import java.util.Arrays;
import java.util.List;

import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;


@RestController
@RequestMapping("/milvus")
public class MilvusRagController {

    @Autowired
    private VectorStore vectorStore;

    @Autowired
    @Qualifier("milvusRagChatClient")
    private ChatClient chatClient;

    @Autowired
    private RedisChatMemoryRepository redisChatMemoryRepository;

    @Autowired
    private RedisTemplate redisTemplate;

    private static final Logger logger = LoggerFactory.getLogger(MilvusRagController.class);

    /**
     * 向Milvus向量数据库添加文档
     */
    @PostMapping(value = "/add-documents", produces = "text/html;charset=utf-8")
    public String addDocuments(String[] text) {
        try {
            List<Document> documents = Arrays.stream(text).toList().stream()
                    .map(t -> new Document(t))
                    .toList();
            // 分批添加文档,每批最多10个,以避免超出DashScope API限制
            int batchSize = 10;
            for (int i = 0; i < documents.size(); i += batchSize) {
                int endIndex = Math.min(i + batchSize, documents.size());
                List<Document> batch = documents.subList(i, endIndex);
                vectorStore.add(batch);
                logger.info("已添加批次文档到Milvus,批次范围: {}-{}", i, endIndex-1);
            }

            return String.format("成功添加 %d 个水果文档到Milvus向量数据库", documents.size());
        } catch (Exception e) {
            logger.error("添加文档到Milvus时发生错误: ", e);
            return "添加文档失败: " + e.getMessage();
        }
    }

    /**
     * 使用RAG从Milvus检索信息并回答问题
     */
    @GetMapping(value = "/rag-query", produces = "text/html;charset=utf-8")
    public Flux<String> ragQuery(
            @RequestParam String query,
            @RequestParam(value = "sessionId", defaultValue = "student_session") String sessionId,
            @RequestParam(value = "userId", defaultValue = "default_userId") String userId) {

        String redisKey = userId + ":" + sessionId;

        MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
                .chatMemoryRepository(redisChatMemoryRepository)
                .maxMessages(Integer.MAX_VALUE)
                .build();
        // 如果是新的会话 将 sessionId 存入 Redis list类型 userId为key sessionId为值
        if (messageWindowChatMemory.get(redisKey).size() <1){
            redisTemplate.setKeySerializer(RedisSerializer.string());
            redisTemplate.setValueSerializer(RedisSerializer.json());
            BoundListOperations boundListOperations = redisTemplate.boundListOps("history:"+userId);
            boundListOperations.leftPush(sessionId);
        }

        try {
            // 使用QuestionAnswerAdvisor实现RAG功能
            return chatClient
                    .prompt()
                    .advisors(
                            a -> a.param(CONVERSATION_ID, redisKey)
                    )
                    .user(query)
                    .advisors(QuestionAnswerAdvisor
                            .builder(vectorStore)
                            .searchRequest(SearchRequest.builder().query(query).build())
                            .build()
                    )
                    .stream()
                    .content();
        } catch (Exception e) {
            logger.error("RAG查询时发生错误: ", e);
            return Flux.error(e);
        }
    }
}

7.8 玩转RAG进阶-构建知识库让AI读懂PDF和MD

添加依赖

XML 复制代码
        <!-- PDF文档读取器 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-pdf-document-reader</artifactId>
        </dependency>
        
        <!-- Markdown文档读取器 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-markdown-document-reader</artifactId>
        </dependency>
        
        <!-- Tika文档读取器 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-tika-document-reader</artifactId>
        </dependency>
        
        <!-- Spring AI Alibaba文档读取器 -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-document-reader-poi</artifactId>
        </dependency>

文本向量化存储与检索

java 复制代码
package com.clay.springaialibabaragmilvus.controller;

import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository;
import com.alibaba.cloud.ai.reader.poi.PoiDocumentReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.document.Document;
import com.alibaba.cloud.ai.reader.poi.PoiDocumentReader;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.core.io.InputStreamResource;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.core.io.InputStreamResource;
import java.io.ByteArrayInputStream;


import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;

import java.util.List;

import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;


@RestController
@RequestMapping("/api/rag")
public class RagController {

    private static final Logger logger = LoggerFactory.getLogger(RagController.class);

    @Autowired
    @Qualifier("milvusRagChatClient")
    private ChatClient ragChatClient;

    @Autowired
    private VectorStore vectorStore;

    @Autowired
    private RedisChatMemoryRepository redisChatMemoryRepository;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 上传文件并将其内容添加到向量数据库
     */
    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
        try {
            if (file.isEmpty()) {
                return ResponseEntity.badRequest().body("文件不能为空");
            }

            String fileName = file.getOriginalFilename();
            String fileExtension = getFileExtension(fileName);

            List<Document> documents;

            if ("pdf".equalsIgnoreCase(fileExtension)) {
                // 对PDF文件使用专门的PDF文档读取器
                byte[] pdfContent = file.getBytes();
                ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfContent);
                InputStreamResource pdfResource = new InputStreamResource(pdfInputStream);

                PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(pdfResource);
                documents = pdfReader.get();
            } else {
                // 对其他格式使用TikaDocumentReader
                TikaDocumentReader reader = new TikaDocumentReader(file.getResource());
                documents = reader.get();
            }

            // 使用文本分割器处理文档
            TokenTextSplitter textSplitter = new TokenTextSplitter();
            List<Document> splitDocuments = textSplitter.apply(documents);

            // 添加到向量数据库
            vectorStore.add(splitDocuments);

            logger.info("成功上传并处理文件: {}, 文档数量: {}", fileName, splitDocuments.size());
            return ResponseEntity.ok("成功上传文件并添加到向量数据库,共处理了 " + splitDocuments.size() + " 个文档片段");

        } catch (Exception e) {
            logger.error("处理文件上传时发生错误: ", e);
            return ResponseEntity.status(500).body("处理文件时发生错误: " + e.getMessage());
        }
    }

    /**
     * 使用RAG从向量数据库检索信息并回答问题
     */
    @GetMapping(value = "/query", produces = "text/html;charset=utf-8")
    public Flux<String> ragQuery(
            @RequestParam String query,
            @RequestParam(value = "sessionId", defaultValue = "rag_session") String sessionId,
            @RequestParam(value = "userId", defaultValue = "default_user") String userId) {

        String redisKey = userId + ":" + sessionId;

        MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
                .chatMemoryRepository(redisChatMemoryRepository)
                .maxMessages(Integer.MAX_VALUE)
                .build();

        // 如果是新的会话,将sessionId存入Redis list类型,以userId为key
        if (messageWindowChatMemory.get(redisKey).size() < 1) {
            redisTemplate.setKeySerializer(RedisSerializer.string());
            redisTemplate.setValueSerializer(RedisSerializer.json());
            BoundListOperations boundListOperations = redisTemplate.boundListOps("history:" + userId);
            boundListOperations.leftPush(sessionId);
        }

        try {
            // 使用QuestionAnswerAdvisor实现RAG功能
            return ragChatClient
                    .prompt()
                    .advisors(a -> a.param(CONVERSATION_ID, redisKey))
                    .user(query)
                    .advisors(QuestionAnswerAdvisor
                            .builder(vectorStore)
                            .searchRequest(SearchRequest.builder()
                                    .query(query)
                                    .topK(5)  // 设置返回最相似的5个文档
                                    .similarityThreshold(0.7)  // 设置相似度阈值为0.7
                                    .build())
                            .build())
                    .stream()
                    .content();
        } catch (Exception e) {
            logger.error("RAG查询时发生错误: ", e);
            return Flux.error(e);
        }
    }


    /**
     * 获取文件扩展名
     */
    private String getFileExtension(String fileName) {
        if (fileName == null || !fileName.contains(".")) {
            return "";
        }
        return fileName.substring(fileName.lastIndexOf(".") + 1);
    }
}

简单页面测试文档上传

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>RAG文件上传测试</title>
    <meta charset="utf-8">
</head>
<body>
    <h1>RAG文件上传测试</h1>
    <form id="uploadForm" action="/api/rag/upload" method="post" enctype="multipart/form-data">
        <div>
            <label for="file">选择文件:</label>
            <input type="file" id="file" name="file" accept=".pdf,.txt,.md,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.html,.xml" required>
        </div>
        <br>
        <button type="submit">上传文件</button>
    </form>
</body>
</html>
相关推荐
爱打代码的小林2 小时前
基于 OpenCV 实现实时目标跟踪:CSRT 跟踪器
人工智能·opencv·目标跟踪
主机哥哥2 小时前
OpenClaw:让 AI 替你干活!基础定义 + 功能场景 + 部署教程
人工智能·openclaw·openclaw部署·openclaw安装
BIST2 小时前
ICML 2025 | 仅需 6.5% 显存!GS-Bias:高效视觉语言模型测试时自适应新范式
人工智能·深度学习·机器学习·计算机视觉
H Journey2 小时前
学习OpenCV之HSV 颜色模式
人工智能·opencv·学习·hsv
shy^-^cky2 小时前
卷积神经网络(CNN)客观题(含答案+解析)
人工智能·神经网络·cnn
东离与糖宝2 小时前
微软BitNet开源:用Java在边缘设备部署7B级本地大模型(含ONNX Runtime优化)
java·人工智能
xixixi777772 小时前
从图灵测试到大模型:人工智能的演进之路(最近open claw及重看流浪地球有感)
安全·ai·大模型·模型·通信
老成说AI2 小时前
营收跨越400亿:拆解追觅科技的全球化“炸场”战略与TikTok操盘术
人工智能·科技·tiktok·soundview
桂花饼2 小时前
国内直连 GPT-5.4、 qwen3.5-plus 与 Gemini 3.1(附API接入方案)
人工智能·sora2·openclaw·gpt-5.4·gemini3.1·qwen 3.5 plus