Java后端接入大模型:LangChain4j+Qwen+Milvus混合检索RAG实战
2026/9/8 13:22:42 网站建设 项目流程

不少 Java 团队在做内部知识库问答时,都会遇到同一个问题:RAG 相关的教程几乎被 Python 生态刷屏了,Java 后端想接入大模型,却找不到一条能直接照着做的链路。LangChain4j 就是这个缺口里比较成熟的解决方案。本文围绕“LangChain4j 入门 + Qwen Embedding 向量化 + Milvus 存储 + 混合检索与重排”这条完整链路展开,梳理概念、版本、可运行代码和排错方案,适合刚从零接触大模型开发的 Java 工程师,也适合想把手头文档问答能力落到项目里的后端团队。

本文以 2026 年初的 LangChain4j 1.x 系列为主要参考。不过 LLM 框架迭代速度很快,示例代码中的 API 名称、依赖坐标和模型接口都有可能调整。遇到版本差异时,优先以你实际引入的 JAR 包的 Javadoc 为准。

1. 为什么 Java 开发者也关心 LangChain4j

1.1 LangChain4j 到底是什么

LangChain4j 是一个面向 JVM 生态的大模型应用开发框架。它为 Java / Kotlin / Scala 等语言提供了统一的大模型调用抽象,避免你在代码里直接拼 HTTP 请求、手工解析 JSON、自己设计消息结构。

它的核心理念和 Python 的 LangChain 一脉相承,但并不是简单“翻译”过来的版本。LangChain4j 更贴近 Java 工程的习惯:使用 Builder 模式构建对象,默认支持 Spring Boot 自动装配,把流式输出封装成TokenStream,与EmbeddingStore配合时也能走完整的 RAG 链路。

一个最简单的 LangChain4j 使用场景是这样:

ChatLanguageModel chatModel = OpenAiChatModel.builder() .apiKey("your-api-key") .modelName("qwen-plus") .build(); String answer = chatModel.generate("用一句话介绍 Java"); System.out.println(answer);

你只负责配置模型和发起调用,消息拼接、token 计费等细节都由框架完成。

1.2 它能解决哪些日常开发问题

在真实的后端项目里,直接调大模型 API 会遇到一些重复性问题:

  • 聊天历史要自己维护,多轮对话越写越乱。
  • 文档切分、向量化、存入向量库的代码到处复制。
  • 每换一个模型厂商,就要重新封装一次 API 签名。
  • RAG 检索结果与 Prompt 拼装的流程没有标准化。

LangChain4j 用一套可插拔的接口把这些问题串了起来。你写一套代码,可以通过不同实现接入 OpenAI、DashScope、Ollama、本地 vLLM 服务等渠道;切换模型厂商时,改动集中在配置层。这也是很多 Java 后端团队优先选择它的原因。

1.3 与 Python LangChain 的差异

不要把 LangChain4j 当成 Python 版的 1:1 复制。两者在模块划分上有些对应,但 LangChain4j 做了很多 JVM 生态的适配。例如:

  • 原生支持StreamingChatLanguageModel流式响应。
  • 通过AiServices让大模型直接调用你的 Java 方法。
  • 内置EmbeddingStore抽象,可以对接 Milvus、OpenSearch、PgVector、Redis 等。
  • 提供 Spring Boot Starter,依赖注入非常方便。

如果你的团队都是 Java 技术栈,引入 LangChain4j 的维护成本通常比硬套 Python 微服务更低,调试链路也更短。

2. 环境准备与版本规划

2.1 基础环境清单

本文的实战示例会用到下面的环境。版本号不必完全一致,重点是思路能复用。

组件说明
JDK建议 JDK 17 及以上,LangChain4j 1.x 已全面支持
Maven3.8+ 即可,也可以用 Gradle
Spring Boot3.2 或 3.3 均可,配合对应 Spring Boot Starter
Milvus2.4 及以上版本,推荐先通过 Docker 启动单机版
大模型服务支持 OpenAI 兼容协议的接口,例如阿里云 DashScope 兼容模式
操作系统Windows / Linux / macOS 均可,本文命令以 Linux 为例

Milvus 单机版可以通过 Docker 快速启动。先确认机器上已经安装 Docker,然后执行:

docker run -d \ --name milvus \ -p 19530:19530 \ -p 9091:9091 \ milvusdb/milvus:latest

