1. 为什么 Java 工程师现在该认真看看 LangChain4j
这两年 AI 应用的开发热度一直居高不下,但如果你是一个常年写 Spring Boot、MyBatis、微服务那一套的 Java 工程师,可能会有一种被落下的焦虑感——满屏的 LangChain、LlamaIndex 都是 Python 的,教程一搜一大把,可落到自己熟悉的 Java 技术栈里,能直接抄作业的完整方案并不多。LangChain4j 就是在这个缝隙里长出来的东西:它是 LangChain 生态在 Java 侧的对应实现,把大模型调用、提示词模板、向量检索、对话记忆、工具调用这些能力,用 Java 工程师熟悉的方式封装起来,让你不用切语言、不用重搭技术栈,就能在现有的 Spring Boot 项目里把 AI 能力接进去。
我最初接触它的时候,动机很朴素:手上有个内部知识库问答的需求,团队全是 Java 背景,没人愿意为了一个功能去维护一套 Python 服务。试了一圈之后发现 LangChain4j 的抽象层次做得比较克制,既不像裸调 HTTP 接口那样什么都要自己写,也不像某些框架那样把逻辑藏得太深、出问题无从下手。它把 RAG、Embedding、AI Agent 这几个当下最核心的概念都做了落地封装,而且和 Spring Boot 的整合相当顺滑。
这篇内容适合三类人看:一是完全没接触过 AI 应用开发、但 Java 基础扎实的工程师,想从零搭一个能跑起来的 RAG 问答;二是已经在用 Python 方案、想评估 Java 侧可行性的技术负责人;三是面试里被问到 RAG、Agent 这些概念、想真正动手理解一遍的求职者。我会从整体设计思路讲到具体代码,把每个关键选择背后的理由说清楚,也会把我自己踩过的坑摊开来讲。全程围绕 LangChain4j 展开,但涉及的 RAG、Embedding、Agent 这些通用概念,理解透了换任何框架都能迁移。
2. 整体设计思路与方案选型拆解
2.1 为什么是 LangChain4j 而不是裸调接口
很多人第一反应是:调大模型不就是发个 HTTP 请求吗,我用 OkHttp 或者 Spring 的 RestTemplate 自己封装一下不就行了?这个想法在 demo 阶段没问题,但一旦进入真实项目就会迅速失控。你要处理的东西包括:不同模型厂商的接口格式差异、流式输出的解析、对话历史的截断策略、提示词模板的变量填充、向量库的读写、检索结果的重排序……每一项单独看都不难,堆在一起就是几百上千行的胶水代码,而且极难维护。
LangChain4j 的价值就在于它把这些共性能力抽象成了统一的接口。比如ChatLanguageModel这个接口,OpenAI、通义千问、Ollama 本地模型都可以实现它,你换模型的时候业务代码基本不用动。再比如EmbeddingStore,内存版、Redis 版、Milvus 版实现的是同一套接口,前期用内存版快速验证,后期换生产级向量库,改动量很小。这种"面向接口编程"的思路对 Java 工程师来说太亲切了,本质上就是我们天天在做的依赖倒置。
提示:选型时不要一上来就追求"最强模型 + 最强向量库"的组合。先用内存向量库加一个便宜的小模型把链路跑通,确认业务逻辑没问题,再逐步替换组件。我见过太多人卡在环境配置上,链路一次都没跑通就放弃了。
2.2 RAG 到底解决了什么问题
RAG 是 Retrieval-Augmented Generation 的缩写,中文一般叫检索增强生成。要理解它,先得理解大模型的两个硬伤:一是知识有截止日期,训练完之后新发生的事情它不知道;二是它不知道你私有的数据,比如你公司的内部文档、产品手册、客服话术。你直接问它,它要么答不上来,要么一本正经地胡说八道,也就是所谓的幻觉。
RAG 的思路很直白:既然模型不知道,那我在提问之前,先去我的知识库里把相关内容检索出来,连同问题一起塞给模型,让它"看着材料回答"。这就像开卷考试,模型不需要把答案背下来,只需要具备阅读理解能力,根据你给的材料组织语言。这个思路的好处是知识可以随时更新——你只要更新知识库,不用重新训练模型,成本极低。
一个完整的 RAG 链路包含几个环节:文档加载、文本切分、向量化、存储、检索、重排、生成。每个环节都有讲究,后面会逐个拆开讲。这里先建立一个整体认知:RAG 不是某个具体技术,而是一套工程流程,LangChain4j 帮你把流程里的每个环节都提供了现成的组件。
2.3 Embedding 在链路里的位置
Embedding 是 RAG 的技术基石,理解它是理解整个检索逻辑的前提。简单说,Embedding 就是把一段文本转换成一串浮点数组成的向量,比如[0.12, -0.34, 0.56, ...],通常有几百到几千维。这个向量的妙处在于:语义相近的文本,它们的向量在空间里的距离也相近。
打个比方,如果把"苹果手机"和"iPhone"这两个词映射到向量空间,它们的距离会很近;而"苹果手机"和"红烧肉"的距离会很远。检索的时候,我把用户的问题也转成向量,然后去向量库里找距离最近的几条文本,这就是语义检索。它比传统的关键词匹配强在能理解同义表达——用户问"怎么退货",文档里写的是"申请退款流程",关键词匹配可能漏掉,但语义检索能命中。
选 Embedding 模型时有几个维度要考虑:维度高低、支持的语言、是否支持本地部署、推理速度。维度越高表达能力通常越强,但存储和计算成本也越高。中文场景下要特别确认模型对中文的支持程度,有些英文模型在中文上表现会明显打折。这块后面会专门讲选型。
2.4 AI Agent 与普通调用的区别
普通的大模型调用是"一问一答",你给提示词,它给回答,结束。AI Agent 则是在这个基础上加了"自主决策"和"工具使用"的能力。你可以给 Agent 注册一批工具(比如查天气、查数据库、发邮件),然后给它一个目标,它会自己判断该调用哪个工具、调用几次、怎么把结果组合起来回答。
举个具体场景:你问 Agent"帮我看看明天北京天气,如果下雨就提醒我带伞"。它会先调用天气查询工具拿到结果,然后根据结果判断是否下雨,如果下雨就调用提醒工具。整个过程是模型自己编排的,你只提供了工具和目标。这就是 Agent 和普通问答的本质区别——它有了行动能力。
LangChain4j 里 Agent 的实现主要围绕工具调用(Tool Calling)和对话记忆展开。理解 Agent 的关键是理解"模型如何决定调用工具":模型会输出一个结构化的调用意图,框架解析后执行对应方法,再把结果回传给模型继续推理。这个循环可能执行多轮,直到模型认为可以给出最终答案。
3. 核心细节解析与实操要点
3.1 环境准备与依赖引入
先说环境。Java 版本建议 17 及以上,LangChain4j 的新版本对 17 支持最好,用 8 或者 11 会遇到一些兼容问题。构建工具用 Maven 或 Gradle 都行,我这边用 Maven 演示。核心依赖分几块:LangChain4j 核心包、具体模型厂商的适配包、向量库适配包、文档解析包。
<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-easy-rag</artifactId> <version>0.35.0</version> </dependency>版本号这块要提醒一句:LangChain4j 迭代很快,不同版本之间 API 有变动,网上搜到的教程如果版本对不上,代码可能编译不过。建议锁定一个版本,以官方文档为准。langchain4j-easy-rag这个包很实用,它把文档加载、切分、向量化、存储这一整套流程封装成了一个EmbeddingStoreIngestor,新手用它能在几行代码内跑通 RAG。
注意:如果你用的是国内模型厂商,比如通义千问、智谱,需要引入对应的适配包,而不是 open-ai 包。虽然很多厂商兼容 OpenAI 的接口格式,但用官方适配包会更稳,参数映射也更准确。
3.2 文档加载与文本切分的门道
文档加载相对简单,LangChain4j 提供了FileSystemDocumentLoader、ClassPathDocumentLoader等工具,支持 txt、pdf、docx、md 等常见格式。真正有讲究的是文本切分,这一步直接决定了检索质量。
为什么要切分?因为大模型有上下文长度限制,你不可能把整本手册塞进去;而且检索的粒度太粗,命中一段无关内容会稀释相关性。切分的核心目标是:每一块(chunk)语义相对完整,同时大小适中。
切分策略主要有几种:按固定字符数切、按段落切、按句子切、递归切分。LangChain4j 默认用的是递归切分,它会优先按段落分,段落太长再按句子分,尽量保持语义完整。关键参数有两个:maxSegmentSizeInChars(每块最大字符数)和maxOverlapSizeInChars(相邻块的重叠字符数)。
重叠这个设计很关键。假设一句话被从中间切开,前半句在块 A,后半句在块 B,检索时可能只命中其中一块,导致语义不完整。加上重叠,让相邻块共享一部分内容,能缓解这个问题。我的经验值是:块大小 500 到 800 字符,重叠 50 到 100 字符,中文场景下这个范围比较稳。块太小会导致检索到的信息碎片化,块太大又会引入噪声,需要根据你的文档特点调。
DocumentSplitter splitter = DocumentSplitters.recursive(600, 80); List<Document> documents = FileSystemDocumentLoader.loadDocuments("/path/to/docs", splitter);3.3 Embedding 模型选型实战
Embedding 模型的选择直接决定检索效果,这块值得多花点时间。选型时我一般看几个维度,整理成表格方便对比:
| 维度 | 说明 | 建议 |
|---|---|---|
| 语言支持 | 是否对中文优化 | 中文场景优先选多语言或中文专用模型 |
| 向量维度 | 影响存储和计算成本 | 768 到 1536 维是常见区间 |
| 部署方式 | 云端 API 还是本地 | 数据敏感选本地,追求效果选云端 |
| 推理速度 | 影响入库和检索延迟 | 大批量入库时尤其重要 |
| 成本 | 按 token 计费还是自建 | 数据量大时自建更划算 |
实际选型时,如果数据不敏感、追求开箱即用,用云端 API 最省事。如果数据敏感或者想控制成本,本地部署开源模型是更好的选择。本地模型可以用 Ollama 跑,LangChain4j 有对应的适配。要注意的是,本地模型的推理速度受硬件影响很大,没有 GPU 的话大批量入库会比较慢,建议先在少量数据上验证效果。
还有一个容易被忽略的点:入库用的 Embedding 模型和检索时用的必须是同一个。因为不同模型生成的向量空间不兼容,混用会导致检索结果完全错乱。这个坑我在早期项目里踩过,换了模型忘了重新入库,检索出来的东西驴唇不对马嘴,排查了半天才反应过来。
3.4 向量存储与检索策略
向量存储这块,LangChain4j 提供了内存版和多种外部向量库的适配。内存版InMemoryEmbeddingStore适合开发和测试,重启数据就没了,但胜在零配置。生产环境一般用专门的向量数据库,或者用 Redis、PostgreSQL 的向量扩展。
检索策略上,最基础的是相似度检索,就是找距离最近的 top-k 条。但单纯相似度检索有个问题:它只看语义相似,不考虑关键词精确匹配。比如用户搜一个产品型号"X2000",语义检索可能返回一堆"X系列"的内容,但精确匹配能直接命中。所以进阶方案是混合检索,把语义检索和关键词检索的结果融合,这就是热词里提到的"多路召回"。
多路召回的基本思路是:同时跑语义检索和关键词检索,各自拿到一批候选,然后用融合算法(比如 RRF,倒数排名融合)合并排序。LangChain4j 本身对多路召回的支持还在演进,实践中可以自己组合实现。我的做法是先用纯语义检索跑通,观察哪些查询效果不好,再针对性加关键词召回。
提示:top-k 的 k 值不是越大越好。k 太大,塞给模型的上下文里噪声多,反而降低回答质量,还浪费 token。一般从 3 到 5 开始调,根据效果增减。
4. 实操过程与核心环节实现
4.1 从零跑通第一个 RAG 问答
先跑一个最小可用的 RAG,把链路打通,建立信心。这个例子里我用内存向量库加 OpenAI 兼容接口,你换成任何兼容的模型服务都行。
// 1. 初始化对话模型 ChatLanguageModel model = OpenAiChatModel.builder() .apiKey("your-api-key") .baseUrl("https://your-endpoint/v1") .modelName("gpt-4o-mini") .build(); // 2. 初始化 Embedding 模型 EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .apiKey("your-api-key") .baseUrl("https://your-endpoint/v1") .modelName("text-embedding-3-small") .build(); // 3. 初始化内存向量库 EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>(); // 4. 加载并入库文档 List<Document> documents = FileSystemDocumentLoader.loadDocuments("/path/to/docs"); EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .documentSplitter(DocumentSplitters.recursive(600, 80)) .build(); ingestor.ingest(documents); // 5. 构建检索增强的问答助手 Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .contentRetriever(EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.6) .build()) .build(); // 6. 提问 String answer = assistant.chat("你们的退货政策是什么?"); System.out.println(answer);这段代码里Assistant是一个接口,用AiServices动态生成实现。你只需要定义接口方法,框架会自动处理检索、拼提示词、调模型、返回结果这一整套流程。minScore这个参数是相似度阈值,低于它的结果会被过滤掉,避免把不相关的内容塞给模型。这个值设太低会引入噪声,设太高可能什么都检索不到,需要根据实际数据调。
4.2 对话记忆的实现方式
上面那个例子是无状态的,每次提问都是独立的。但真实场景里,用户会追问,比如先问"退货政策",再问"那运费谁出"。这时候需要对话记忆,让模型知道上下文。
LangChain4j 提供了ChatMemory接口,最常用的是MessageWindowChatMemory,它维护一个固定大小的消息窗口,超出就丢弃最老的。窗口大小要权衡:太小记不住上下文,太大浪费 token 还可能超出模型限制。
ChatMemory memory = MessageWindowChatMemory.withMaxMessages(10); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .contentRetriever(retriever) .build();这里有个细节要注意:对话记忆和 RAG 检索是两回事。记忆是让模型记住"我们聊过什么",检索是让模型知道"知识库里有什么"。追问场景下,检索的查询词可能需要结合历史来改写,否则用户问"那运费谁出",单独拿这句话去检索,很可能检索不到相关内容。进阶做法是引入查询改写,用模型把追问改写成完整的独立问题再检索。这块 LangChain4j 有QueryTransformer相关的能力,可以按需使用。
4.3 用 AI Agent 实现工具调用
Agent 的核心是工具调用。先定义一个工具类,方法上标注@Tool注解,框架会自动把方法描述和参数信息提供给模型。
public class WeatherTools { @Tool("查询指定城市的天气") public String getWeather(@P("城市名称") String city) { // 实际调用天气 API return city + " 明天小雨,18-24 度"; } } WeatherTools tools = new WeatherTools(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(tools) .build(); String result = assistant.chat("明天北京天气怎么样,需要带伞吗?");模型收到问题后,会判断需要调用getWeather,生成调用参数city=北京,框架执行方法拿到结果,再把结果回传给模型,模型据此组织最终回答。整个过程对调用方是透明的,你只管提问。
工具描述写得好不好,直接影响模型能否正确调用。描述要清晰说明工具做什么、参数是什么含义。我见过因为描述含糊导致模型乱调工具的情况,比如把"查询订单"和"查询物流"两个工具描述写得差不多,模型就分不清该用哪个。所以工具描述要尽量区分度高,必要时在描述里写明适用场景。
4.4 参数调优与效果验证
链路跑通之后,效果调优才是重头戏。我一般从几个维度入手:切分参数、检索参数、提示词。
切分参数前面说过,块大小和重叠要调。检索参数主要是 top-k 和相似度阈值。提示词则是告诉模型怎么用检索到的内容,比如"只根据提供的资料回答,资料里没有的信息就说不知道",这句能显著降低幻觉。
验证效果不能靠感觉,要建一个小型测试集。准备二三十个典型问题,每个问题标注期望答案,然后跑一遍看命中率。这个测试集不用很正式,一个 Excel 就行,但有了它你才能客观判断调优有没有效果。我自己的经验是,从零到能用,切分和检索参数的调整贡献最大,提示词优化是锦上添花。
5. 常见问题与排查技巧实录
5.1 检索不到内容怎么办
这是新手最常见的问题,提问后模型说"资料里没有相关信息",但你明明知道文档里有。排查思路按顺序来:先确认文档是否真的入库成功,打印一下向量库里的记录数;再确认 Embedding 模型是否一致,入库和检索用了不同模型是高频错误;然后看相似度阈值是不是设太高,把minScore调低试试;最后看切分是否合理,如果关键信息被切碎了,检索可能命中不到。
还有一种隐蔽情况:文档格式解析出问题。比如 PDF 里的文字是扫描图片,加载出来是空的,自然检索不到。这种情况需要先做 OCR 处理。排查时可以把加载后的Document内容打印出来看看,确认文本正常。
5.2 回答质量差、答非所问
检索到了内容但回答不好,通常是几个原因。一是检索到的内容本身不相关,说明检索环节有问题,回到上一条排查。二是检索到的内容太多太杂,模型被噪声干扰,这时候减小 top-k 或者提高相似度阈值。三是提示词没约束好,模型自由发挥,加上"只根据资料回答"这类约束。
我整理了一个速查表,方便对照:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 检索不到 | 未入库/模型不一致/阈值过高 | 查记录数、核对模型、调阈值 |
| 答非所问 | 检索噪声大/提示词弱 | 减 top-k、加约束提示词 |
| 回答不完整 | 切分过碎/上下文不足 | 增大块大小、增加 top-k |
| 响应慢 | 模型慢/检索慢/块太多 | 换模型、优化向量库、减 top-k |
| 幻觉严重 | 提示词无约束/检索为空 | 加约束、确认检索命中 |
5.3 性能与成本优化
上线之后性能和成本就是绕不开的话题。响应慢通常出在模型推理和向量检索两处。模型这块,能换小模型就换小模型,很多场景小模型够用。检索这块,向量库的索引类型影响很大,数据量大时要用带近似检索的索引。
成本主要是 token 消耗。RAG 场景下,检索到的内容会拼进提示词,top-k 越大 token 消耗越多。所以前面反复强调 top-k 不要盲目调大。另外对话记忆也会累积 token,窗口大小要控制。我的做法是给每个环节都加日志,记录 token 消耗,找出消耗大头针对性优化。
注意:不要为了省钱把 top-k 压到 1 或 2,检索不全导致的回答错误,代价比省下的 token 高得多。这是典型的捡芝麻丢西瓜。
5.4 几个我踩过的坑
第一个坑是版本兼容。LangChain4j 不同版本 API 变动不小,我照着旧教程写的代码在新版本上编译不过,浪费了不少时间。后来养成习惯,锁定版本,升级前先看 changelog。
第二个坑是 Embedding 模型不一致。前面提过,换了模型忘了重新入库,检索结果全乱。现在我的做法是把模型名和入库时间记录在元数据里,检索时校验。
第三个坑是中文切分。有些切分器是按空格切的,对中文不友好,会把句子切得莫名其妙。中文场景一定要确认切分器支持中文,或者用按字符数切分的策略。
第四个坑是并发。向量库和模型调用在高并发下可能成为瓶颈,早期没做限流,压测时直接把模型服务的配额打满了。生产环境一定要加限流和降级。
6. 进阶方向与知识库的延伸思考
6.1 RAG 知识库与结构化知识库的区别
热词里提到了"rag知识库和结构知识库区分以及应用场景",这个点值得单独说。RAG 知识库本质上是非结构化的,它存的是文本片段和对应的向量,擅长处理文档、手册、问答这类自然语言内容。而结构化知识库存的是表格、关系数据,擅长精确查询和聚合,比如"上个月销售额是多少"这种。
两者不是替代关系,而是互补。真实系统里经常需要混合使用:用户问"这个产品的退货政策",走 RAG 检索文档;用户问"我订单号 12345 的状态",走结构化查询。更进阶的做法是把结构化数据也转成自然语言描述入库,或者用 Agent 让模型自己决定走哪条路。理解这个区别,能帮你在设计系统时选对工具,而不是硬用 RAG 去解决所有问题。
6.2 多路召回与重排的实践
前面提过多路召回,这里展开说下落地。多路召回的核心是"召回阶段求全,排序阶段求精"。召回阶段用多种策略(语义、关键词、甚至图检索)各自捞一批候选,宁滥勿缺;排序阶段用重排模型对候选精排,把最相关的排前面。
重排模型和 Embedding 模型不同,它直接对"问题-文档"对打分,精度更高但速度慢,所以只用在少量候选上。LangChain4j 生态里有重排相关的组件,也可以接外部重排服务。实践建议是:数据量小的时候,纯语义检索加调参就够了,别过早引入多路召回增加复杂度;数据量大、查询类型多样时,多路召回的价值才体现出来。
6.3 从 RAG 到 Agent 的演进路径
很多人的学习路径是先把 RAG 跑通,再往 Agent 演进。这个路径是合理的,因为 Agent 的很多能力建立在 RAG 之上。Agent 可以调用 RAG 作为一个工具,也可以调用其他工具,组合出更复杂的能力。
演进时要注意的是,Agent 的调试比 RAG 难得多,因为它的执行路径是模型动态决定的,同样的输入可能走不同的路径。调试 Agent 的关键是加详细的日志,把每一轮的模型输出、工具调用、返回结果都记下来,出问题时能复盘整个决策链。另外 Agent 容易陷入循环调用,要设置最大轮数限制,避免无限循环烧 token。
6.4 给 Java 工程师的学习建议
最后说点个人体会。Java 工程师学 LangChain4j,最大的优势是工程能力扎实,劣势是对 AI 领域的概念不熟。我的建议是概念和实操并行:概念上把 RAG、Embedding、Agent 这几个核心搞明白,不用深究数学原理,理解它们解决什么问题、怎么配合就行;实操上一定要动手跑,光看教程不动手,遇到报错就卡住了。
另外不要被"AI"两个字吓到,LangChain4j 用起来本质上还是 Java 那套东西——接口、实现、依赖注入、Builder 模式,都是你熟悉的味道。真正需要新学的,是那些 AI 特有的概念和调优经验,而这些恰恰是靠动手积累的。我自己的经验是,跑通第一个 RAG 之后,后面的路就顺了,因为你会发现大部分问题都是工程问题,而不是什么高深的算法问题。
后续如果想深入,可以往几个方向扩展:一是把向量库换成生产级的,理解索引和性能优化;二是研究提示词工程,这是投入产出比很高的技能;三是尝试把 AI 能力集成到现有的 Spring Boot 项目里,用@AiService这类注解把 AI 服务做成 Bean,和你的业务代码无缝衔接。这些方向每一个都够写一篇长文,但核心思路是一致的:用工程化的方式,把 AI 能力变成你系统里一个可维护、可测试、可扩展的模块。