实战:Spring AI + Milvus + RAG实现智能问答系统的上下文感知与查询优化(附完整代码)
1. 从零开始:为什么你需要一个“会思考”的智能问答系统?
你好,我是老张,一个在AI和智能硬件领域摸爬滚打了十多年的老码农。今天,我想和你聊聊一个特别有意思的话题:如何用Spring AI、Milvus和RAG技术,亲手搭建一个真正“懂你”的智能问答系统。你可能已经用过不少聊天机器人,但有没有遇到过这样的尴尬:你问了一个问题,它答得挺好;你接着追问一句“还有吗?”,它却一脸懵,完全忘了刚才聊了什么。或者,你提了一个又长又啰嗦的问题,它抓不住重点,给出的答案驴唇不对马嘴。
这些问题,本质上是因为系统缺乏上下文感知和查询优化的能力。一个只会“一问一答”的机器人,就像一本不会翻页的字典,用起来总感觉差点意思。而我们今天要做的,就是给它装上“记忆”和“理解力”,让它能记住对话历史,还能把你的大白话自动“翻译”成精准的查询指令。听起来是不是很酷?别担心,整个过程就像搭积木,我会手把手带你走一遍,从环境搭建到代码实现,最后你也能拥有一个能进行多轮流畅对话、还能帮你优化问题的智能助手。
这个系统的核心是 RAG(检索增强生成)。简单来说,它结合了“大海捞针”和“妙笔生花”两个步骤。首先,系统会从你自己的知识库(比如公司文档、产品手册)里,快速找到和你问题最相关的几段信息(检索);然后,把这些信息作为“参考资料”交给大语言模型,让它组织成通顺、准确的回答(生成)。这比让模型凭空瞎编要靠谱得多。而我们要做的升级,就是在“检索”这一步下功夫:让系统能结合之前的聊天记录来理解你当前的问题(上下文感知),还能自动把你的口语化问题提炼成更精准的关键词(查询重写)。这样一来,检索到的资料更准,生成的答案自然也就更好了。
接下来,我们就从准备“施工场地”开始,一步步把这个智能系统搭建起来。我会分享我在搭建过程中踩过的坑和总结的经验,保证你跟着做,一次就能跑通。
2. 环境准备:搭建你的AI开发“工作台”
工欲善其事,必先利其器。在开始写代码之前,我们需要把运行环境准备好。这里我强烈推荐使用Docker,它能帮你把所有依赖的服务(比如数据库)打包成一个个独立的“集装箱”,一键启动,环境干净又隔离,再也不用为“在我机器上好好的”这种问题头疼了。
2.1 Docker与基础服务部署
首先,确保你的电脑上已经安装了Docker Desktop。去Docker官网下载对应你操作系统的版本(Windows、macOS都有),安装过程很简单,一路下一步就行。安装完成后,打开终端(Windows用PowerShell或CMD,macOS/Linux用Terminal),输入 docker --version 和 docker compose version,如果能看到版本号,恭喜你,第一步成功了。
我们的系统需要两个核心的“后勤”服务:Redis 和 Milvus。
- Redis: 我们将用它来存储对话历史。想象一下,每次你和机器人聊天,它都需要记住最近几句对话,才能理解“还有吗?”指的是什么。Redis这种内存数据库,读写速度极快,非常适合做这种临时的“记忆中枢”。
- Milvus: 这是我们的“超级大脑图书馆”。所有你的知识文档(比如我后面会用的医院介绍文本),都会被转换成一种叫“向量”的数字形式,存进Milvus。当你提问时,你的问题也会被转换成向量,Milvus能瞬间从海量向量中找到最相似的那几段文本。它专为向量检索设计,速度比传统数据库快几个数量级。
部署Redis: 我们用一个简单的命令来拉取并运行Redis。在终端里执行:
docker run -d --name my-redis -p 6379:6379 redis:7.4.2
这个命令做了几件事:-d 表示后台运行,--name 给容器起个名字叫my-redis,-p 6379:6379 把容器内的6379端口映射到你电脑的6379端口(这样你的Spring Boot程序才能连上它),最后指定使用redis:7.4.2这个镜像。执行后,用 docker ps 命令看看,如果 my-redis 的状态是 Up,就说明Redis已经欢快地跑起来了。
部署Milvus及其全家桶:
Milvus本身运行还需要两个小伙伴:etcd(负责协调服务)和minio(负责存储数据)。手动配起来比较麻烦,好在官方提供了“一键安装包”——Docker Compose文件。
在你电脑上找个顺眼的地方,比如 C:\docker\milvus 或 ~/docker/milvus,新建一个文件叫 docker-compose.yml。把下面这段配置复制进去:
version: '3.5'
services:
etcd:
container_name: milvus-etcd
image: quay.io/coreos/etcd:v3.5.16
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
volumes:
- ./volumes/etcd:/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:
container_name: milvus-minio
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
ports:
- "9001:9001"
- "9000:9000"
volumes:
- ./volumes/minio:/minio_data
command: minio server /minio_data --console-address ":9001"
standalone:
container_name: milvus-standalone
image: milvusdb/milvus:v2.5.4
command: ["milvus", "run", "standalone"]
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
volumes:
- ./volumes/milvus:/var/lib/milvus
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- "etcd"
- "minio"
attu:
image: zilliz/attu:v2.5
container_name: milvus-dashboard
environment:
MILVUS_URL: http://standalone:19530
ports:
- "8000:3000"
depends_on:
- standalone
保存文件后,在这个目录下打开终端,运行 docker compose up -d。Docker会开始拉取四个镜像并启动容器,第一次运行可能需要几分钟,喝杯咖啡等等。完成后,再用 docker ps 检查,应该能看到四个容器都在运行。
特别提一下最后一个服务 attu,这是Milvus的可视化管理界面。在浏览器里访问 http://localhost:8000,就能看到一个Web界面。第一次进入时,连接地址保持默认的 http://standalone:19530,直接点【Connect】。进去后,建议在用户管理里创建一个新用户,比如用户名 derek,密码 milvus-4321,角色选 admin。后面我们的Spring Boot程序会用到这个账号来连接Milvus。
2.2 Spring Boot项目初始化与核心依赖
环境搭好了,现在来创建我们的Spring Boot项目。我习惯用IntelliJ IDEA,你可以用任何你顺手的IDE。创建一个新的Spring Boot项目,选择Java 17或更高版本,打包方式用Maven。
项目创建好后,打开 pom.xml 文件,这里是管理所有“零件”(依赖)的地方。我们需要引入几个关键的依赖,我把核心部分贴出来,并解释每个是干嘛用的:
<properties>
<java.version>17</java.version>
<spring-boot.version>3.4.4</spring-boot.version>
<spring-ai.version>1.0.0-M6</spring-ai.version>
<spring-alibaba.version>1.0.0-M6.1</spring-alibaba.version>
</properties>
<dependencies>
<!-- Spring Boot Web 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Spring Data Redis 用于对话记忆 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Spring AI 与 Milvus 集成的向量存储 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId>
</dependency>
<!-- Spring AI 与阿里云百炼平台集成 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>${spring-alibaba.version}</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- 统一管理 Spring AI 版本 -->
<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>
这里有几个关键点:
spring-ai-milvus-store-spring-boot-starter: 这是Spring AI官方提供的Milvus集成包,有了它,我们就能用几行配置轻松操作Milvus向量数据库,不用自己写复杂的客户端代码。spring-ai-alibaba-starter: 这是阿里云提供的Spring AI适配器。我们需要一个大语言模型来生成回答,也需要一个嵌入模型来把文本变成向量。阿里云百炼平台提供了稳定、高效的API,这个依赖包就是连接我们代码和百炼API的桥梁。- 版本管理: 注意
spring-ai.version和spring-alibaba.version,Spring AI生态还在快速迭代,用我上面写的这两个版本组合是亲测可用的,能避免很多兼容性坑。
依赖加好后,别忘了配置一下Maven仓库,因为有些包可能在默认的中央仓库没有。在 pom.xml 的 <repositories> 部分,确保添加阿里云仓库和Spring的里程碑仓库,具体配置可以参考我后面给出的完整代码。
3. 核心配置与基础功能实现
环境和服务都就绪了,现在我们来给Spring Boot项目注入“灵魂”——配置文件和基础功能代码。这部分就像给机器人接通电源和设定初始程序。
3.1 应用配置详解
首先,打开 src/main/resources/application.yml 文件,这是整个应用的“控制中心”。我们把所有需要连接的外部服务信息都写在这里:
server:
port: 8080
spring:
application:
name: smart-qa-system
data:
redis:
host: localhost
port: 6379
# 如果你之前给Redis设了密码,这里要填上
# password: yourpassword
database: 0
ai:
dashscope:
# 这里是关键!去阿里云百炼平台申请一个API-KEY填进来
api-key: sk-你的真实API-KEY
# 我们使用DeepSeek-R1模型进行对话生成,它的推理和指令跟随能力很强
model: deepseek-r1
embedding:
options:
# 使用百炼的text-embedding-v2模型将文本转换为向量
model: text-embedding-v2
vectorstore:
milvus:
client:
host: "localhost"
port: 19530
# 这里填写你在Attu界面创建的用户名和密码
username: "derek"
password: "milvus-4321"
databaseName: "default"
# 向量数据存储的集合(类似数据库的表)名称
collectionName: "hospital_knowledge_base"
# 嵌入向量的维度,text-embedding-v2模型生成的是1536维向量
embeddingDimension: 1536
indexType: IVF_FLAT
metricType: COSINE
# 启动时自动创建集合结构,第一次运行设为true
initialize-schema: true
我来划几个重点:
spring.ai.dashscope.api-key: 这是项目的“钥匙”,没有它就无法调用AI模型。你需要注册阿里云账号,开通百炼平台服务,然后创建一个API-KEY。记得保护好它,不要上传到公开的代码仓库。spring.ai.dashscope.model: 这里我选了deepseek-r1,你也可以根据百炼平台提供的模型列表换成其他,比如qwq-plus。不同模型在效果和速度上略有差异,deepseek-r1在逻辑推理和长文本理解上表现不错。embeddingDimension: 1536: 这个数字必须和你使用的嵌入模型匹配。text-embedding-v2模型输出的向量就是1536维,填错了会导致向量存储和检索失败。initialize-schema: true: 第一次运行时务必设为true,Spring AI会自动在Milvus里创建好集合(Collection)和索引。之后运行可以改为false以加快启动速度。
3.2 构建知识库:把文档“喂”给系统
一个空的问答系统是没用的,我们必须先给它“灌输”知识。假设我们有一份关于“不爱学习康复医院”的详细介绍文档(demoHospital.txt),里面包含了医院概况、科室和医生信息。我们需要把这些文本“喂”进Milvus向量数据库。
这个过程分为三步:读取、切分、入库。我写了一个简单的数据加载接口来实现:
@Slf4j
@RestController
@RequestMapping("/api/v1")
public class KnowledgeBaseController {
@Autowired
private VectorStore vectorStore; // Spring AI自动注入的向量存储操作接口
@GetMapping("/load-data")
public String loadData() throws IOException {
// 1. 读取文档
Resource resource = new ClassPathResource("static/demoHospital.txt");
DocumentReader reader = new TikaDocumentReader(resource.getFile());
List<Document> documents = reader.get();
// 2. 文本切分(这是关键!)
TextSplitter splitter = new TokenTextSplitter(
500, // 每个文本块的最大token数
100, // 相邻文本块之间的重叠token数,防止上下文断裂
20, // 最大块数限制(可选)
200, // 每个块的最小token数(可选)
true // 是否详细记录切分过程
);
List<Document> splitDocs = splitter.apply(documents);
log.info("原始文档已切分为 {} 个文本块", splitDocs.size());
// 3. 向量化并存入Milvus
vectorStore.add(splitDocs);
log.info("知识库数据已成功写入向量数据库。");
return "数据加载成功!共处理 " + splitDocs.size() + " 个文本块。";
}
}
这里最核心的是文本切分。你不能把一整篇几十页的文档直接扔给模型,那样效率低,而且检索时容易抓不到重点。TokenTextSplitter 会按语义和长度,把长文档切成一个个小片段(比如每段500个token左右),并且让相邻片段有少量重叠(比如100个token),这样能保证一个完整的句子或概念不会被生硬地切断。切分的好坏,直接决定了后续检索的精度。
启动你的Spring Boot应用,然后在浏览器或Postman里访问 http://localhost:8080/api/v1/load-data。如果看到“数据加载成功”的返回,并且日志没有报错,就可以打开Attu界面(http://localhost:8000),在 Collections 页面应该能看到一个名为 hospital_knowledge_base 的集合,并且里面有数据了。这意味着你的知识库已经准备就绪!
3.3 实现基础对话与记忆功能
有了知识库,我们现在来实现最基础的问答功能,并给它加上“记忆”。我们创建一个 ChatService 来封装核心逻辑:
@Service
public class ChatService {
@Autowired
private ChatClient chatClient; // 用于调用大模型
@Autowired
private VectorStore vectorStore; // 用于检索知识库
@Autowired
private ChatMemory chatMemory; // 用于存储对话历史
private static final int HISTORY_SIZE = 5; // 记住最近5轮对话
public String chat(String userId, String userMessage) {
// 1. 获取该用户最近的对话历史
List<Message> historyMessages = chatMemory.get(userId, HISTORY_SIZE);
// 2. 从知识库中检索与当前问题相关的信息
SearchRequest request = SearchRequest.query(userMessage)
.withTopK(5) // 返回最相关的5个片段
.withSimilarityThreshold(0.7); // 相似度阈值,过滤掉太不相关的
List<Document> relevantDocs = vectorStore.similaritySearch(request);
// 3. 构建提示词(Prompt),将历史、检索结果和当前问题组合起来
String context = relevantDocs.stream()
.map(Doc::getContent)
.collect(Collectors.joining("\n\n"));
Prompt prompt = new Prompt(
new SystemMessage("你是一个专业的医疗咨询助手,请根据以下已知信息回答问题。如果信息不足,请如实告知。\n已知信息:\n" + context),
historyMessages, // 注入历史对话
new UserMessage(userMessage) // 当前用户问题
);
// 4. 调用大模型生成回答
ChatResponse response = chatClient.call(prompt);
String assistantReply = response.getResult().getOutput().getContent();
// 5. 将本轮对话(用户问题+助手回答)存入记忆
List<Message> currentTurn = List.of(
new UserMessage(userMessage),
new AssistantMessage(assistantReply)
);
chatMemory.add(userId, currentTurn);
return assistantReply;
}
}
这段代码体现了RAG的核心流程。ChatMemory 是一个接口,我们需要实现它。这里我用Redis来实现:
@Component
public class RedisChatMemory implements ChatMemory {
private final RedisTemplate<String, Object> redisTemplate;
private static final String KEY_PREFIX = "chat:history:";
@Override
public void add(String conversationId, List<Message> messages) {
String key = KEY_PREFIX + conversationId;
// 将Message列表序列化后存入Redis列表
redisTemplate.opsForList().rightPushAll(key, messages.toArray());
// 设置过期时间,例如30分钟,避免内存无限增长
redisTemplate.expire(key, 30, TimeUnit.MINUTES);
}
@Override
public List<Message> get(String conversationId, int lastN) {
String key = KEY_PREFIX + conversationId;
Long size = redisTemplate.opsForList().size(key);
if (size == null || size == 0) {
return Collections.emptyList();
}
// 获取列表最后lastN条记录
int start = Math.max(0, (int)(size - lastN));
List<Object> stored = redisTemplate.opsForList().range(key, start, -1);
// 反序列化回Message对象
return stored.stream()
.map(obj -> (Message) obj)
.collect(Collectors.toList());
}
}
这样,一个具备基础记忆能力的问答系统就完成了。你可以创建一个简单的REST控制器来暴露 /chat 接口,传入 userId 和 message 进行测试。同一个 userId 的多次对话,系统就能记住上下文了。
4. 高级功能实战:上下文感知与查询重写
基础功能跑通后,我们来点更高级的。这也是让我们的系统从“还行”变得“好用”的关键。
4.1 上下文感知查询:让机器人“记住”刚才聊了什么
在多轮对话中,用户的问题往往是简略的、有上下文的。比如:
- 用户第一问:“不爱学习康复医院的副主任医生都有谁?”
- 系统回答:(列出张三、李四等副主任医师)
- 用户第二问:“还有吗?”
如果没有上下文感知,系统会把“还有吗?”当成一个独立的新问题去知识库检索,很可能返回一些无关信息,或者直接说“我不明白你的问题”。上下文感知查询,就是要让系统能结合历史对话,把“还有吗?”自动补全成“不爱学习康复医院除了刚才提到的张三、李四, 还有哪些副主任医生?”
Spring AI 提供了一个非常方便的工具:CompressionQueryTransformer。看代码:
@GetMapping("/context-aware-query")
public String contextAwareQuery(@RequestParam String userId, @RequestParam String currentQuery) {
// 1. 获取对话历史
List<Message> history = chatMemory.get(userId, 5);
// 2. 构建一个包含历史的查询对象
Query query = Query.builder()
.text(currentQuery) // 用户当前的问题,如“还有吗?”
.history(history) // 注入历史对话
.build();
// 3. 使用上下文感知查询转换器
CompressionQueryTransformer transformer = CompressionQueryTransformer.builder()
.chatClientBuilder(chatClient.mutate())
.build();
// 4. 转换!得到融合了上下文的新查询
Query enhancedQuery = transformer.transform(query);
String finalQueryText = enhancedQuery.text();
log.info("原始查询:'{}' -> 上下文感知后查询:'{}'", currentQuery, finalQueryText);
return finalQueryText;
}
这个接口返回的不是最终答案,而是经过“理解”后的新查询语句。你可以把这个新查询语句,再交给之前的基础 chat 方法去知识库检索并生成答案。在实际项目中,你可以把第3、4步集成到 ChatService.chat() 方法内部,对用户输入的问题先做一次上下文增强,再进行检索,实现无缝的体验。
我实测下来,这个功能对提升多轮对话的连贯性帮助巨大。它本质上是用大模型对当前问题和历史记录做了一次“阅读理解”,然后生成一个更完整、更独立的查询语句。
4.2 查询重写:把口语化问题变成“标准提问”
另一个常见问题是,用户的提问方式千奇百怪,非常口语化、冗长。比如:“我想去北京,坐高铁过去,北京有什么景点比较好玩,还得便宜”。直接拿这句话去向量库做相似度匹配,效果可能不好,因为向量匹配对措辞比较敏感。
查询重写(Query Rewrite)就是来解决这个问题的。它的目标是把用户啰嗦、不规范的提问,改写成简洁、清晰、关键词明确的“标准问法”。例如,把上面那句重写成:“北京性价比高的旅游景点推荐”。
Spring AI 同样提供了现成的 RewriteQueryTransformer:
@GetMapping("/query-rewrite")
public String rewriteQuery(@RequestParam String userQuery) {
RewriteQueryTransformer rewriter = RewriteQueryTransformer.builder()
.chatClientBuilder(chatClient.mutate())
.build();
Query originalQuery = new Query(userQuery);
Query rewrittenQuery = rewriter.transform(originalQuery);
log.info("查询重写:'{}' -> '{}'", userQuery, rewrittenQuery.text());
return rewrittenQuery.text();
}
这个转换器内部也是调用大模型,给它一个指令,比如“请将以下用户查询改写为更简洁、更适合信息检索的句子,保留核心意图”,然后让它对原查询进行改写。改写后的查询,再用于向量检索,命中率会显著提升。
把两者结合起来,就是一个强大的查询预处理流水线:
public String smartChat(String userId, String rawUserInput) {
// 步骤1:查询重写(优化表达)
String rewrittenQuery = queryRewriteService.rewrite(rawUserInput);
// 步骤2:上下文感知(补充信息)
List<Message> history = chatMemory.get(userId, 5);
String finalQuery = contextAwareService.enhance(rewrittenQuery, history);
// 步骤3:用优化后的查询进行RAG检索与生成
return chatService.ragChat(userId, finalQuery);
}
这个流程模拟了人类助理的思考过程:先听清你说什么(重写),再结合之前的聊天背景理解你的真实意图(上下文感知),最后再去资料库(向量库)里找答案并组织语言回复你。
4.3 效果对比与参数调优
理论说再多,不如看实际效果。我们用一个简单的测试来对比:
| 用户输入 | 处理方式 | 系统内部使用的查询语句 | 可能的结果差异 |
|---|---|---|---|
| “还有吗?” | 基础RAG(无上下文) | “还有吗?” | 检索结果混乱,可能返回无关信息。 |
| “还有吗?” | 上下文感知RAG | “不爱学习康复医院除了已提到的,还有哪些副主任医师?” | 能准确检索出其他副主任医师信息。 |
| “我想去北京,坐高铁过去,北京有什么景点比较好玩,还得便宜” | 基础RAG | 原句直接检索 | 可能因表述冗长,相似度匹配不佳。 |
| “我想去北京,坐高铁过去,北京有什么景点比较好玩,还得便宜” | 查询重写RAG | “北京性价比高的旅游景点推荐” | 检索更精准,更易找到相关攻略。 |
参数调优心得:
- 文本切分参数:
TokenTextSplitter的chunkSize(块大小)和overlap(重叠长度)需要根据你的文档类型调整。对于技术文档,chunkSize=500可能不错;对于对话记录,chunkSize=300可能更合适。overlap一般设为chunkSize的 1/5 到 1/4,确保上下文连贯。 - 检索参数:
SearchRequest的topK(返回数量)和similarityThreshold(相似度阈值)需要平衡。topK太小可能漏掉相关信息,太大则会给模型输入过多噪音。我一般从topK=5开始调。similarityThreshold可以过滤掉低质量片段,初期可以设低点(如0.1),观察效果后再调整。 - 对话记忆长度:
HISTORY_SIZE不是越大越好。太长的历史可能会引入无关信息,干扰当前问题的理解。通常记住最近3-5轮对话已经足够。对于超长对话,可以考虑更复杂的记忆压缩或总结策略。
5. 系统集成、测试与踩坑指南
所有零件都准备好了,现在我们把它们组装起来,并看看实际运行效果。
5.1 完整代码结构与集成
我建议的项目结构如下,清晰易懂:
src/main/java/com/example/smartqa/
├── SmartQaApplication.java // 启动类
├── config/
│ ├── RedisConfig.java // Redis配置
│ └── ChatClientConfig.java // ChatClient与向量存储Bean配置
├── controller/
│ ├── KnowledgeBaseController.java // 数据加载
│ ├── ChatController.java // 基础对话接口
│ └── AdvancedQueryController.java // 高级查询(上下文、重写)接口
├── service/
│ ├── ChatService.java // 核心对话服务
│ ├── ContextAwareService.java // 上下文感知服务
│ └── QueryRewriteService.java // 查询重写服务
├── memory/
│ └── RedisChatMemory.java // 基于Redis的对话记忆实现
└── entity/
└── ChatMessage.java // 对话消息实体
在 ChatClientConfig 中,我们将所有Bean组装起来:
@Configuration
public class ChatClientConfig {
@Autowired
private VectorStore vectorStore;
@Autowired
private ChatModel chatModel; // 由application.yml配置自动注入
@Bean
public ChatClient chatClient(ChatMemory chatMemory) {
return ChatClient.builder(chatModel)
.defaultSystem("你是一个专业、友好的助手。请根据提供的上下文信息回答问题。如果信息不足,请礼貌地告知用户。回答时请条理清晰。")
.defaultAdvisors(
new QuestionAnswerAdvisor(
vectorStore,
SearchRequest.builder()
.topK(5)
.similarityThreshold(0.7)
.build()
)
)
.build();
}
@Bean
public ChatMemory chatMemory(RedisTemplate<String, Object> redisTemplate) {
return new RedisChatMemory(redisTemplate);
}
}
这个配置类是整个应用的核心粘合剂。它创建了 ChatClient,并为其装配了两个“顾问”:一个是系统指令,定义了AI的“人设”;另一个是 QuestionAnswerAdvisor,它会在每次对话前,自动使用配置好的 vectorStore 和检索参数去知识库查找资料,并将结果作为上下文提供给大模型。这样,我们在业务代码里只需要关注对话逻辑本身,检索增强的过程被自动完成了。
5.2 全流程测试验证
启动你的Spring Boot应用(确保Redis和Milvus都在运行),然后我们按顺序测试:
- 加载知识库:访问
GET http://localhost:8080/api/v1/load-data。看到成功提示后,去Attu检查hospital_knowledge_base集合是否有数据。 - 测试基础对话:访问
GET http://localhost:8080/api/v1/chat?userId=user123&message=不爱学习康复医院的副主任医生都有谁?。系统应该能从我们“喂”进去的文档中,找到并列出副主任医师的信息。 - 测试上下文感知:先调用一次上面的基础对话。然后调用
GET http://localhost:8080/api/v1/context-aware?userId=user123¤tQuery=还有吗?。观察返回的查询语句是否变成了一个包含上下文(比如医院名称、医生职称)的完整问题。 - 测试查询重写:访问
GET http://localhost:8080/api/v1/rewrite?query=我想去北京,坐高铁过去,北京有什么景点比较好玩,还得便宜。看看返回的是不是一句简洁的“北京性价比高景点推荐”之类的句子。
你可以把第3步得到的“增强后查询”,直接作为 message 参数,再调用第2步的基础对话接口,看看返回的答案是否更精准、连贯。
5.3 常见问题与排查心得
这条路我踩过不少坑,这里分享几个最常见的:
- Milvus连接失败:最常见的原因是Attu里创建的用户名密码,和
application.yml里配置的不一致。或者Milvus服务没完全启动好。多等一会儿,用docker logs milvus-standalone看看容器日志。 - 向量维度不匹配:报错信息可能提到
dimension mismatch。一定要确认embeddingDimension: 1536和你使用的嵌入模型(text-embedding-v2)输出维度一致。不同模型维度可能不同。 - API-KEY无效或额度不足:调用阿里云百炼API失败。去百炼平台控制台检查API-KEY是否启用,以及是否有足够的调用额度。
- 检索结果不相关:首先检查知识库数据是否成功加载(Attu里看)。然后调整文本切分参数,
chunkSize可能太大了,把完整的语义切碎了。也可以尝试调低similarityThreshold,或者增加topK。 - 对话历史不生效:检查Redis是否正常运行,以及
RedisTemplate的序列化配置是否正确。确保ChatMemory接口的add和get方法被正确调用。
最后,这个项目只是一个起点。你可以在此基础上做很多扩展,比如:
- 支持多格式文档:使用
TikaDocumentReader可以支持PDF、Word、PPT等格式,让知识库来源更丰富。 - 增加来源引用:在返回答案时,附带引用的原文片段,增加可信度。
- 实现流式输出:对于长答案,使用Spring AI的流式响应,让答案一个字一个字地“打”出来,体验更好。
- 接入前端界面:用Vue或React写个简单的聊天界面,打造完整的应用。
希望这篇详细的实战指南能帮你打通Spring AI、Milvus和RAG的任督二脉。这套组合拳威力不小,无论是做企业知识库、智能客服还是个人学习助手,都能派上用场。编程最快乐的事,不就是看着自己搭建的系统,真正地“理解”和“回答”问题吗?动手试试吧,遇到问题随时来交流。
更多推荐



所有评论(0)