目录
[2.1 初识阿里云百炼-你的云端模型炼丹炉](#2.1 初识阿里云百炼-你的云端模型炼丹炉)
[获取你的神兵利器 - API Key](#获取你的神兵利器 - API Key)
[2.2 理解通讯协议-大模型OpenAI接口规范](#2.2 理解通讯协议-大模型OpenAI接口规范)
[OpenAI API 接口规范](#OpenAI API 接口规范)
[API 基础 URL](#API 基础 URL)
[3.1 讲解Ollama框架及其安装](#3.1 讲解Ollama框架及其安装)
[3.2 如何召唤你的第一个本地模型-qwen3](#3.2 如何召唤你的第一个本地模型-qwen3)
[四、带你快速入门SpringAI Alibaba](#四、带你快速入门SpringAI Alibaba)
[4.1 讲解SpringAI Alibaba及其核心价值和与SpringAI的区别](#4.1 讲解SpringAI Alibaba及其核心价值和与SpringAI的区别)
[SpringAI Alibaba简介](#SpringAI Alibaba简介)
[SpringAI Alibaba官方文档](#SpringAI Alibaba官方文档)
[4.2 SpringAI Alibaba如何配置你的双模型战略](#4.2 SpringAI Alibaba如何配置你的双模型战略)
[如何在Spring Boot中配置云端和本地大模型](#如何在Spring Boot中配置云端和本地大模型)
[配置多模型 application.yml](#配置多模型 application.yml)
[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记住上下文-上)
[5.3 SpringAI Alibaba集成Redis让AI记住上下文-下](#5.3 SpringAI Alibaba集成Redis让AI记住上下文-下)
[六、使用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-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文档参考
-
访问阿里云百炼
体验云模型

获取你的神兵利器 - 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会自动运行模型服务
-
一个本地大模型API服务已经启动
-
可以测试
-

- 也可以通过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>



