Java开发者实战LangChain4j:构建RAG与混合检索系统

Java开发者实战LangChain4j:构建RAG与混合检索系统 如果你是一名 Java 开发者最近两年一定感受到了一种奇怪的不安AI 应用的浪潮几乎铺天盖地但翻遍热门教程十篇有八篇是 Python。社区里讨论最多的是 LangChain、LlamaIndex、Dify可这类框架从生态到示例都以 Python 为中心Java 开发者想搭一个 RAG 问答系统往往要把 Python 代码“翻译”成 Java再自己封装 HTTP 接口最后还要处理线程池、JSON 解析和一堆兼容性细节。这个过程的体验一点都不比业务开发轻松。这种割裂感不是你的错觉。LLM 应用开发框架的第一波红利确实集中在 Python 生态但企业级后端最庞大的存量代码在 Java模型调用、向量检索、Prompt 编排这些事情不可能永远只服务 Python 开发者。LangChain4j 就是为这个缺口而生的。LangChain4j 的核心目标是把 LangChain 那套“LLM 应用可组装积木”的理念带到 Java 和 Android 世界。它不是一个简单的 HTTP SDK 封装而是一套包含模型抽象、提示词管理、聊天记忆、结构化输出、RAG、Tools/Function Calling、向量存储接入等能力的完整框架。更关键的是它对 Spring Boot 有天然友好的集成路径这一点对 Java 后端工程师来说价值甚至超过了“支持多少模型”这件事本身。这篇文章会从一个零基础 Java 开发者的视角出发完整跑通一条 RAG 实战链路先用 LangChain4j 调用通义千问Qwen大模型再把文档切分后用 Qwen Embedding 向量化存入 Milvus 向量数据库最后实现“向量检索 关键词检索 重排”的混合检索方案。整个过程会给出可复制的 Java 代码、完整配置和排错思路而不是只停留在概念讲解。1. 这篇文章真正要解决的问题很多 Java 开发者第一次接触 LangChain4j 时会遇到来自三个层面的困惑我把它们提前摆出来后面逐一解决。1.1 第一层不知道该把它当成什么工具LangChain4j 不是一个“自动帮你做 AI”的黑盒工具也不是一个只有一两个 Demo 的玩具项目。它是帮你把 LLM 应用中的标准化环节抽象出来的开发框架。通俗地讲如果你要在 Java 项目里对接大模型、做 Prompt 模板管理、让模型输出固定 JSON、把聊天记录保存在数据库、基于向量库做相似度检索LangChain4j 能帮你把大部分样板代码省掉。但这不等于你不需要理解底层的模型调用和向量检索原理。框架解决的是“重复劳动”不是“设计能力”。1.2 第二层不知道一套完整的 RAG 链路由哪些零件构成最简单的 RAG 流程可以拆成四步文档加载与切分把 PDF、Markdown、Word 等源文档读取出来按长度或语义切分成 Chunk。向量化存储每个 Chunk 通过 Embedding 模型变成向量写入向量库。检索用户提问时把问题也转成向量在向量库中做相似度检索。生成把检索到的文本片段拼进 Prompt交给大模型生成最终答案。很多教程只讲到第 3 步就结束了但生产环境里真正决定效果的是第 4 步的“让模型学会引经据典”以及“检索结果不理想怎么办”的补救机制。这篇会覆盖到。1.3 第三层不知道混合检索和重排到底解决了什么单独用向量检索会遇到一个常见问题用户的提问用词和答案原文用词差别太大时向量相似度可能不高但如果只做关键词匹配又会漏掉语义相近但字面不同的内容。混合检索把两者结合起来再用重排模型对候选结果重新打分是当前 RAG 召回质量的主流优化方向。LangChain4j 配合 Milvus 的混合索引能力可以在 Java 侧把这套流程组织起来。所以本文真正要解决的问题是让 Java 开发者不再靠“翻译 Python 代码”的方式做 AI 应用而是直接用 Java 技术栈独立搭出一套可运行的 RAG 系统。2. LangChain4j 核心概念与架构解析在写代码之前先把 LangChain4j 的概念地图建立起来。这样后面看到每个类名你都不会觉得陌生。2.1 从 Python LangChain 到 Java LangChain4jLangChain 在 Python 生态中的成功是因为它把 LLM 应用中的高频组件抽象成了统一接口。LangChain4j 沿用了类似思路但重新针对 JVM 环境做了实现。它和 Python 版并不是逐行移植的关系而是“同一设计哲学、另一套代码实现”。用一句通俗的话概括LangChain4j 就是 Java 生态的 LLM 应用积木箱。对比维度Python LangChainLangChain4j语言生态PythonJava / Kotlin / AndroidSpring 集成通常自己封装官方提供 Spring Boot Starter模型抽象LangChain 内部接口ChatLanguageModel / EmbeddingModel 等接口向量存储多种 VectorStore 封装EmbeddingStore 接口 多种实现典型使用者Python 算法、AI 工程师Java 后端、企业应用开发者2.2 核心接口ChatLanguageModel这是 LangChain4j 中最高频的接口。所有大模型对话统一走它无论底层接的是 OpenAI、通义千问、文心一言还是本地部署的模型。在 Spring Boot 项目中你通常只需要在配置里写好 API Key、模型名称、Base URL然后注入一个ChatLanguageModel对象即可调用。代码示意见后面的实战部分。2.3 核心接口EmbeddingModelEmbeddingModel 负责把文本变成向量。向量化是把文本转换为数学向量的过程AI 并不是真的“理解”文字而是把文字映射到一个高维空间让语义相近的文本在高维空间中距离更近。在 Java 侧LangChain4j 调用 Qwen Embedding 模型后返回的Embedding对象包含一个float[]数组这个数组会被写入向量数据库。2.4 核心接口EmbeddingStoreEmbeddingStore是向量数据库的抽象层。LangChain4j 提供了多个实现包括 Milvus、Chroma、PGVector、Redis、Elasticsearch 等。注意区分两个概念EmbeddingModel把文本变成向量。EmbeddingStore把向量存起来并支持相似度检索。2.5 AiServicesLangChain4j 的杀手锏如果你用过 Python LangChain 的 LangGraph会发现编排多个步骤很灵活。LangChain4j 中对应的利器是AiServices它可以用“接口 注解”的方式把 LLM 能力绑定到业务接口上。例如你定义一个Assistant接口写一个方法和一段 Prompt 说明LangChain4j 会自动生成实现类让你像调用普通 Java 方法一样调用 AI 能力。它内部还串联了 Retriever检索器、Memory记忆、Tools工具调用等能力。这个设计对 Java 后端非常友好因为业务方只需要定义接口不需要关心大模型调用的细节。2.6 一个容易误解的地方很多人以为 LangChain4j 只是一个“调大模型的 SDK”实际上它的能力边界大得多Prompt 模板管理、输出解析、对话记忆持久化、RAG 全链路、Function Calling、自动 Agent 编排都在它的体系内。但也不要指望它一步到位解决所有问题。工程上真正的难点在于数据切分策略、Embedding 模型的选择、向量检索的阈值设置、Prompt 的写作质量这些都需要开发者自己调优。3. 环境准备与前置条件开始动手前先把环境备好。下面的版本信息以当前主流可用版本为例具体版本号请以实际项目为准本文重点是通用思路。3.1 基础环境清单组件说明建议JDKJava 开发环境JDK 17 或更高版本后面示例用 Java 17Maven依赖管理与构建Maven 3.8Spring Boot应用框架Spring Boot 3.x配合官方 StarterDashScope API Key通义千问模型凭证在阿里云百炼平台创建Milvus向量数据库Milvus 2.4 或更高版本用于混合检索Docker本地快速启动 Milvus推荐 Docker Compose 方式3.2 本地启动 MilvusMilvus 是一个开源的分布式向量数据库支持向量检索、标量过滤和稀疏向量检索。本地开发时使用 Docker Compose 启动最简单。先创建一个docker-compose.ymlversion: 3.5 services: etcd: image: quay.io/coreos/etcd:v3.5.5 environment: - ETCD_AUTO_COMPACTION_MODErevision - ETCD_AUTO_COMPACTION_RETENTION1000 - ETCD_QUOTA_BACKEND_BYTES4294967296 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd command: etcd -advertise-client-urlshttp://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd minio: image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data healthcheck: test: [CMD, curl, -f, http://localhost:9000/minio/health/live] interval: 30s timeout: 20s retries: 3 standalone: image: milvusdb/milvus:v2.4.1 command: [milvus, run, standalone] environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus ports: - 19530:19530 - 9091:9091 depends_on: - etcd - minio启动命令docker compose up -d启动后验证 Milvus 端口docker compose ps如果看到standalone服务状态为Up说明 Milvus 已经就绪。3.3 创建 Spring Boot 项目你可以通过start.spring.io创建工程也可以直接在已有项目中加入依赖。本文示例采用 Maven 管理。4. LangChain4j 快速入门调用通义千问大模型我们先从最小闭环开始用 LangChain4j 调用 Qwen 聊天模型完成一次普通对话。这一步的目的是验证配置和环境不涉及 RAG。4.1 添加 Maven 依赖在pom.xml中加入以下依赖properties langchain4j.version1.0.0-beta3/langchain4j.version /properties dependencies !-- LangChain4j 核心 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency !-- DashScope / 通义千问 集成 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-dashscope/artifactId version${langchain4j.version}/version /dependency !-- Spring Boot Starter -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version${langchain4j.version}/version /dependency !-- Milvus 向量存储 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-milvus/artifactId version${langchain4j.version}/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version3.2.5/version /dependency /dependencies版本说明langchain4j的版本迭代较快请以 Maven 中央仓库中的最新稳定版本为准。本文示例使用1.0.0-beta3作为演示版本如果你看到更新的版本直接替换即可。4.2 配置 application.yml在src/main/resources/application.yml中配置server: port: 8080 langchain4j: dash-scope: chat-model: api-key: ${DASHSCOPE_API_KEY} model-name: qwen-plus embedding-model: api-key: ${DASHSCOPE_API_KEY} model-name: text-embedding-v3这里有两个关键配置api-key在阿里云百炼控制台创建的 API Key建议用环境变量注入而不是写死在配置文件里。model-nameqwen-plus是通义千问的对话模型text-embedding-v3是文本向量化模型。不同模型的计费、能力、向量维度都可能不同请注意区分。4.3 编写一个简单的聊天接口创建一个测试 Controllerpackage com.example.ragdemo.controller; import dev.langchain4j.model.chat.ChatLanguageModel; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; RestController public class ChatController { private final ChatLanguageModel chatLanguageModel; public ChatController(ChatLanguageModel chatLanguageModel) { this.chatLanguageModel chatLanguageModel; } GetMapping(/chat) public String chat(RequestParam(defaultValue 你好请介绍一下你自己) String message) { return chatLanguageModel.chat(message); } }这段代码的核心逻辑通过构造器注入ChatLanguageModel在接口方法中直接调用chat()方法。Spring Boot 的自动配置帮你完成了模型客户端创建。4.4 运行并测试启动 Spring Boot 应用后在浏览器或命令行中访问curl http://localhost:8080/chat?message用一句话解释什么是RAG预期输出是一段由通义千问生成的回答。如果这一步成功说明 LangChain4j 与 Qwen 的链路已经打通。5. 构建知识库文本切分、Embedding 与 Milvus 存储现在进入 RAG 的核心。这一章的目标是把一个文本文件处理成向量存入 Milvus。5.1 完整流程梳理在 Java 项目中一次完整的知识库构建流程如下原始文档 - 读取 - 切分为 Chunk - 转换为 Embedding - 写入 Milvus Collection实际操作中文档加载、切分、向量化、写入存储每一步都可以用 LangChain4j 的标准组件完成。5.2 文档切分以段落为最小单位切分策略直接影响检索效果。切得太粗检索到的片段可能包含太多无关信息切得太细又会丢失上下文。LangChain4j 中可使用DocumentSplitter实现切分package com.example.ragdemo.service; import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.DocumentSplitter; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; import java.util.List; public class DocumentProcessService { public ListTextSegment split(String content) { Document document Document.from(content); DocumentSplitter splitter DocumentSplitters.recursive(500, 100); return splitter.split(document); } }参数含义500每个 Chunk 大约 500 个字符。100相邻 Chunk 之间重叠 100 个字符用来避免语义被切断。对于中文知识库建议根据实际段落长度调整没有绝对最优的参数。5.3 写入 Milvus 的完整工具类下面的示例把“切分 向量化 写入 Milvus”封装成一个服务便于在项目里复用。package com.example.ragdemo.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 dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; Service public class KnowledgeBaseService { private final EmbeddingModel embeddingModel; Value(${milvus.host:localhost}) private String milvusHost; Value(${milvus.port:19530}) private int milvusPort; public KnowledgeBaseService(EmbeddingModel embeddingModel) { this.embeddingModel embeddingModel; } /** * 将文本写入 Milvus * * param content 原始文本内容 * param source 来源标识便于追溯 * param collectionName 集合名称 */ public void addDocument(String content, String source, String collectionName) { // 1. 切分文档 ListTextSegment segments DocumentSplitters.recursive(500, 80) .split(Document.from(content)); // 2. 逐个向量化并写入 Milvus EmbeddingStoreTextSegment embeddingStore MilvusEmbeddingStore.builder() .host(milvusHost) .port(milvusPort) .collectionName(collectionName) .dimension(1024) .build(); for (TextSegment segment : segments) { segment.metadata().put(source, source); Embedding embedding embeddingModel.embed(segment.text()).content(); embeddingStore.add(embedding, segment); } } }这段代码中有几个容易出错的地方我拆开说明。维度问题dimension(1024)必须与 Embedding 模型输出的向量维度一致。text-embedding-v3支持多种维度默认输出 1024 维。如果模型维度与集合维度不一致Milvus 写入时可能报错。Metadadata 用途segment.metadata().put(source, source)是为每个 Chunk 附加业务元数据。检索时可以通过元数据进行过滤比如“只检索某个部门的数据”。批量写入示例中是单条循环写入。生产环境建议对几百个片段做批量插入减少网络往返。5.4 Embedding 模型的另一种编程式创建方式有些场景不想依赖 Spring Boot 自动配置可以直接用代码创建 DashScope Embedding 模型import dev.langchain4j.model.dashscope.QwenEmbeddingModel; String apiKey System.getenv(DASHSCOPE_API_KEY); EmbeddingModel embeddingModel QwenEmbeddingModel.builder() .apiKey(apiKey) .modelName(text-embedding-v3) .build();这种方式适合在非 Spring 环境中使用也便于在工具类中动态切换模型。6. 基于 Qwen Embedding 与 Milvus 的向量检索实战知识库构建完成之后开始做检索。这里从一个最简单的“用户提问 - 向量检索 - 返回相关片段”切入然后升级到“结合大模型生成答案”。6.1 最小检索示例package com.example.ragdemo.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 dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore; import org.springframework.stereotype.Service; import java.util.List; Service public class RetrieveService { private final EmbeddingModel embeddingModel; public RetrieveService(EmbeddingModel embeddingModel) { this.embeddingModel embeddingModel; } public ListString search(String query, String collectionName, int maxResults) { EmbeddingStoreTextSegment embeddingStore MilvusEmbeddingStore.builder() .host(localhost) .port(19530) .collectionName(collectionName) .dimension(1024) .build(); // 查询问题向量化 Embedding queryEmbedding embeddingModel.embed(query).content(); // 相似度检索 ListEmbeddingMatchTextSegment matches embeddingStore.findRelevant( queryEmbedding, maxResults, 0.5); return matches.stream() .map(match - match.embedded().text()) .toList(); } }关键点findRelevant(queryEmbedding, maxResults, 0.5)的第三个参数是相似度阈值。低于阈值的候选会被过滤掉。0.5是示例值具体阈值取决于 Embedding 模型和业务场景需要在一组真实数据上验证。返回结果按照相似度降序排列EmbeddingMatch里除了embedded().text()还能拿到score()和embedded().metadata()。6.2 完整 RAG 对话检索引擎 大模型生成有了检索能力就可以把它和大模型串起来。package com.example.ragdemo.service; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; import dev.langchain4j.rag.content.retriever.ContentRetriever; import dev.langchain4j.service.AiServices; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore; import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Service; Service public class RagChatService { private final ChatLanguageModel chatLanguageModel; private final EmbeddingModel embeddingModel; private Assistant assistant; public interface Assistant { String chat(String userMessage); } public RagChatService(ChatLanguageModel chatLanguageModel, EmbeddingModel embeddingModel) { this.chatLanguageModel chatLanguageModel; this.embeddingModel embeddingModel; } PostConstruct public void init() { EmbeddingStoreTextSegment embeddingStore MilvusEmbeddingStore.builder() .host(localhost) .port(19530) .collectionName(knowledge_collection) .dimension(1024) .build(); ContentRetriever contentRetriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.5) .build(); this.assistant AiServices.builder(Assistant.class) .chatLanguageModel(chatLanguageModel) .contentRetriever(contentRetriever) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build(); } public String chat(String userMessage) { return assistant.chat(userMessage); } }这个示例的价值在于它展示了 LangChain4j 最舒服的用法——定义接口交给框架生成实现。Assistant接口没有任何实现类AiServices会动态生成一个代理实现把检索、记忆、提示词拼接全部串联起来。用户只需要调用ragChatService.chat(根据知识库内容回答什么是混合检索)LangChain4j 会自动去 Milvus 中检索相关片段把片段放到系统上下文中并结合聊天记忆生成回答。6.3 关于 Prompt 模板的说明在 RAG 链路中真正影响回答质量的往往是内部使用的 Prompt 模板。LangChain4j 自动生成实现时会使用默认模板。如果你希望模型严格“只根据知识库回答不知道就说不清楚”建议在Assistant接口方法上使用SystemMessage注解public interface Assistant { SystemMessage( 你是一个企业知识库问答助手。 请只根据给定的知识库内容回答用户问题。 如果知识库中没有相关内容请直接回答知识库中暂无相关信息。 回答时注明内容来源。 ) String chat(String userMessage); }这种方式比在调用层拼接字符串更清晰也是 LangChain4j 推荐的做法。7. 混合检索与重排从“能搜出来”到“搜得准”很多人以为向量检索是 RAG 的银弹实际上在真实数据上测试几次就会发现向量检索擅长语义相近但不擅长精确关键词匹配BM25 关键词检索擅长字面匹配却对同义词和语义改写无能为力。混合检索的目标是把两者结合起来之后再让重排模型对候选集精排。7.1 为什么单独用向量检索不够场景一用户的提问是“QPS 怎么计算”知识库原文是“每秒查询率Queries Per Second计算方式”。向量检索在语义上能匹配但分数可能不高。场景二产品编码是“KN-2026-001”用户提问时输错成“KN-2026-001”如果只是语义检索向量会忽略这种细节而关键词检索能精确命中。混合检索的价值在于向量召回一批“语义相近”的结果关键词召回一批“字面相关”的结果合并后做去重和重排。最终进入 Prompt 的内容质量会明显提高。7.2 Milvus 2.4 的混合检索能力Milvus 从 2.4 版本开始支持混合检索即同一 Collection 中可以同时拥有稠密向量索引和稀疏向量索引并使用 BM25 算法做关键词检索。这对 LangChain4j 开发者意味着你可以用一套 Milvus 基础设施同时服务向量检索和关键词检索不需要再额外引入 Elasticsearch。Java 侧实现时可以用两条独立的检索路径分别召回候选集再在应用层做合并package com.example.ragdemo.service; import dev.langchain4j.data.document.Metadata; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.store.embedding.EmbeddingMatch; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore; import org.springframework.stereotype.Service; import java.util.*; import java.util.stream.Collectors; Service public class HybridRetrieveService { private final EmbeddingModel embeddingModel; public HybridRetrieveService(EmbeddingModel embeddingModel) { this.embeddingModel embeddingModel; } public ListTextSegment hybridSearch(String query, int topK) { // 向量检索结果 ListEmbeddingMatchTextSegment vectorResults vectorSearch(query, topK); // 关键词检索结果这里用系统内嵌的文本匹配模拟 BM25 召回 ListTextSegment keywordResults keywordSearch(query, topK); // 合并去重 MapString, TextSegment merged new LinkedHashMap(); for (EmbeddingMatchTextSegment match : vectorResults) { TextSegment segment match.embedded(); merged.putIfAbsent(segment.text(), segment); } for (TextSegment segment : keywordResults) { merged.putIfAbsent(segment.text(), segment); } return new ArrayList(merged.values()); } private ListEmbeddingMatchTextSegment vectorSearch(String query, int topK) { EmbeddingStoreTextSegment store MilvusEmbeddingStore.builder() .host(localhost) .port(19530) .collectionName(knowledge_collection) .dimension(1024) .build(); Embedding queryEmbedding embeddingModel.embed(query).content(); return store.findRelevant(queryEmbedding, topK, 0.3); } private ListTextSegment keywordSearch(String query, int topK) { // 这里应该替换为真正的倒排索引或 BM25 实现 // 生产实现使用 Elasticsearch 或 Milvus 2.4 的稀疏向量能力 // 或是 Milvus 内建的 BM25 索引 return Collections.emptyList(); } }上面的keywordSearch方法在真实项目中一般不会手写。生产环境更常见的三种实现方式使用 Milvus 2.4 的 Sparse Float Vector 索引让 Milvus 原生支持 BM25。使用 Elasticsearch 做关键词检索LangChain4j 负责调度两条通道。使用 Lucene 自带的功能在应用内维护倒排索引适合数据量较小的场景。选择哪种方式取决于团队已有的基础设施。如果团队已经有 Elasticsearch不一定要强行改造 Milvus如果希望保持向量数据库单点Milvus 2.4 的混合索引更合理。7.3 重排让模型对候选结果打一次分混合检索合并之后候选结果数量可能较多而且混合了两路不同评分体系的分数无法直接比较。重排Rerank的思想是用一个专门的交叉编码器模型把“用户问题 候选文档”拼接在一起计算出更精确的相关性分数再按新分数排序。在阿里云百炼生态中可以使用文本重排模型gte-rerank或 Qwen 系列的多模态重排模型。Java 侧调用方式如下package com.example.ragdemo.service; import dev.langchain4j.data.segment.TextSegment; import okhttp3.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Comparator; import java.util.List; Service public class RerankService { private final OkHttpClient httpClient new OkHttpClient(); Value(${DASHSCOPE_API_KEY}) private String apiKey; public ListTextSegment rerank(String query, ListTextSegment candidates) { // 调用重排模型接口对候选文档重新打分排序 // 这里以 DashScope 重排模型为例实际接口以官方文档为准 // 伪代码示意 // 将 query 与每个候选文本构造为输入 // 调用重排服务获得每个候选的 score // 按 score 降序排列 return candidates.stream() .sorted(Comparator.comparingDouble( (TextSegment s) - computeScore(query, s.text())).reversed()) .toList(); } private double computeScore(String query, String candidate) { // 实际项目中替换为真实重排模型调用 // 这里只是返回一个模拟分数 return candidate.contains(query) ? 1.0 : 0.5; } }需要明确一点重排模型的调用开销比向量检索大因为要对每个候选文档单独推理一次。所以重排应该只作用于前 N 条候选通常 N 在 20 到 100 之间重排后取 Top 3 到 Top 5 进入 Prompt。实际生产接入时建议先用阿里云百炼的text-reranker模型因为它在中文场景的开箱效果不错而且通过 DashScope SDK 调用非常方便。如果数据量特别大也可以考虑自部署开源 Rerank 模型。7.4 混合检索 重排的完整调用链在一个完整项目中最终可以这样编排用户提问 - Qwen Embedding 向量化 - Milvus 向量检索召回 Top 50 - Elasticsearch 或 Milvus BM25 召回 Top 50 - 合并去重得到 Top 80 - Rerank 模型精排取 Top 5 - 构造 Prompt - Qwen 大模型生成最终答案这套流程中LangChain4j 的作用是统一管理模型调用、接口定义和代码组织而 Milvus、Elasticsearch、Rerank 模型都是可替换的组件。8. 常见问题与排查方法下面是实战中容易踩的坑按排查顺序整理。问题现象可能原因排查方式解决方案启动报错找不到 DashScope API Key环境变量未设置或配置项的 key 名不一致检查application.yml和 IDEA 环境变量配置在环境变量中设置DASHSCOPE_API_KEY或写入配置文件调用聊天接口返回 401 / 403API Key 无效或账户没有开通百炼服务用 curl 直接测试 DashScope API检查 Key 是否有效确认开通对应模型服务Milvus 连接超时本地 Milvus 未启动或端口映射错误docker compose ps检查容器状态启动 Milvus确认 19530 端口监听写入 Milvus 报维度错误dimension(1024)与实际模型输出维度不一致打印 Embedding vector 的长度按模型文档设置正确维度例如text-embedding-v3的1024检索结果为空相似度阈值设置过高或 collection 中数据为空查看 collection 中记录数调低minScore降低阈值或确认数据写入成功检索结果相关度差切分粒度不合适 / 混合检索未启用检查 Chunk 长度与语义完整性调整切分参数引入关键词检索和重排Spring Boot 3.x langchain4j依赖冲突版本兼容性问题查看 Maven 依赖树统一 langchain4j 版本到同一版本这里特别注意API Key 绝对不要提交到 Git 仓库。建议把 Key 放在环境变量或配置中心并在项目根目录的.gitignore中排除本地配置。9. 最佳实践与工程建议RAG 系统从 Demo 到生产中间还有很长一段路。这里补充几个重点建议都是我实际做项目时认为最有价值的部分。9.1 先控制数据质量再调模型参数很多团队第一步就把精力花在“调 Prompt、调 Rerank 阈值”上却忽略了一个事实如果知识库源文档本身是混乱的任何检索优化都收效甚微。建议按这样的顺序推进清洗源文档删掉页眉页脚、固定导航、重复段落。设计元数据每条 Chunk 都带上业务线、部门、文档标题、更新时间。建立 Chunk 标识方便回溯“这句话到底来自哪个文档的哪一段”。再考虑切分参数和重排模型。9.2 每个 RAG 接口都应该能观测把 RAG 当作一个普通后端服务去治理记录每条问题的检索 Top N、最终进入 Prompt 的片段、大模型使用的总 token 数、响应耗时。否则线上出问题时你很难判断是“检索没召回”还是“模型回答跑偏”。推荐日志格式示例[RAG] query混合检索如何实现 queryEmbeddingTime213ms retrieveTop5[文档A, 文档B, ...] promptTokens1200 answerTokens350 totalTime2340ms9.3 慎用全局阈值向量检索的相似度分数在不同 Embedding 模型之间没有绝对可比性。同一个分数在模型 A 下可能代表高度相关在模型 B 下可能只是勉强相关。正确做法是用一批带标注的测试问题统计正确命中结果的分数范围再决定阈值。而不是拍脑袋定一个0.5或0.7。9.4 混合检索要处理“两路分数不可比”的问题向量检索的分数和 BM25 的分数不做归一化直接合并排序是新手最常犯的错误。虽然 Milvus 2.4 的混合检索在服务端已经做了分数融合但你如果使用自建双通道方案一定要注意对两路分数做归一化。常见做法有Rank Sum把两路结果分别排序按名次求和。Score 归一化用 Min-Max 把两路分数映射到 0-1再加权求和。RRFReciprocal Rank Fusion按1 / (k rank)加权融合简单有效推荐使用。9.5 利用缓存减少重复调用高性能场景下用户高频问题的答案其实是重复的。可以在 RAG 链路前加一层语义缓存先对用户问题做 Embedding再到“问题缓存 Collection”中检索如果相似度足够高且答案生命周期未过期直接返回缓存答案。这样可以省掉大部分大模型调用费用也显著降低响应时间。9.6 数据安全与权限控制在企业内部落地 RAG 时权限控制比检索效果更优先。如果知识库中包含不同层级、不同部门的数据必须从两个方面做隔离元数据过滤每个 Chunk 写入时带上可见范围检索时根据用户身份强制拼接过滤条件。结果脱敏即使某条 Chunk 检索命中也要判断当前用户是否有权限看到。LangChain4j 的ContentRetriever支持自定义实现可以在检索后追加业务权限过滤逻辑这是企业落地的必修课。10. 总结与后续学习方向这篇文章从 Java 开发者的视角把 LangChain4j 的核心概念、环境准备、Qwen 模型接入、Milvus 向量存储、混合检索和重排展开讲了一遍。现在你已经可以独立搭建一个最小可用的 Java RAG 系统也知道生产环境中需要在数据质量、可观测性、阈值调优、权限控制上继续打磨。如果想继续深入下面几个方向值得探索把切分策略从“固定长度”升级为“语义切分”利用小模型判断段落边界。研究 Function Calling让 LangChain4j 调用外部 API、数据库、内部系统把 RAG 扩展成真正的 Agent。深入 Milvus 的索引类型和参数调优理解不同索引在召回率和延迟上的取舍。尝试引入流式输出让大模型回答逐字返回提升用户体验。对于刚上手的同学我建议先把本地环境跑通往 Milvus 里写入一份自己的测试文档然后多尝试几种提问方式观察检索结果的变化。只有在真实数据上调试过你才能真正理解阈值、切分和重排这几个参数的分量。如果你正在规划一个 Java 技术栈的 AI 应用LangChain4j 是一个值得投入时间的技术选型。它现在可能还没有 Python 生态那么庞大但企业级 Java 后端的存量需求决定了这个框架的成长空间会非常大。希望这篇文章能帮你少走一些弯路。