Spring Boot 3.5 + LangChain4j实战:5分钟搞定Milvus向量数据库集成(含阿里百炼模型配置)
Spring Boot 3.5 + LangChain4j实战:5分钟搞定Milvus向量数据库集成(含阿里百炼模型配置)
如果你是一名Java开发者,最近被各种AI应用撩得心痒痒,想在自己的Spring Boot项目里快速接入一个能聊、能记、还能从文档里找答案的智能助手,但又不想陷入繁琐的配置和漫长的学习曲线,那么这篇文章就是为你准备的。我们直接切入正题,不谈宏大叙事,只聚焦于一个核心目标:在5分钟内,将一个具备会话管理和RAG检索能力的AI大脑,塞进你的Spring Boot 3.5应用里。
这听起来有点夸张,但得益于LangChain4j这个Java生态的AI编排框架,以及Milvus这类开箱即用的向量数据库,快速集成已经不再是梦想。我们这次的主角是阿里百炼的qwen-plus模型,它提供了强大的推理能力,并且LangChain4j已经对其做了良好的兼容性封装。整个过程,你将体验到一种“声明式”的AI集成快感——大部分工作就是写几个配置Bean,定义个接口,剩下的,框架帮你搞定。
1. 极速启动:项目骨架与核心依赖
别从零开始折腾了,我们的目标是快。直接用Spring Initializr创建一个新项目,或者在你现有的Spring Boot 3.5项目里,把下面这些依赖加进去。这些依赖构成了我们AI能力的基石。
打开你的pom.xml,确保包含以下关键依赖:
<!-- LangChain4j 核心启动器,提供AI服务的基础设施 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.0.0-beta3</version>
</dependency>
<!-- 用于兼容阿里百炼等OpenAI兼容API的启动器 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.0.0-beta3</version>
</dependency>
<!-- Milvus向量数据库的集成支持 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-milvus-spring-boot-starter</artifactId>
<version>1.0.0-beta3</version>
</dependency>
<!-- 一个轻量级但效果不错的默认嵌入模型 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
<version>1.2.0-beta8</version>
</dependency>
<!-- WebFlux用于支持流式响应,让AI回答像打字一样逐个词蹦出来 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
注意:版本号请根据实际情况调整,但建议使用较新的Beta版以获取最新特性和对Spring Boot 3.5的更好支持。如果遇到依赖冲突,尝试使用
<exclusions>或统一管理版本。
依赖搞定后,我们先把Milvus跑起来。如果你已经有一个Milvus实例(无论是Docker、云服务还是本地安装),可以跳过这一步。如果没有,用Docker启动一个单机版是最快的方式:
docker run -d --name milvus-standalone \
-p 19530:19530 \
-p 9091:9091 \
milvusdb/milvus:v2.3.4-standalone
这条命令会在后台启动一个Milvus容器,并将向量数据库服务的端口19530和管理端口9091映射到宿主机。用docker logs milvus-standalone看看日志,出现“Milvus successfully started”就说明它已经准备好为你存储向量了。
2. 核心配置:连接模型与数据库
现在进入核心环节——配置。我们需要告诉应用三件事:1. 用什么AI模型;2. 向量存到哪里;3. 怎么从向量里检索信息。这些都在一个配置类里完成,清晰又集中。
创建一个AiConfig类(或者任何你喜欢的名字),加上@Configuration注解。首先,配置阿里百炼模型:
@Bean
public OpenAiChatModel openAiChatModel() {
return OpenAiChatModel.builder()
.apiKey("sk-你的阿里云API密钥") // 重要:建议从环境变量或配置中心读取
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.modelName("qwen-plus")
.temperature(0.7) // 控制创造性,0更确定,1更随机
.maxTokens(2000) // 限制单次回复长度
.timeout(Duration.ofSeconds(60))
.logRequests(true) // 开发阶段开启,方便调试
.logResponses(true)
.build();
}
这里有几个关键点:
baseUrl:必须设置为阿里百炼的兼容端点,这是它能正确工作的前提。apiKey:务必妥善保管,不要硬编码在代码中提交到版本库。使用@Value("${aliyun.api-key}")从application.yml读取是更好的实践。modelName:除了qwen-plus,你也可以尝试qwen-max(能力更强)或qwen-turbo(速度更快、成本更低),根据你的需求平衡。
接下来,配置Milvus作为向量存储。这里需要特别注意维度匹配问题:
@Bean
public MilvusEmbeddingStore embeddingStore() {
return MilvusEmbeddingStore.builder()
.host("localhost")
.port(19530)
.collectionName("my_knowledge_base") // 集合名,相当于数据库表
.dimension(384) // 关键!必须与嵌入模型输出维度一致
.indexType(IndexType.FLAT) // FLAT索引精度最高,适合中小规模数据
.metricType(MetricType.COSINE) // 余弦相似度,最常用的语义相似度度量
.consistencyLevel(ConsistencyLevelEnum.EVENTUALLY)
.build();
}
为什么维度384这么重要? 我们使用的默认嵌入模型all-MiniLM-L6-v2会将任何一段文本转换成一个384维的浮点数向量(即一组长384的数字列表)。这个向量就是文本的“数学指纹”。Milvus在创建集合(collection)时,必须预先知道它要存储的向量是几维的。如果这里填写的dimension与模型实际产生的维度不匹配,插入数据时就会报错。如果你后续换用其他嵌入模型(如OpenAI的text-embedding-3-small是1536维),这里必须同步修改。
为了让你更直观地了解不同配置的选择,这里有一个快速参考表格:
| 配置项 | 推荐值 | 其他选项与说明 |
|---|---|---|
| 索引类型 (IndexType) | FLAT | 精确检索,百分百准确,适合数据量小于100万。数据量大时可考虑IVF_FLAT(平衡速度与精度)或HNSW(速度快,精度略降)。 |
| 度量类型 (MetricType) | COSINE | 余弦相似度,最适用于文本语义相似度。L2(欧氏距离)和IP(内积)在某些特定场景下使用。 |
| 一致性级别 | EVENTUALLY | 最终一致,写入性能最好。STRONG(强一致)保证读写立即可见,但性能有损耗。 |
| 自动刷盘 | 默认false | 为true时每次插入立即持久化,更安全但慢;false时依赖后台刷盘,性能好。 |
配置好存储,下一步是配置检索器(Retriever),它决定了如何从向量库中找出最相关的文档片段:
@Bean
public ContentRetriever contentRetriever(MilvusEmbeddingStore embeddingStore) {
// 1. 使用与上面维度匹配的嵌入模型
EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();
// 2. 文档分割器:这里按段落分割,每段最多1000字符,相邻段重叠50字符防止割裂语义
DocumentSplitter splitter = new DocumentByParagraphSplitter(1000, 50);
// 3. 构建一个“摄取器”,负责把原始文档切片、向量化并存入Milvus
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(splitter)
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
// 4. 假设你的文档放在resources/docs下,加载并处理它们
// 首次运行后,数据已存入向量库,这步可以注释掉或改为增量更新逻辑
// List<Document> documents = FileSystemDocumentLoader.loadDocuments("src/main/resources/docs");
// ingestor.ingest(documents);
// 5. 返回检索器,它将用于每次提问时的向量相似度搜索
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(3) // 每次检索返回最相关的3个片段
.minScore(0.75) // 相似度分数阈值,低于0.75的结果将被过滤,保证相关性
.build();
}
提示:
ingestor.ingest(documents)这行代码是数据初始化步骤。在实际项目中,你应该将其放在一个独立的初始化服务或命令行Runner中,而不是每次启动都执行。对于生产环境,需要考虑增量更新、去重和更复杂的分块策略。
3. 赋予记忆:实现多会话隔离
一个没有记忆的AI,每次对话都是“初见”。为了实现多用户或多对话线程的隔离,我们需要引入聊天记忆(Chat Memory)。LangChain4j通过ChatMemoryProvider来管理不同会话的记忆上下文。
@Bean
public ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId) // 关键:用这个ID区分不同会话,通常用用户ID或会话ID
.maxMessages(20) // 保留最近20轮对话作为上下文
.build();
}
这个Bean的作用像一个记忆工厂。当AI服务需要为一个特定的memoryId(例如从HTTP请求中传来的sessionId)提供记忆时,它就调用这个工厂方法。MessageWindowChatMemory是一种滑动窗口式的记忆,只保留最近的若干条消息,既能提供上下文,又避免了上下文过长导致的模型性能下降和成本增加。
你可以根据场景选择其他记忆类型,比如TokenWindowChatMemory是基于Token数量限制,更适合按Token计费的模型。
4. 定义AI服务:用声明式接口召唤智能
这是最体现LangChain4j优雅之处的部分。你不需要写复杂的调用逻辑,只需要定义一个接口,加上注解,框架就会自动生成一个代理实现。
import dev.langchain4j.service.AiService;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import reactor.core.publisher.Flux;
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT, // 显式指定依赖的Bean名
chatModel = "openAiChatModel",
chatMemoryProvider = "chatMemoryProvider",
contentRetriever = "contentRetriever"
)
public interface Assistant {
@SystemMessage("""
你是一个专业的文档分析助手,名字叫“小智”。
请严格根据提供的文档上下文来回答问题。
如果上下文信息不足以回答问题,请如实告知“根据现有资料,我无法回答这个问题”。
回答请保持专业、清晰、简洁。
""")
Flux<String> chat(@MemoryId String sessionId, @UserMessage String userMessage);
}
逐行解析这个“魔法”接口:
@AiService:标记这是一个AI服务接口。里面的参数显式引用了我们之前定义的三个核心Bean。@SystemMessage:这是给AI的“角色设定”和系统指令。它会被预置到每次对话的上下文中,强烈影响AI的行为。这里我们明确要求它做基于上下文的问答(RAG),并规定了无法回答时的应对策略。- 方法签名:
Flux<String>表示返回一个响应式流,用于支持流式输出(SSE)。@MemoryId注解的参数会传递给ChatMemoryProvider来获取或创建对应的记忆。@UserMessage注解的参数就是用户的问题。
就这样,一个具备记忆、检索和流式回答能力的AI服务接口就定义完了。你可能会觉得“这就完了?”,是的,核心逻辑框架已经帮你封装好了。
5. 暴露API与流式响应
最后一步,我们需要一个控制器(Controller)来接收HTTP请求,调用上面定义的AI服务,并将流式响应返回给前端。
@RestController
@RequiredArgsConstructor // 使用Lombok简化注入
public class AiController {
private final Assistant assistant;
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestParam String message,
@RequestParam(defaultValue = "default") String sessionId) {
// 直接调用AI服务,sessionId用于隔离对话记忆
return assistant.chat(sessionId, message);
}
// 可选:一个简单的文档上传并向量化的端点
@PostMapping("/ingest")
public ResponseEntity<String> ingestDocument(@RequestParam("file") MultipartFile file) throws IOException {
// 这里简化为示例,实际应解析文件内容,调用前面提到的ingestor
String text = new String(file.getBytes());
// 伪代码: ingestor.ingest(Document.from(text));
return ResponseEntity.ok("文档已处理并存入知识库");
}
}
这个/chat端点做了几件事:
- 接收用户消息和一个可选的
sessionId(前端可以用UUID生成)。 - 将
sessionId和message传递给AI服务。 - 设置响应内容类型为
TEXT_EVENT_STREAM_VALUE,这是支持Server-Sent Events (SSE) 协议的关键,允许服务器持续向客户端推送数据。 - 返回
Flux<String>,AI模型生成的每个词或词块都会作为一个事件推送给客户端,实现打字机效果。
你可以用curl命令立刻测试一下:
curl -N "http://localhost:8080/chat?message=Spring Boot是什么?&sessionId=user_001"
如果一切顺利,你应该能看到答案像流水一样,逐字逐句地出现在终端里。
6. 进阶技巧与避坑指南
走到这里,一个基础可用的AI聊天接口已经搭建完毕。但想让它更健壮、更实用,还需要考虑一些进阶问题。
性能与稳定性调优
- 超时与重试:在模型配置中合理设置
timeout和maxRetries。网络波动或模型服务暂时不可用是常态,有重试机制能提升用户体验。 - 连接池:对于Milvus,生产环境建议配置连接池。虽然starter可能做了基础封装,但高并发下需要关注。可以考虑在
MilvusEmbeddingStorebuilder中配置client参数,传入一个自定义的、带连接池配置的MilvusClient。 - 异步化:AI模型调用和向量检索都是I/O密集型操作。确保你的服务线程(如Tomcat或Netty工作线程)不被阻塞。我们的Controller返回
Flux已经是响应式非阻塞的,但如果你在服务层有复杂逻辑,也要注意使用异步编程模型。
RAG效果提升
RAG的效果很大程度上取决于“检索”的质量。如果检索不到相关文档,再强的模型也白搭。
- 分块策略:
DocumentByParagraphSplitter只是基础。对于代码、Markdown、PDF等结构化文档,有更优的分割器。可以尝试DocumentBySentenceSplitter,或使用更高级的基于语义的递归分割。 - 元数据过滤:除了语义相似度,检索时还可以结合元数据(如文档来源、章节、日期)进行过滤。Milvus支持标量字段与向量字段的混合查询,可以在定义Collection Schema时提前规划。
- 重排序(Re-ranking):简单的向量相似度检索可能会返回一些“似是而非”的片段。可以引入一个轻量级的重排序模型(如
BGE-reranker),对Top K的检索结果进行二次排序,将最相关的结果排到最前面,显著提升最终答案的准确性。
生产环境部署考量
- 配置外部化:所有敏感信息(API Key、数据库连接串)和可变参数(模型温度、最大Token数)必须放到
application.yml或配置中心(如Nacos、Apollo)。 - 健康检查:为Milvus和外部模型API配置健康检查端点。Spring Boot Actuator可以很方便地集成自定义健康指示器。
- 监控与日志:开启模型的
logRequests和logResponses在开发时很有用,但在生产环境要谨慎,避免日志泄露敏感数据或过于庞大。建议将AI调用日志(特别是Token消耗)接入专门的监控系统,便于成本分析。 - 异常处理:在Controller或使用全局异常处理器(
@ControllerAdvice)中,妥善处理模型调用超时、额度不足、向量库连接失败等异常,给前端返回友好的错误信息。
我自己的经验是,第一次跑通整个流程后,最大的成就感来自于那种“连接”的快感——看着自己熟悉的Java代码和Spring生态,与前沿的AI能力无缝对接。接下来,你可以尝试用更复杂的文档(比如整个产品手册)来喂养你的知识库,或者把AI服务嵌入到一个更复杂的业务工作流中。LangChain4j的模块化设计让这些扩展变得相当直观。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)