Milvus 默认端口是 19530(gRPC),9091 是监控管理端口。生产环境不建议直接用 latest,最好锁定官方发布的稳定标签,具体镜像版本以 Milvus 官方文档为准。

2.2 初始化 Maven 项目

建议创建一个独立的 Maven 工程来跑 Demo 工程。示例项目结构如下:

rag-demo/ ├── pom.xml └── src/main/java/ └── com/example/rag/ ├── RagDemoApplication.java ├── config/ │ └── ModelConfig.java └── service/ ├── DocumentImportService.java └── RagSearchService.java

创建 Spring Boot 工程时,可以直接使用 Spring Initializr,也可以手动生成一个最简的pom.xml

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <langchain4j.version>1.0.0-beta1</langchain4j.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-community-milvus</artifactId> <version>${langchain4j.version}</version> </dependency> </dependencies>

这里把langchain4j-community-milvus单独列出来,是因为 Milvus 的向量库集成在社区扩展包里,而不是核心包里。不同版本之间可能移动包路径,引入依赖后建议马上写一个最小的连接测试,确认编译和运行都能通过。

2.3 版本选择建议

LangChain4j 从 0.36 到 1.x 做了一次比较大的 API 整合。很多博客里的示例是基于 0.30 左右的老版本,如果你直接复制到 1.x 工程里,可能会遇到EmbeddingModelChatLanguageModel的包名或方法签名变化。

建议优先选择当前 Maven 中央仓库中已发布的最新稳定版。如果你的项目已经有其他依赖(比如 Spring Boot 版本、milvus-sdk-java),重点关注这些库和 LangChain4j 是否存在冲突。版本策略是“从新工程用新版本,老工程改造时小步升级”。

3. 核心 API 拆解:从模型到向量存储

3.1 ChatLanguageModel:对话入口

ChatLanguageModel是 LangChain4j 中最核心的接口,负责与大模型完成一次对话生成。它屏蔽了底层 HTTP 请求细节,提供同步、流式、带消息历史的多种能力。

ChatLanguageModel model = OpenAiChatModel.builder() .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1") .apiKey(System.getenv("DASHSCOPE_API_KEY")) .modelName("qwen-plus") .build(); String response = model.generate("什么是 RAG?");

需要注意,baseUrl要指向兼容 OpenAI 协议的网关地址。阿里云 DashScope 提供了一个兼容模式,把 LangChain4j 的 OpenAI 模块指向这个地址,就可以复用现有代码接入 Qwen 系列模型。

3.2 EmbeddingModel:如何把文本变成向量

EmbeddingModel 用于将一段文本转换为向量数组。这个向量不是随便生成的特征,而是模型在训练中学到的语义表示。文本语义越接近,向量在空间中的距离就越近。

EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1") .apiKey(System.getenv("DASHSCOPE_API_KEY")) .modelName("text-embedding-v3") .build(); Response<Embedding> response = embeddingModel.embed("Java 内存模型"); Embedding embedding = response.content(); float[] vector = embedding.vector();

在 Milvus 中创建集合时,维度必须和 Embedding 模型的输出维度保持一致。如果指定错了维度,插入向量时会报维度校验错误。

3.3 EmbeddingStore:向量库的接入抽象

EmbeddingStore<TextSegment>是 LangChain4j 对向量数据库的抽象。它提供了add()search()等方法,底层会根据实现类连接到 Milvus、OpenSearch、Redis 等系统。

对开发者来说,接入 Milvus 的代码非常简洁:

EmbeddingStore<TextSegment> embeddingStore = MilvusEmbeddingStore.builder() .host("localhost") .port(19530) .collectionName("java_doc_collection") .dimension(1024) .build();

这段代码会在首次写入时自动创建 Collection,前提是dimension字段与 Embedding 模型输出一致。生产环境建议预先在 Milvus 中手动创建 Collection,并配置索引参数。

3.4 什么是 RAG、混合检索和重排

RAG(检索增强生成)是当前知识库问答的主流方案。它先把文档切成片段,向量化后存入向量库;用户提问时,先从向量库检索相关片段,再把片段拼进 Prompt 交给大模型生成答案。

