Java后端接入大模型:LangChain4j+Qwen+Milvus混合检索RAG实战

Java后端接入大模型:LangChain4j+Qwen+Milvus混合检索RAG实战 不少 Java 团队在做内部知识库问答时都会遇到同一个问题RAG 相关的教程几乎被 Python 生态刷屏了Java 后端想接入大模型却找不到一条能直接照着做的链路。LangChain4j 就是这个缺口里比较成熟的解决方案。本文围绕“LangChain4j 入门 Qwen Embedding 向量化 Milvus 存储 混合检索与重排”这条完整链路展开梳理概念、版本、可运行代码和排错方案适合刚从零接触大模型开发的 Java 工程师也适合想把手头文档问答能力落到项目里的后端团队。本文以 2026 年初的 LangChain4j 1.x 系列为主要参考。不过 LLM 框架迭代速度很快示例代码中的 API 名称、依赖坐标和模型接口都有可能调整。遇到版本差异时优先以你实际引入的 JAR 包的 Javadoc 为准。1. 为什么 Java 开发者也关心 LangChain4j1.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 即可也可以用 GradleSpring Boot3.2 或 3.3 均可配合对应 Spring Boot StarterMilvus2.4 及以上版本推荐先通过 Docker 启动单机版大模型服务支持 OpenAI 兼容协议的接口例如阿里云 DashScope 兼容模式操作系统Windows / Linux / macOS 均可本文命令以 Linux 为例Milvus 单机版可以通过 Docker 快速启动。先确认机器上已经安装 Docker然后执行docker run -d \ --name milvus \ -p 19530:19530 \ -p 9091:9091 \ milvusdb/milvus:latestMilvus 默认端口是 19530gRPC9091 是监控管理端口。生产环境不建议直接用 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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version relativePath/ /parent properties java.version17/java.version langchain4j.version1.0.0-beta1/langchain4j.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version${langchain4j.version}/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-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 工程里可能会遇到EmbeddingModel、ChatLanguageModel的包名或方法签名变化。建议优先选择当前 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(); ResponseEmbedding response embeddingModel.embed(Java 内存模型); Embedding embedding response.content(); float[] vector embedding.vector();在 Milvus 中创建集合时维度必须和 Embedding 模型的输出维度保持一致。如果指定错了维度插入向量时会报维度校验错误。3.3 EmbeddingStore向量库的接入抽象EmbeddingStoreTextSegment是 LangChain4j 对向量数据库的抽象。它提供了add()、search()等方法底层会根据实现类连接到 Milvus、OpenSearch、Redis 等系统。对开发者来说接入 Milvus 的代码非常简洁EmbeddingStoreTextSegment embeddingStore MilvusEmbeddingStore.builder() .host(localhost) .port(19530) .collectionName(java_doc_collection) .dimension(1024) .build();这段代码会在首次写入时自动创建 Collection前提是dimension字段与 Embedding 模型输出一致。生产环境建议预先在 Milvus 中手动创建 Collection并配置索引参数。3.4 什么是 RAG、混合检索和重排RAG检索增强生成是当前知识库问答的主流方案。它先把文档切成片段向量化后存入向量库用户提问时先从向量库检索相关片段再把片段拼进 Prompt 交给大模型生成答案。单纯依赖向量检索并不完美。向量检索擅长语义相似但关键词精确匹配能力弱关键词检索负责精确匹配却缺乏语义理解。因此越来越多的项目采用“混合检索”同时跑向量检索和关键词检索再把结果合并排序。合并排序会用到 RRFReciprocal 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_KEYsk-xxxxxxxxxxxxxx如果你的 API Key 来自其他平台也可以对应修改代码里的 Base URL 和模型名。4.2 配置 DashScope 兼容接口的模型客户端为了让几个 Service 共用模型实例我把ChatLanguageModel和EmbeddingModel都注册成 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 EmbeddingStoreTextSegment 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 EmbeddingStoreTextSegment embeddingStore; public DocumentImportService(EmbeddingModel embeddingModel, EmbeddingStoreTextSegment embeddingStore) { this.embeddingModel embeddingModel; this.embeddingStore embeddingStore; } public void importDocument(Document document) { ListTextSegment 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 EmbeddingStoreTextSegment embeddingStore; public RagSearchService(EmbeddingModel embeddingModel, EmbeddingStoreTextSegment embeddingStore) { this.embeddingModel embeddingModel; this.embeddingStore embeddingStore; } public ListString hybridSearch(String query) { Embedding queryEmbedding embeddingModel.embed(query).content(); ListEmbeddingMatchTextSegment vectorResults embeddingStore.findRelevant(queryEmbedding, 10); ListString keywordResults keywordSearch(query); // RRF 合并 MapString, 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 ListString 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 ListString rerank(String query, ListString 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) { ListString relatedDocs hybridSearch(query); ListString 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 接口调用返回 401401 表示鉴权失败。检查环境变量是否真的生效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 阶段对包结构进行了梳理社区模块的命名也统一了。如果你在网上找到的示例与本地代码不一致先看三处ChatLanguageModel、EmbeddingModel的 import 路径。MilvusEmbeddingStore是核心模块还是 community 模块。embeddingModel.embed()返回的是ResponseEmbedding还是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 存储以及混合检索和重排的工程实现思路。如果你是从零开始建议按下面的顺序推进学习先把ChatLanguageModel跑通完成一次最简单的对话。再接入EmbeddingModel了解向量化后的数据长什么样。然后配置EmbeddingStore用 100 条文档验证“导入—检索”闭环。之后再做 RRF 混合检索替换成 Milvus 原生混合检索 API。最后引入重排模型做离线评测和调参。在实际项目中RAG 效果瓶颈往往不在代码而在文档切分、检索召回和重排质量上。框架只是帮你把链路串起来真正决定体验的是数据准备与持续优化。先从最基础的消息模型跑通再逐步加入向量库与重排链路这样即使中间出了偏差也不需要推翻重来。