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);
}

逐行解析这个“魔法”接口:

  1. @AiService:标记这是一个AI服务接口。里面的参数显式引用了我们之前定义的三个核心Bean。
  2. @SystemMessage:这是给AI的“角色设定”和系统指令。它会被预置到每次对话的上下文中,强烈影响AI的行为。这里我们明确要求它做基于上下文的问答(RAG),并规定了无法回答时的应对策略。
  3. 方法签名: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可能做了基础封装,但高并发下需要关注。可以考虑在MilvusEmbeddingStore builder中配置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的模块化设计让这些扩展变得相当直观。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