1. 项目概述
作为一名长期从事Java企业级开发的工程师,最近在探索如何将大语言模型(LLM)能力更好地集成到现有系统中。Spring AI作为Spring生态中的AI集成框架,提供了便捷的API来连接各种AI服务。本文将重点分享如何利用Milvus向量数据库构建知识库,并通过Spring AI的RAG(检索增强生成)能力实现智能问答功能。
这个方案特别适合需要将企业私有知识(如产品文档、客服话术、业务规则)与通用大模型结合的场景。通过将知识向量化存储,我们可以在保持大模型通用能力的同时,为其注入领域专业知识,显著提升回答的准确性和专业性。
2. 核心组件与原理
2.1 技术栈选型解析
选择Milvus作为向量数据库主要基于以下几点考虑:
- 性能优势:Milvus专为向量搜索优化,支持多种索引类型(如IVF_FLAT、HNSW),在千万级向量中仍能保持毫秒级响应
- 生态兼容:提供完善的Java SDK,与Spring生态集成顺畅
- 可扩展性:支持分布式部署,方便后续业务增长时横向扩展
- 成本效益:相比纯内存方案(如FAISS),Milvus在保证性能的同时更节省资源
实际选型时还需考虑团队技术栈。如果已在使用Elasticsearch,也可考虑其8.0+版本的原生向量搜索功能,但专业向量数据库在相似度计算精度上通常更优。
2.2 RAG架构解析
检索增强生成(RAG)的核心思想是将传统信息检索与生成式AI结合:
- 检索阶段:将用户问题转换为向量,在知识库中查找最相关的文档片段
- 生成阶段:将检索结果作为上下文注入LLM提示词,指导模型生成专业回答
这种架构的优势在于:
- 避免了大模型常见的"幻觉"问题(编造不存在的信息)
- 知识更新只需维护向量库,无需重新训练模型
- 回答可追溯来源,增强可信度
3. 实现细节与配置
3.1 环境准备与依赖配置
3.1.1 Maven依赖详解
除了文中提到的核心依赖,实际项目中还需要注意:
xml复制<!-- 必须与Spring Boot版本匹配的BOM -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>0.8.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 实际业务中常用的额外依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
关键点:
- 始终通过BOM管理版本,避免依赖冲突
- 生产环境建议添加Actuator用于健康检查
- Retry模块对处理向量搜索的偶发超时很有帮助
3.1.2 Milvus配置优化
application.yml中的配置可以进一步优化:
yaml复制vectorstore:
milvus:
initialize-schema: true
client:
host: "127.0.0.1"
port: 19530
connect-timeout: 5000
keep-alive-time: 180
collection:
shards-num: 2
consistency-level: BOUNDED
index:
nlist: 1024 # 聚类中心数,影响搜索精度和性能
nprobe: 32 # 搜索时探查的聚类中心数
重要参数说明:
nlist:值越大搜索精度越高,但会消耗更多内存nprobe:平衡查询延迟和召回率的关键参数consistency-level:根据业务对一致性的要求选择
3.2 数据准备与向量化
3.2.1 文档预处理实践
实际业务中,原始数据往往需要预处理:
java复制public List<Document> preprocessDocuments(List<String> rawTexts) {
return rawTexts.stream()
.map(text -> {
// 移除特殊字符
String cleaned = text.replaceAll("[\\x00-\\x1F]", "");
// 分段处理(防止长文本丢失关键信息)
List<String> segments = splitBySentence(cleaned);
return segments.stream()
.map(seg -> new Document(seg, Map.of("source", "internal_kb")))
.collect(Collectors.toList());
})
.flatMap(List::stream)
.collect(Collectors.toList());
}
private List<String> splitBySentence(String text) {
// 实际实现可使用NLP库或基于标点的简单分割
return Arrays.asList(text.split("(?<=[.!?])\\s+"));
}
经验之谈:
- 文档分段长度建议控制在200-500字
- 添加metadata便于后续过滤和溯源
- 对中文文本特别注意去除乱码和特殊符号
3.2.2 批量导入优化
当需要导入大量文档时,建议采用批处理:
java复制@Scheduled(fixedDelay = 3600000) // 每小时执行一次
public void batchImport() {
List<Document> documents = documentReaderService.loadFromDirectory("data/knowledge");
int batchSize = 100;
for (int i = 0; i < documents.size(); i += batchSize) {
List<Document> batch = documents.subList(i, Math.min(i + batchSize, documents.size()));
vectorStore.add(batch);
// 防止速率限制
Thread.sleep(500);
}
}
4. 核心功能实现
4.1 相似性搜索进阶
4.1.1 混合搜索策略
单纯向量搜索有时会漏掉关键词完全匹配的重要文档。可以结合传统全文检索:
java复制public List<Document> hybridSearch(String query) {
// 向量搜索
SearchRequest vectorRequest = SearchRequest.builder()
.query(query)
.topK(3)
.build();
// 关键词搜索(需提前建立倒排索引)
List<Document> keywordResults = keywordSearchService.search(query);
// 结果融合与去重
return Stream.concat(
vectorStore.similaritySearch(vectorRequest).stream(),
keywordResults.stream()
)
.distinct()
.sorted(Comparator.comparingDouble(Document::getScore).reversed())
.collect(Collectors.toList());
}
4.1.2 动态相似度阈值
固定阈值可能不适应所有查询场景:
java复制public List<Document> adaptiveSearch(String query) {
// 简单根据查询长度动态调整
double baseThreshold = 0.3;
double lengthFactor = Math.min(query.length() / 50.0, 1.0);
double threshold = baseThreshold * (1 - lengthFactor * 0.5);
return vectorStore.similaritySearch(
SearchRequest.builder()
.query(query)
.topK(5)
.similarityThreshold(threshold)
.build()
);
}
4.2 RAG集成进阶
4.2.1 自定义提示模板
默认的提示词可能不适合业务场景:
java复制public class CustomPromptAdvisor implements Advisor {
private static final String PROMPT_TEMPLATE = """
你是一名专业的客服助手,请根据以下知识回答问题:
{documents}
当前问题:{question}
回答要求:
- 使用中文回答
- 如知识库无相关信息,明确告知"未找到相关条款"
- 引用具体条款时注明出处
""";
@Override
public void advise(ChatClient.ChatClientRequest request) {
List<Document> docs = // 获取检索结果...
String context = docs.stream()
.map(d -> "【知识片段】"+d.getContent()+"(相关度:"+d.getScore()+")")
.collect(Collectors.joining("\n"));
request.getMessages().add(
new SystemMessage(PROMPT_TEMPLATE
.replace("{documents}", context)
.replace("{question}", request.getLastUserMessage()))
);
}
}
4.2.2 多阶段问答流程
复杂问题可能需要分步处理:
java复制public String multiStageQA(String question) {
// 第一阶段:问题分类
String category = openAiChatClient.prompt()
.system("将用户问题分类为:预订、取消、改签、其他")
.user(question)
.call()
.content();
// 第二阶段:针对性检索
SearchRequest request = SearchRequest.builder()
.query(question)
.filterExpression("category == '"+category+"'") // 元数据过滤
.build();
// 第三阶段:生成回答
return openAiChatClient.prompt()
.advisors(new CustomPromptAdvisor(request))
.user(question)
.call()
.content();
}
5. 生产环境注意事项
5.1 性能优化
-
索引策略:
- 定期重建索引(如每周一次)
- 对热门查询建立专用索引
- 考虑使用HNSW索引替代IVF_FLAT
-
缓存机制:
java复制@Cacheable(value = "vectorSearch", key = "#query") public List<Document> cachedSearch(String query) { return vectorStore.similaritySearch( SearchRequest.builder().query(query).build()); } -
连接池配置:
yaml复制vectorstore: milvus: client: pool: max-active: 20 max-idle: 10 min-idle: 5
5.2 监控与维护
-
健康检查端点:
java复制@RestController @RequestMapping("/api/vector") public class VectorHealthController { @GetMapping("/health") public ResponseEntity<?> healthCheck() { try { vectorStore.similaritySearch( SearchRequest.builder().query("test").topK(1).build()); return ResponseEntity.ok().build(); } catch (Exception e) { return ResponseEntity.status(503).build(); } } } -
关键指标监控:
- 查询延迟(P99)
- 召回率(人工抽样检查)
- 缓存命中率
- 向量存储使用量
-
数据更新策略:
- 增量更新:监听源数据变更事件
- 全量更新:低峰期定时任务
- 版本控制:通过collection命名区分版本
6. 常见问题排查
6.1 典型错误与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 检索结果不相关 | 嵌入模型与查询不匹配 | 确保训练和推理使用相同嵌入模型 |
| 高负载时超时 | 资源不足或参数不当 | 调整nprobe参数,增加节点资源 |
| 分数异常低 | 维度不匹配 | 检查embeddingDimension配置 |
| 更新后结果不变 | 缓存未刷新 | 清除向量搜索缓存 |
6.2 调试技巧
-
向量可视化:
python复制# 临时用Python快速检查向量分布 import matplotlib.pyplot as plt from sklearn.decomposition import PCA vectors = # 从Milvus导出样本向量 pca = PCA(n_components=2) reduced = pca.fit_transform(vectors) plt.scatter(reduced[:,0], reduced[:,1]) plt.show() -
查询分析:
java复制@Aspect @Component public class SearchMonitor { @Around("execution(* org.springframework.ai.vectorstore.VectorStore.similaritySearch(..))") public Object logSearch(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); log.info("Search took {}ms with params {}", System.currentTimeMillis()-start, pjp.getArgs()); return result; } } -
AB测试框架:
java复制public SearchResult evaluateSearchMethods(String query) { // 传统方法 List<Document> keywordResults = keywordSearch(query); // 向量方法 List<Document> vectorResults = vectorSearch(query); // 混合方法 List<Document> hybridResults = hybridSearch(query); return new SearchResult( keywordResults, vectorResults, hybridResults); }
在实际项目中落地RAG系统时,最大的挑战往往不是技术实现,而是知识库的质量管理和持续运营。建议建立专门的内容审核流程,定期评估问答质量,形成数据飞轮不断优化系统表现。