单纯依赖向量检索并不完美。向量检索擅长语义相似,但关键词精确匹配能力弱;关键词检索负责精确匹配,却缺乏语义理解。因此,越来越多的项目采用“混合检索”:同时跑向量检索和关键词检索,再把结果合并排序。合并排序会用到 RRF(Reciprocal Rank Fusion)等算法。

重排发生在检索之后、生成答案之前。第一轮检索通常会召回过多样本,重排模型根据 Query 与文档的语义相关度重新打分,去掉噪声,把真正有用的片段排在前面。混合检索解决“召回多”的问题,重排解决“排序准”的问题,两者互补。

4. 完整实战:Qwen Embedding + Milvus 向量库 + 重排问答链路

下面以一个“Java 技术文档问答 Demo”为例,把整个流程走通。

4.1 创建 Spring Boot 工程与依赖

pom.xml中加入依赖后,编写启动类:

package com.example.rag; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RagDemoApplication { public static void main(String[] args) { SpringApplication.run(RagDemoApplication.class, args); } }

启动前准备好环境变量:

export DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxxx

如果你的 API Key 来自其他平台,也可以对应修改代码里的 Base URL 和模型名。

4.2 配置 DashScope 兼容接口的模型客户端

为了让几个 Service 共用模型实例,我把ChatLanguageModelEmbeddingModel都注册成 Spring Bean。这样后续业务类通过构造器注入,代码更干净。

package com.example.rag.config; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.openai.OpenAiEmbeddingModel; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ModelConfig { @Bean public ChatLanguageModel chatLanguageModel() { String apiKey = System.getenv("DASHSCOPE_API_KEY"); return OpenAiChatModel.builder() .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1") .apiKey(apiKey) .modelName("qwen-plus") .build(); } @Bean public EmbeddingModel embeddingModel() { String apiKey = System.getenv("DASHSCOPE_API_KEY"); return OpenAiEmbeddingModel.builder() .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1") .apiKey(apiKey) .modelName("text-embedding-v3") .build(); } }

这里没有把 API Key 写死在代码里。通过环境变量配置密钥,是避免密钥进入 Git 仓库的最基本操作。

4.3 接入 Milvus 存储向量

连接 Milvus 的 Bean 需要一点额外处理。因为MilvusEmbeddingStore在 community 包中,不同版本对构造器的要求略有差异。下面是一个常见的构建方式:

package com.example.rag.config; import dev.langchain4j.community.store.embedding.milvus.MilvusEmbeddingStore; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.store.embedding.EmbeddingStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MilvusConfig { @Bean public EmbeddingStore<TextSegment> embeddingStore() { return MilvusEmbeddingStore.builder() .host("localhost") .port(19530) .collectionName("java_doc_collection") .dimension(1024) .build(); } }

如果dimension和实际 Embedding 输出不一致,Milvus 会在写入时抛出异常。建议本地先写一个测试方法,打印一下embedding.vector().length,再确定 Collection 的维度。

4.4 导入文档并向量化

文档导入是整个 RAG 链路的数据准备阶段。这里用一个简单工具类读取文本文件,切成TextSegment,然后向量化并写入 Milvus。

package com.example.rag.service; import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingStore; import org.springframework.stereotype.Service; import java.util.List; @Service public class DocumentImportService { private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; public DocumentImportService(EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore) { this.embeddingModel = embeddingModel; this.embeddingStore = embeddingStore; } public void importDocument(Document document) { List<TextSegment> segments = DocumentSplitters.recursive(500, 50) .split(document); for (TextSegment segment : segments) { Embedding embedding = embeddingModel.embed(segment.text()).content(); embeddingStore.add(embedding, segment); } } }

关于切分参数:recursive(500, 50)表示每个片段尽量控制在 500 字符以内,片段之间重叠 50 字符。重叠能避免语义断层——比如一句话刚好被切到上一段的末尾,下一段开头又缺少上下文。实际项目中,切分长度需要根据文档类型和模型窗口调整,没有万能参数。

4.5 混合检索与重排实现

混合检索的第一步是先分别做向量检索和关键词检索。这里我把关键词检索简化成一个文本打分方法,方便看清单个环节的处理逻辑。

