先说一个很常见的场景:后端是 Java 技术栈,业务突然需要把内部文档接入大模型,做一个知识库问答或者智能客服。翻了一圈资料,大多数教程都基于 Python 的 LangChain,Java 团队要么自己写 HTTP 封装,要么把检索逻辑放到 Python 微服务里,链路长、排错难、维护成本高。后来接触到 LangChain4j,才意识到 Java 生态里也能把大模型调用、Embedding 向量化、Milvus 存储、RAG 检索完整串起来。本文就围绕 LangChain4j 零基础入门到项目实战展开,完整演示从环境搭建、接入 Qwen 对话模型,到 Qwen Embedding 写入 Milvus,再到混合检索与重排的整个闭环。
如果你正准备在 Java 项目里落地大模型应用,或者已经尝试过 LangChain4j 但卡在 Milvus 集成和检索质量调优上,这篇文章应该能帮到你。文章不会只贴代码,也会解释每一步背后的原理,包括为什么用向量数据库、为什么需要混合检索、重排到底在解决什么问题。代码示例以常见环境为准,版本迭代较快的组件我会给出明确的替换思路,你按实际依赖版本微调即可。
1. LangChain4j 是什么,为什么 Java 开发者需要它
1.1 技术诞生背景
LangChain4j 可以理解为 Java 生态里的 LLM 编排框架,名字里的 4j 就是 for Java 的意思。它诞生的背景很直接:Python 生态里已经有 LangChain、LlamaIndex 这些框架,可以快速把大模型、提示词、记忆、外部工具、向量数据库组装成 AI 应用。但很多企业的核心业务系统是 Java 写的,特别是金融、电商、政企类项目,想在已有 Spring Boot 服务里直接引入 AI 能力,不可能把所有逻辑都迁到 Python。
早期 Java 开发者的做法是直接调用大模型 HTTP API,自己维护对话上下文,自己处理文档切分和向量化,再自己写向量数据库的客户端代码。功能能做出来,但每个项目都要重复造轮子,而且不同大模型厂商的 API 格式还不一致,切换模型成本很高。LangChain4j 就是把这一层通用能力抽象出来,让 Java 开发者可以用统一 API 对接不同大模型、不同向量库、不同 RAG 组件。
1.2 它解决什么问题
LangChain4j 解决的问题可以拆成四类:第一是模型接入统一化,通过ChatLanguageModel接口屏蔽各家大模型 API 差异,OpenAI、Qwen、通义千问、智谱等模型都可以通过对应模块接入;第二是对话管理标准化,Message、ChatMemory、AiServices这些组件让你不用自己拼历史消息数组;第三是 RAG 链路组件化,EmbeddingModel、EmbeddingStore、ContentRetriever分别负责向量化、向量存储和召回,替换实现类就能切换底层组件;第四是与 Java 生态融合,它不强制你换框架,Spring Boot、Quarkus、普通 Maven 项目都能用。
对于团队来说,使用 LangChain4j 之后,AI 能力和业务代码可以放进同一个工程,共用日志、监控、配置中心和权限体系。这样理解起来更简单:LangChain4j 是 Java 侧的一层 AI 编排中间件,你只需要负责业务数据和提示词策略,底层通信交给它。
1.3 典型应用场景
LangChain4j 的典型场景主要有这么几类:企业内部知识库问答,把产品文档、技术规范、操作手册切成片段后存入向量库,用户提问时先召回相关内容,再交给大模型生成回答;智能客服工单处理,结合工具调用实现查订单、查物流、转人工;代码辅助和日志分析,把异常日志喂给模型,让它输出定位建议;内容分析和信息抽取,从非结构化文本中抽取结构化字段。这些场景本质上有共同点:先用检索缩小范围,再用大模型做生成或决策,LangChain4j 正好覆盖这条完整链路。
2. 核心概念扫盲:模型、消息、记忆与 RAG
2.1 ChatLanguageModel 与 Message
在 LangChain4j 里,ChatLanguageModel是最核心的接口,它表示一个能处理多轮对话的大语言模型。你不需要关心底层是调用 OpenAI 还是 Qwen,只要拿到ChatLanguageModel实例,调用它的generate系列方法就能得到模型回复。对话内容由Message表达,常见的有SystemMessage系统消息、UserMessage用户消息、AiMessage助手消息,还有ToolExecutionResultMessage工具执行结果消息。
为什么要用 Message 而不是简单传一个字符串?因为大模型的上下文是一系列消息组成的数组,系统消息可以设定角色,历史消息帮助模型理解对话脉络,工具消息让模型知道工具调用结果。LangChain4j 把这些概念封装成类型,开发时不会拼错 JSON,IDE 也能自动提示。比如你要让模型扮演客服,可以SystemMessage.from("你是 XX 产品的客服助手"),然后把用户问题和历史对话一起传进去。
2.2 EmbeddingModel 与 EmbeddingStore
Embedding 就是把文本变成一组浮点数向量,语义相近的文本在向量空间里距离更近。EmbeddingModel负责做向量化,EmbeddingStore负责存储向量并执行相似度检索。LangChain4j 中的EmbeddingStore<TextSegment>是一个泛型接口,TextSegment代表一段被切分后的文本,同时可以附带元数据,比如文档 ID、标题、章节、来源链接等。
典型流程是:先把文档拆成多个TextSegment,用EmbeddingModel给每段生成向量,再调用EmbeddingStore.add把向量和原始文本存进去。查询时把用户问题向量化,调用findRelevant方法返回最相似的 TopK 片段。当前支持多种向量库,Milvus 是其中比较常用的一种,适合海量向量和分布式部署场景。
2.3 AiServices 与 RAG
AiServices是 LangChain4j 的高级 API,它的思想是把 AI 能力绑定到普通 Java 接口上。你定义一个接口,比如Assistant,里面声明一个方法answer(String question),然后用AiServices.builder(Assistant.class)给它配置模型、记忆、工具和检索器,LangChain4j 会自动生成这个接口的实现类。调用方法时,它内部自动完成消息组装、工具调用、RAG 召回、结果生成。
RAG(Retrieval-Augmented Generation,检索增强生成)在 LangChain4j 中对应的核心组件是ContentRetriever。它接收用户的查询,返回相关的Content列表。框架内置了基于向量库的EmbeddingStoreContentRetriever,也支持自定义实现。如果你想做混合检索或者重排,自定义ContentRetriever是最灵活的方式。
2.4 与 Python LangChain 的差异
很多同学会问:我直接学 Python LangChain 不行吗?当然可以,但从 Java 工程化角度看,LangChain4j 有几个明显优势。第一是类型安全,接口方法、消息类型、返回结果都是 Java 强类型,编译期就能发现问题;第二是依赖治理,Maven/Gradle 管理依赖比 Python 虚拟环境在大型企业里更成熟;第三是部署一致性,AI 应用和业务服务可以打成同一个 Jar,沿用现有 CI/CD 流程;第四是回归测试,JUnit 可以方便地模拟模型返回,做接口级测试。
当然 LangChain4j 的生态组件数量相比 Python LangChain 还有差距,部分新模型和新的 RAG 技术可能需要自己封装。但核心链路已经很完整,覆盖大多数生产需求。技术选型时不要只看生态数量,还要看团队维护成本和系统集成复杂度。
3. 环境准备与项目初始化
3.1 运行环境清单
本文示例以一套常见 Java 开发环境为例,具体版本需要根据你本机情况调整。推荐环境如下:JDK 17 或更高版本,Maven 3.8 以上,IDE 使用 IntelliJ IDEA;需要一个可访问的通义千问 DashScope API Key,用于调用对话模型和 Embedding 模型;还需要一个可访问的 Milvus 服务,本地开发可以用 Docker 起单机版,也可以用公司测试环境。
这里要特别提醒:LangChain4j 版本更新非常快,不同小版本的 API 可能有细微差异。我在示例代码里不写死具体版本,你用 Maven 依赖时建议直接访问 Maven 中央仓库查询最新稳定版,或者使用你项目里已经引入的 LangChain4j 版本。本文重点关注设计思路和核心代码,API 变化后通过官方 Javadoc 很容易调整。
3.2 创建 Maven 项目结构
为了演示方便,我建议创建一个简单的 Maven 项目,不引入 Spring Boot,先用main方法跑通全流程。后续接入 Spring Boot 时,把核心代码抽成 Bean 即可。项目结构大致如下:
langchain4j-demo ├── pom.xml └── src/main/java └── com/example/demo ├── DemoApplication.java ├── QwenConfig.java ├── MilvusConfig.java ├── HybridContentRetriever.java └── Assistant.java如果你用的是 Spring Boot,可以把QwenConfig和MilvusConfig改成@Configuration配置类,把模型和向量库实例声明为@Bean,这样业务代码里直接注入使用。先把直接可运行的main方法写出来,更容易理解原理。
3.3 添加 Maven 依赖
在pom.xml中加入核心依赖。因为版本迭代快,我把版本号用占位符标注,请替换成你选择的版本:
<dependencies> <!-- LangChain4j 核心依赖 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>${langchain4j.version}</version> </dependency> <!-- 通过 OpenAI 兼容模式接入 Qwen 需要用到 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>${langchain4j.version}</version> </dependency> <!-- Milvus 向量库集成 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-milvus</artifactId> <version>${langchain4j.version}</version> </dependency> </dependencies>在properties中定义langchain4j.version,例如:
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <langchain4j.version>请填写你选择的最新稳定版本</langchain4j.version> </properties>注意,上面这个占位符不能直接用于 Maven 构建,实际使用时要替换成具体版本号。如果你不确定哪个版本稳定,建议先看官方 GitHub 仓库的 release 说明,或者看 Maven 中央仓库中dev.langchain4j的最新版本。
3.4 配置文件
虽然纯 Java 示例不需要配置文件,但我建议把 API Key、Milvus 地址统一放到环境变量或配置中心,避免硬编码。这里使用环境变量方式,更安全。后面代码中会读取DASHSCOPE_API_KEY、MILVUS_URI、MILVUS_TOKEN这几个环境变量。
export DASHSCOPE_API_KEY=你的DashScope密钥 export MILVUS_URI=http://localhost:19530 export MILVUS_TOKEN=root:MilvusAPI Key 是敏感信息,生产环境一定不要提交到 Git 仓库,也不要写在配置文件中随代码发布。Spring Boot 项目可以用配置中心管理,普通项目用环境变量或密钥管理服务。
4. 第一个案例:通过 LangChain4j 接入 Qwen 对话模型
4.1 选择接入协议
Qwen 系列模型可以通过 DashScope 的 OpenAI 兼容模式接入,LangChain4j 的langchain4j-open-ai模块可以直接复用。这种做法的好处是无需引入额外的国产模型专用 SDK,只要配置baseUrl、apiKey和modelName就能切换模型。如果你企业内部已经部署了兼容 OpenAI API 的模型网关,也可以用同样的方式接入。
需要说明的是,DashScope 的兼容模式接口地址是https://dashscope.aliyuncs.com/compatible-mode/v1。对话模型可以填写qwen-plus、qwen-turbo等,实际支持情况以 DashScope 官方文档为准。下面代码用qwen-plus做演示,你可以根据业务场景选择合适模型。
4.2 最小代码示例
创建一个DemoApplication.java,先体验最简单的对话生成:
// 文件路径:src/main/java/com/example/demo/DemoApplication.java package com.example.demo; import dev.langchain4j.model.openai.OpenAiChatModel; public class DemoApplication { public static void main(String[] args) { OpenAiChatModel chatModel = OpenAiChatModel.builder() .apiKey(System.getenv("DASHSCOPE_API_KEY")) .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1") .modelName("qwen-plus") .build(); String answer = chatModel.generate("请用一句话介绍 LangChain4j"); System.out.println(answer); } }这段代码里的OpenAiChatModel来自langchain4j-open-ai,它实现了ChatLanguageModel接口。generate方法接收一个用户消息字符串,返回模型生成的文本。因为配置了 DashScope 兼容地址,实际请求会发送到通义千问服务。
运行前确认环境变量DASHSCOPE_API_KEY已设置,然后执行:
mvn compile exec:java -Dexec.mainClass=com.example.demo.DemoApplication如果你在 IDE 里运行,直接右键执行main方法即可。
4.3 运行与验证
正常执行后,控制台会输出类似下面的内容:
LangChain4j 是一个面向 Java 开发者的大语言模型应用开发框架,用于构建基于大模型的智能应用。由于模型输出有随机性,每次结果不完全一样,这是正常现象。到这里你已经完成了 LangChain4j 接入 Qwen 的第一个闭环:Java 代码直接对话大模型。这个基础非常重要,后面的 RAG 和工具调用都是在这个模型实例之上扩展的。
4.4 引入记忆与流式输出
生产场景里,问答助手通常需要多轮对话记忆。LangChain4j 里最简单的做法是使用MessageWindowChatMemory,并在AiServices中配置。先看依赖已经包含,不需要额外引入。示例改造为带记忆的助手:
// 文件路径:src/main/java/com/example/demo/Assistant.java package com.example.demo; import dev.langchain4j.service.AiServices; import dev.langchain4j.memory.chat.MessageWindowChatMemory; public interface Assistant { String chat(String message); } // 在 main 中构建 Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build(); System.out.println(assistant.chat("我叫小明")); System.out.println(assistant.chat("我叫什么名字?"));AiServices会自动管理历史消息,第二句回答会记得用户叫小明。这种方式比手动拼接历史消息可靠得多,也是后续项目实战的首选方式。流式输出则使用OpenAiStreamingChatModel,回调接口里接收每个 token,适合打字机效果,这里先不展开。
5. Qwen Embedding 与 Milvus 向量存储
5.1 为什么需要向量化与向量数据库
问答模型本身不具备企业私有知识,它只知道训练时见过的数据。要让模型回答特定文档内容,常见做法是 RAG:先把私有文档切分成片段,离线生成向量并存入向量数据库;用户提问时,把问题也转成向量,在向量库中查找语义相似的文档片段,最后把这些片段作为上下文交给对话模型生成回答。
向量数据库负责存储和检索高维向量。Milvus 是开源向量数据库,支持海量向量、标量过滤、混合检索,Java 集成也相对成熟。在 LangChain4j 中,MilvusEmbeddingStore封装了向量集合管理、索引和查询逻辑,我们不用直接操作 Milvus Java SDK 的复杂客户端。
5.2 创建 EmbeddingModel
使用 Qwen 的向量模型生成 Embedding。DashScope 兼容模式同样支持 Embedding 接口,模型名可以填写text-embedding-v3。示例代码如下:
// 文件路径:src/main/java/com/example/demo/QwenConfig.java package com.example.demo; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.openai.OpenAiEmbeddingModel; public class QwenConfig { public static EmbeddingModel createEmbeddingModel() { return OpenAiEmbeddingModel.builder() .apiKey(System.getenv("DASHSCOPE_API_KEY")) .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1") .modelName("text-embedding-v3") .build(); } }EmbeddingModel的核心方法是embed(String text),返回一个Response<Embedding>,里面有向量数据。不同向量模型输出的维度不同,比如text-embedding-v3可以配置输出维度,使用时需要和 Milvus 集合的维度保持一致。
5.3 文档拆分为 TextSegment
向量化之前需要把文档拆成适合检索的片段。原因很简单:整篇文档直接转成一个向量,检索精度很低;但如果切得太碎,又会丢失上下文。常用的策略是按标题、段落、固定长度切分,片段之间保留少量重叠。下面是最简单的按固定长度切分:
// 文件路径:src/main/java/com/example/demo/TextSplitter.java package com.example.demo; import dev.langchain4j.data.segment.TextSegment; import java.util.ArrayList; import java.util.List; public class TextSplitter { public static List<TextSegment> split(String text, int chunkSize, int overlap) { List<TextSegment> segments = new ArrayList<>(); int start = 0; int index = 0; while (start < text.length()) { int end = Math.min(start + chunkSize, text.length()); segments.add(TextSegment.from(text.substring(start, end))); if (end == text.length()) { break; } start = end - overlap; index++; } return segments; } }这个工具类只是一个演示,实际生产中文档结构很复杂,建议使用 LangChain4j 内置的DocumentSplitter,或者按 Markdown 标题、PDF 段落先做结构化切分。切分质量直接影响 RAG 回答效果,值得多花时间调优。
5.4 接入 MilvusEmbeddingStore
接下来把向量和原始片段写入 Milvus。用MilvusEmbeddingStore的 builder 创建实例,配置 Milvus 地址、集合名和向量维度。示例假设text-embedding-v3输出维度为 1024,实际以你使用的模型配置为准。
// 文件路径:src/main/java/com/example/demo/MilvusConfig.java package com.example.demo; import dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore; public class MilvusConfig { public static MilvusEmbeddingStore createStore() { return MilvusEmbeddingStore.builder() .uri(System.getenv("MILVUS_URI")) .token(System.getenv("MILVUS_TOKEN")) .collectionName("java_rag_demo") .dimension(1024) .build(); } }如果你的 Milvus 版本不需要 token,可以省略.token()方法。MilvusEmbeddingStore在首次写入时会自动创建 collection,但你也可以在 Milvus 控制台提前建好集合、设置索引和距离算法,这样更可控。
入库示例:
// 文件路径:src/main/java/com/example/demo/IngestExample.java package com.example.demo; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore; public class IngestExample { public static void main(String[] args) { EmbeddingModel embeddingModel = QwenConfig.createEmbeddingModel(); MilvusEmbeddingStore store = MilvusConfig.createStore(); String doc = "LangChain4j 是一个 Java 大模型编排框架。" + "它支持对话模型、Embedding、向量检索、工具调用。" + "Milvus 是一个高性能向量数据库。"; for (TextSegment segment : TextSplitter.split(doc, 30, 5)) { Embedding embedding = embeddingModel.embed(segment.text()).content(); store.add(embedding, segment); System.out.println("已写入片段:" + segment.text()); } } }执行后,每个TextSegment会被向量化并写入 Milvus 的java_rag_demo集合。
5.5 向量相似度检索
检索时使用findRelevant方法,传入问题向量和返回条数:
// 检索示例 Embedding queryEmbedding = embeddingModel.embed("Java 的向量数据库怎么选").content(); List<EmbeddingMatch<TextSegment>> matches = store.findRelevant(queryEmbedding, 5); for (EmbeddingMatch<TextSegment> match : matches) { System.out.println("相似度:" + match.score()); System.out.println("内容:" + match.embedded().text()); }findRelevant返回的列表已经按相似度从高到低排序,score越高越相关。如果你发现检索结果不准,常见原因是文档切分不合理、向量模型维度低、Milvus 索引类型不合适,或者查询表述和文档表达方式差异过大。
6. 混合检索与重排:提升 RAG 精度的关键
6.1 什么是混合检索
向量检索擅长语义相似,比如用户问“怎么使用 LangChain4j 接入向量库”,文档里写的是“LangChain4j Milvus 集成示例”,语义上接近,向量检索可以找回。但向量检索也有弱点:对专有名词、缩写、代码符号、精确 ID 不敏感。比如用户搜“订单号 ORD-2026-001”,向量检索很可能找不到完全匹配片段,而关键词检索可以精准命中。
混合检索就是把向量检索和关键词检索的结果合并起来,再做去重和排序。关键词检索可以用传统数据库的LIKE、全文索引、Elasticsearch,也可以先通过 Milvus 的标量过滤能力实现。混合检索的目的是让召回结果更完整,再用重排模型精排,最后交给大模型生成。
下表对比了两种检索方式的特点:
| 检索方式 | 优势 | 劣势 | 常见实现 |
|---|---|---|---|
| 向量检索 | 语义理解强,容错性好 | 精确关键词可能不准 | Milvus、FAISS |
| 关键词检索 | 精确匹配,可解释性强 | 依赖词表,无法理解语义 | Elasticsearch、数据库全文索引 |
6.2 在 LangChain4j 中实现混合召回
LangChain4j 提供了ContentRetriever接口,我们自定义一个实现类,同时执行向量检索和关键词检索。关键词检索部分需要结合你的实际存储,下面使用一个简单的思路:基于 Milvus 的元数据过滤模拟精准匹配,更完整的生产实现可以用 Elasticsearch。
// 文件路径:src/main/java/com/example/demo/HybridContentRetriever.java package com.example.demo; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.rag.content.Content; import dev.langchain4j.rag.content.ContentRetriever; import dev.langchain4j.rag.content.RetrievalRequest; import dev.langchain4j.rag.content.TextContent; import dev.langchain4j.store.embedding.EmbeddingMatch; import dev.langchain4j.store.embedding.EmbeddingStore; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class HybridContentRetriever implements ContentRetriever { private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; public HybridContentRetriever(EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore) { this.embeddingModel = embeddingModel; this.embeddingStore = embeddingStore; } @Override public List<Content> retrieve(RetrievalRequest retrievalRequest) { String query = retrievalRequest.query().text(); // 1. 向量召回 Embedding queryEmbedding = embeddingModel.embed(query).content(); List<EmbeddingMatch<TextSegment>> vectorMatches = embeddingStore.findRelevant(queryEmbedding, 10); List<Content> vectorContents = vectorMatches.stream() .map(EmbeddingMatch::embedded) .map(TextSegment::text) .map(TextContent::from) .collect(Collectors.toList()); // 2. 关键词召回(这里简化处理,按含有查询关键词过滤) List<Content> keywordContents = keywordSearch(query); // 3. 合并去重 Map<String, Content> mergedMap = new LinkedHashMap<>(); for (Content content : vectorContents) { mergedMap.putIfAbsent(content.textSegment().text(), content); } for (Content content : keywordContents) { mergedMap.putIfAbsent(content.textSegment().text(), content); } return new ArrayList<>(mergedMap.values()); } private List<Content> keywordSearch(String query) { // 生产环境建议接入 Elasticsearch 或数据库全文索引 // 这里仅演示接口扩展点 return List.of(); } }这个类把向量召回和关键词召回合并,后续还可以继续接入重排。实际开发中,keywordSearch可以注入一个 Elasticsearch Client,也可以调用数据库查询,返回结果统一包装成TextContent。
6.3 重排原理与接入方式
混合召回得到的结果可能包含几十条,直接全部塞给大模型会浪费 token,而且相关性差的片段会干扰回答。重排的目的是用更精细的模型对“查询-文档”对重新打分,只保留最相关的 TopK。常见重排模型有 Cohere Rerank、BGE Rerank 等,企业内部也可以自己训练排序模型。
LangChain4j 中接入重排的思路是:在ContentRetriever.retrieve返回之前调用重排服务。我们可以封装一个简单的接口:
// 文件路径:src/main/java/com/example/demo/ReRanker.java package com.example.demo; import dev.langchain4j.rag.content.Content; import java.util.List; public interface ReRanker { List<Content> rerank(String query, List<Content> contents); }实现类可以调用内部 HTTP 重排服务。下面是一个基于 Java HttpClient 的示例思路:
// 文件路径:src/main/java/com/example/demo/HttpReRanker.java package com.example.demo; import dev.langchain4j.rag.content.Content; import dev.langchain4j.rag.content.TextContent; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.ArrayList; import java.util.List; public class HttpReRanker implements ReRanker { private final String endpoint; public HttpReRanker(String endpoint) { this.endpoint = endpoint; } @Override public List<Content> rerank(String query, List<Content> contents) { // 实际需要把 query 和 documents 序列化为 JSON,发送到重排服务 // 下面仅展示结构,需要根据你的重排服务协议调整 List<Content> result = new ArrayList<>(); result.addAll(contents); // TODO: 调用 endpoint,得到排序后的结果并返回 return result; } }在HybridContentRetriever中增加ReRanker成员,合并去重后调用rerank,再截取前 5 条返回。重排会带来额外的网络开销和成本,属于质量与性能的权衡。如果业务场景对延时敏感,可以只对向量和关键词合并后的 Top 30 做重排,而不是全量。
6.4 组装完整的 RAG 问答助手
现在把对话模型、混合检索器、Milvus 存储组装成一个具备 RAG 能力的助手。首先定义一个业务接口:
// 文件路径:src/main/java/com/example/demo/Assistant.java package com.example.demo; import dev.langchain4j.service.SystemMessage; public interface Assistant { @SystemMessage("你是企业知识库助手,请基于提供的上下文回答问题,不要编造。") String answer(String question); }然后在 main 方法中完成组装:
// 文件路径:src/main/java/com/example/demo/RagApplication.java package com.example.demo; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.rag.content.retriever.ContentRetriever; import dev.langchain4j.service.AiServices; import dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore; public class RagApplication { public static void main(String[] args) { ChatLanguageModel chatModel = OpenAiChatModel.builder() .apiKey(System.getenv("DASHSCOPE_API_KEY")) .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1") .modelName("qwen-plus") .build(); MilvusEmbeddingStore store = MilvusConfig.createStore(); ContentRetriever retriever = new HybridContentRetriever( QwenConfig.createEmbeddingModel(), store ); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .contentRetriever(retriever) .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) .build(); String answer = assistant.answer("LangChain4j 如何接入 Milvus?"); System.out.println(answer); } }执行后,助手会先从 Milvus 中召回相关文档片段,再由 Qwen 模型基于这些片段生成回答。这个结构已经非常接近生产项目,后续只需要把数据源、切分策略、检索器替换成你的业务实现。
7. 常见问题与排查思路
7.1 Milvus 连接异常
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 连接超时 | Milvus 服务未启动或网络不通 | 用telnet localhost 19530检查端口 |
| 认证失败 | token 错误 | 确认 Milvus 用户密码,默认 root:Milvus |
| 集合不存在 | collectionName 配置错误 | 检查集合名,让程序首次写入自动创建 |
| 端口冲突 | 本地多个 Milvus 实例 | 检查 docker ps,确保端口唯一 |
排查流程建议从底向上:先确认 Milvus 服务本身可用,再用 LangChain4j 日志观察是否发出请求。Milvus 相关版本升级后,MilvusEmbeddingStore.Builder的方法名可能有变化,注意看依赖源码。
7.2 Embedding 维度不匹配
写入向量时出现类似dimension mismatch的报错,通常是因为EmbeddingModel输出的维度和 Milvus collection 的dimension配置不一致。text-embedding-v3可配置输出维度,而不同配置下模型返回维度不同;如果你换用了其他向量模型,维度需要同步调整。
解决方法:先打印向量长度,比如embedding.content().dimension(),再在MilvusEmbeddingStore.builder().dimension(...)中填写相同数值。如果集合已经创建,修改维度需要删除原有集合重建,所以生产环境要提前固化向量模型。
7.3 Qwen API 调用报错
调用模型时返回 401 或InvalidApiKey,一般是DASHSCOPE_API_KEY设置不正确。排查步骤:确认环境变量已加载,确认 Key 没有多余空格,确认账号已开通 DashScope 服务。如果返回 404,要检查baseUrl是否为https://dashscope.aliyuncs.com/compatible-mode/v1,以及模型名是否正确。如果返回限流错误,需要增加重试和退避策略,或升级模型服务配额。
7.4 检索结果与预期不符
混合检索上线后回答质量仍然不好,先不要急着换大模型,而是检查召回链路。打印ContentRetriever返回的所有片段,看问题是被错误召回还是相关片段没有召回。如果相关片段没有召回,可能是切分粒度太大,也可能是向量模型表达能力不够。如果召回相关但回答不好,可以优化 Prompt 或增加系统提示词约束。重排之后,还要对比重排前后的检索命中率,用评估集量化判断。
8. 最佳实践与工程建议
8.1 文档切分与元数据
文档切分是 RAG 项目里最容易被低估的环节。不要对所有文档使用同一套切分参数,而是按文档类型设计:Markdown 按标题层级切分,PDF 按章节切分,纯文本按段落和固定长度切分。每个TextSegment都建议带上元数据,包括文档标题、URL、更新时间、所属部门、权限级别。这样检索时可以通过元数据过滤缩小范围,也能在回答中标注来源,提升可信度。
切分时保留片段重叠可以缓解标题和正文被切断的问题。重叠长度一般控制在 50 到 100 个字符左右,具体需要实验。比如技术文档中,一个代码块如果被切到两个片段,单独检索可能都不完整,重叠能降低这种风险。
8.2 权限控制与数据安全
企业知识库通常涉及敏感数据,做 RAG 时必须有权限控制意识。最简单的方式是为不同知识库创建不同 Milvus 分区,或者给TextSegment元数据添加department、level字段,在ContentRetriever中根据当前用户权限过滤元数据。更复杂的场景可以引入独立的权限服务,在召回之前把用户权限转换成