package com.example.rag.service; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingMatch; import dev.langchain4j.store.embedding.EmbeddingStore; import org.springframework.stereotype.Service; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @Service public class RagSearchService { private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; public RagSearchService(EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore) { this.embeddingModel = embeddingModel; this.embeddingStore = embeddingStore; } public List<String> hybridSearch(String query) { Embedding queryEmbedding = embeddingModel.embed(query).content(); List<EmbeddingMatch<TextSegment>> vectorResults = embeddingStore.findRelevant(queryEmbedding, 10); List<String> keywordResults = keywordSearch(query); // RRF 合并 Map<String, Double> rrfScores = new ConcurrentHashMap<>(); double k = 60.0; for (int i = 0; i < vectorResults.size(); i++) { String text = vectorResults.get(i).embedded().text(); rrfScores.merge(text, 1.0 / (k + i + 1), Double::sum); } for (int i = 0; i < keywordResults.size(); i++) { String text = keywordResults.get(i); rrfScores.merge(text, 1.0 / (k + i + 1), Double::sum); } return rrfScores.entrySet().stream() .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) .map(Map.Entry::getKey) .limit(5) .collect(Collectors.toList()); } private List<String> keywordSearch(String query) { // 真正的关键词检索可以走 Milvus Sparse Vector、Elasticsearch 或 Lucene 倒排索引 // 这里用一个简单的包含提示方法,仅用于演示链路 return embeddingStore.search(query, 10); } }

这里需要说明:Milvus 2.4 之后提供了 Sparse Vector 和混合检索能力,用 Java SDK 可以直接发起一次包含 Dense + Sparse 的混合搜索。不过不同 SDK 版本的方法名和参数变化较大,建议一开始先用 RRF 把自己的链路跑通,等整体流程稳定后,再替换为 Milvus 原生混合检索 API。

重排放在 RRF 之后。比如把候选文档喂给一个 Rerank 服务:

public List<String> rerank(String query, List<String> docs) { // 生产环境建议换成专门的重排模型接口 // LangChain4j 1.x 已经提供 ReRankingModel 抽象,不同模块的实现类名不同 // 这里先用一个基于文本重叠度的简化打分,演示重排在链路中的位置 return docs.stream() .map(doc -> Map.entry(doc, lexicalScore(query, doc))) .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) .map(Map.Entry::getKey) .limit(3) .collect(Collectors.toList()); } private double lexicalScore(String query, String doc) { int count = 0; for (String word : query.split(" ")) { if (doc.contains(word)) { count++; } } return count; }

你可以把lexicalScore替换成对 Cohere Rerank、阿里云 text-rerank 等服务的 HTTP 调用。重排的目的不是替代检索,而是在检索结果较粗的情况下,把准确率再提一层。

4.6 基于检索结果完成问答

最后把检索片段和用户问题一起传给大模型。

public String answer(String query) { List<String> relatedDocs = hybridSearch(query); List<String> rerankedDocs = rerank(query, relatedDocs); StringBuilder context = new StringBuilder(); for (String doc : rerankedDocs) { context.append(doc).append("\n---\n"); } ChatLanguageModel chatModel = chatLanguageModel(); String prompt = """ 请根据以下资料回答问题。 如果资料中没有答案,请直接说明“资料中未找到相关信息”,不要编造。 资料: %s 问题: %s """.formatted(context, query); return chatModel.generate(prompt); }

这里把 Prompt 设计成“没有答案就明说”的模式,能有效减少大模型在知识库问答中的幻觉。很多初版 RAG 项目都忽略了这个细节,导致模型一本正经地编造答案。

5. 常见问题与排查思路

5.1 启动时报找不到 MilvusEmbeddingStore

问题现象常见原因解决思路
编译时报MilvusEmbeddingStore不存在引入的依赖模块不对,或者版本太旧包名不同检查langchain4j-community-milvus是否在依赖中,并查看实际 JAR 包的类路径
运行时连接 Milvus 超时Milvus 容器未启动,或者端口配置错误执行docker ps确认容器状态,用telnet localhost 19530验证端口连通性
插入向量时报维度错误dimension与 Embedding 输出维度不一致打印embedding.vector().length,修改 Collection 的维度配置
查询结果为空Collection 中没有数据,或者检索参数太严格先执行全量导入,再用metadata过滤排查

5.2 DashScope 接口调用返回 401

401 表示鉴权失败。检查环境变量是否真的生效:

echo $DASHSCOPE_API_KEY

同时确认 Base URL 是否正确。DashScope 的 OpenAI 兼容地址是:

https://dashscope.aliyuncs.com/compatible-mode/v1

很多文章里的旧地址是https://dashscope.aliyuncs.com/api/v1,两种地址的请求格式不同,不要混用。

5.3 LangChain4j 版本更新后 API 变动

LangChain4j 在 1.x 阶段对包结构进行了梳理,社区模块的命名也统一了。如果你在网上找到的示例与本地代码不一致,先看三处:

  1. ChatLanguageModelEmbeddingModel的 import 路径。
  2. MilvusEmbeddingStore是核心模块还是 community 模块。
  3. embeddingModel.embed()返回的是Response<Embedding>还是Embedding

遇到不确定的类,打开 IDE 的External Libraries,直接查langchain4j*.jar里的类和方法,比反复试错更快。

6. 最佳实践与工程建议

6.1 配置管理

大模型 API Key、Base URL、Collection 名称等重要配置,不能写在业务代码里。推荐放到 Spring 的配置文件,并通过环境变量注入:

langchain4j.openai.base-url=${AI_BASE_URL:https://dashscope.aliyuncs.com/compatible-mode/v1} langchain4j.openai.api-key=${AI_API_KEY:} langchain4j.milvus.host=${MILVUS_HOST:localhost} langchain4j.milvus.port=${MILVUS_PORT:19530}

注意把真实密钥放在本地的application-local.yml,并加入.gitignore。生产环境使用密钥管理服务下发密钥,不要用硬编码。

6.2 向量化与索引策略

文本切分和索引参数会直接影响检索质量。建议把这几项纳入测试:

  • 切分窗口大小:通常 300~800 字符,具体要结合文档内容密度调整。
  • 重叠字符数:一般取切分窗口的 10%~20%。
  • Milvus 索引类型:IVF_FLAT适合数据量较大、检索性能要求高的场景;HNSW在召回率和查询性能上更均衡。
  • 标量字段过滤:如果文档带有部门、时间、文档类型等元数据,尽量存储为标量字段,查询时先通过标量过滤缩小范围,再走向量检索。

6.3 重排模型的选择

重排模型不能随意替换。要在自己的业务数据上做了离线评测再上线。最简单的方法是准备一批 Query 和正误文档,对比“单纯向量检索 + 重排”和“混合检索 + 重排”的结果,选择能稳定提升准确率的那套配置。

6.4 异常处理与降级

大模型接口延迟高、不稳定,生产链路不能因为一次模型超时就让整个请求失败。建议在 RAG 调用链路上做多层降级:

  • 先尝试完整 RAG 链路。
  • 如果大模型超时,直接返回检索到的文档摘要。
  • 如果检索服务异常,返回兜底提示语并记录日志。

调用大模型时,OpenAiChatModel的 Builder 提供了timeout()等方法,可以按实际业务调整超时时间。不要让默认超时拖垮接口整体响应。

6.5 数据安全与合规

知识库中往往包含内部敏感文档。对文档导入、检索、问答三个阶段都要做权限控制:

  • 导入阶段:记录文档来源、上传人、密级。
  • 检索阶段:按用户权限过滤标量字段,防止低权限用户检索到高密级文档。
  • 问答阶段:不要把所有检索结果都拼进 Prompt,先做权限过滤和敏感词检测。

这里建议把 RAG 服务拆成独立的鉴权接口,不要直接在 Controller 里透传检索结果。

7. 小结与下一步学习路线

本文完整梳理了 LangChain4j 从入门到项目落地的关键环节:核心 API、环境配置、Qwen Embedding 接入、Milvus 存储,以及混合检索和重排的工程实现思路。

如果你是从零开始,建议按下面的顺序推进学习:

  1. 先把ChatLanguageModel跑通,完成一次最简单的对话。
  2. 再接入EmbeddingModel,了解向量化后的数据长什么样。
  3. 然后配置EmbeddingStore,用 100 条文档验证“导入—检索”闭环。
  4. 之后再做 RRF 混合检索,替换成 Milvus 原生混合检索 API。
  5. 最后引入重排模型,做离线评测和调参。

在实际项目中,RAG 效果瓶颈往往不在代码,而在文档切分、检索召回和重排质量上。框架只是帮你把链路串起来,真正决定体验的是数据准备与持续优化。先从最基础的消息模型跑通,再逐步加入向量库与重排链路,这样即使中间出了偏差,也不需要推翻重来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询