第一次看到 LangChain4j 这个词,我的第一反应是:Java 生态终于有人管这事了。过去一年多,大家聊大模型应用开发,张口闭口都是 LangChain、LlamaIndex 这些 Python 框架,Java 工程师要么捏着鼻子去学 Python,要么就是用 HttpClient 手搓大模型 HTTP 接口,写出来自己都不想看第二遍。LangChain4j 的出现,等于在 Java 世界和大模型之间搭了一座桥,而且不是随便搭的,是按 Java 开发者的习惯重新设计过的。
这篇东西不是官方文档的翻译稿,是我在实际项目里从零开始、趟了一遍坑之后沉淀下来的 LangChain4j 新手入门实战思路。内容覆盖核心概念、环境搭建、以及三个能直接跑的实战案例:带记忆的多轮对话、RAG 私有文档问答、Function Calling 工具调用,最后附一份踩坑清单。适合这几类人看:准备在公司 Java 项目里接入大模型能力的后端工程师、想用 Java 快速做 LLM Demo 的同学、以及所有被 Python 生态劝退但又不甘心放弃 LLM 应用开发的 Java 开发者。
1. 为什么 Java 开发者需要 LangChain4j
1.1 被 Python 统治的 LLM 应用开发圈
先说说很多 Java 后端团队的真实处境。公司核心业务是 Java 写的,会员、订单、工作流、报表系统全是 Spring Boot 那一套,产品和老板一拍脑袋说“我们也上个 AI 功能”,结果你打开网上一搜,热门教程几乎全是 Python。这倒不是说 Java 做不了大模型应用,而是过去一年多,LLM 应用框架的早期红利几乎全被 Python 吃掉了。Python 生态确实灵活,快速验证想法很快,但现实问题是,Java 团队引入一种新语言,会带来一连串的代价:CI/CD 流水线要维护两套,代码规范要重新定,运维同学要学新工具的排障方式。对一个体量不小的后端团队来说,这个隐性成本是真实的。
所以 LangChain4j 这类 Java 原生框架的价值,首先不在于它比 LangChain 强多少,而在于它让 Java 团队可以用自己熟悉的技术栈直接落地大模型功能,不用为了一个聊天机器人单独养一套 Python 服务。这一点在技术选型上的意义,跑过生产环境的同学应该都能体会。
1.2 LangChain4j:不是移植,是 Java 自己的长法
很多人在第一次接触 LangChain4j 的时候,直觉认为它是 LangChain 的 Java 翻译版。这是最常见的误解。LangChain4j 是独立设计的框架,只是借鉴了“链式编排”的核心理念,API 风格完全是按照 Java 开发者的习惯重新设计的。
举个例子,LangChain4j 里最核心的一个类叫AiServices,它允许你用“声明一个接口”的方式来定义 AI 交互。你不用去手工拼接 Prompt、管理一堆回调函数,而是像写正常业务代码一样,定义一个方法,然后 LangChain4j 会在幕后把方法调用变成一次模型对话。这个概念在实际用起来非常震撼,因为它意味着 AI 能力可以像 Spring 的 Service 一样自然嵌入到业务代码里。接口驱动、类型安全、依赖注入,这些都是 Java 开发者最熟悉的模式,LangChain4j 就是按这个思路长的,而不是把 Python 代码翻译成 Java 语法。
1.3 它真正解决的是“贴近业务”和“类型安全”
我实际用过一段时间之后,最大的体感是:LangChain4j 的核心价值在于“贴近业务”。你用AiServices定义接口,接口方法的含义就是业务语义,这段代码放在团队里谁都能看懂。相比之下,Python 那种自由拼接 Prompt 的方式,虽然灵活,但对工程化和代码审查并不友好。
同时,强类型带来一个很实际的好处:IDE 补全、编译期检查、重构安全。接触过大型 Java 项目的工程师都知道,类型安全能提前暴露多少问题。你在 LangChain4j 里操作UserMessage、AiMessage、ChatMemory这些对象,所有字段和响应类型都是明确定义的,而不是到处透传Map<String, Object>然后运行时再猜。对于企业级应用的落地,这一点几乎只可意会不可言传,但写过的都懂。
2. 核心概念与环境准备
2.1 五个必须提前搞懂的核心概念
网上 LangChain4j 的资料不少,但很多上来就贴代码,对新手不太友好。我建议你先花二十分钟把下面五个概念过一遍,后面写代码会顺畅很多。
ChatLanguageModel:整个框架的入口抽象。它屏蔽了不同大模型服务商的接口差异,不管是连本地部署的模型还是云端的模型,你面向的都是同一个接口。你就把它理解成“一个会说话的 Java 对象”。
ChatMessage 体系:模型对话的本质是消息序列。LangChain4j 把消息分成几种类型,最常用的三个是SystemMessage(系统设定)、UserMessage(用户输入)、AiMessage(模型回复)。消息的增删改查都有专门的对象,不是朴素的字符串数组。
ChatMemory:决定模型“记住”多少轮对话。模型本身没有记忆,你发给它的每一次请求都是独立的,ChatMemory 做的事是帮你把历史消息收起来,下一次请求时再拼回去。怎么拼、拼多少条,都通过它的配置来控制。
AiServices:前面提过,是框架的杀手锏。通过接口方法定义 AI 能力,还可以把工具调用、记忆、检索组件注入进去,属于进阶玩法。
EmbeddingModel / EmbeddingStore / ContentRetriever:这三个是为 RAG(检索增强生成)服务的核心组件。EmbeddingModel 负责把文本转成向量,EmbeddingStore 负责存向量,ContentRetriever 负责从向量库里把和问题最相关的片段捞出来。后续第四个实战部分会详细用到。
这五个概念对应着 LangChain4j 的五个核心模块,理解它们是理解整个框架的地基。
2.2 Maven 依赖怎么引
LangChain4j 按模块拆分依赖,核心思路是:你只需要引一个核心包,再加上你实际要用的模型适配包。我的建议是先用本地模型快速跑通全流程,成本低,调试也方便。
以本地模型工具 Ollama 为例,Maven 依赖配置如下:
<properties> <langchain4j.version>1.0.0-beta1</langchain4j.version> </properties> <dependencies> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-ollama</artifactId> <version>${langchain4j.version}</version> </dependency> </dependencies>如果你手头有的是云端模型服务商的 API Key,只需要把langchain4j-ollama换成对应的官方适配模块,代码层面基本不用改。这也是 LangChain4j 模块化设计的优势,你可以在本地用免费模型验证代码逻辑,上生产再切换更高配置的模型。
版本号这里要多说一句。这个框架迭代非常快,截止我写这篇内容时,稳定版本已经到 1.0.0 系列,但版本还处于快速演进期,强烈建议去 Maven Central 看一眼最新的 release 版本,不要直接复制网上旧教程里的版本号,否则会遇到 API 对不上的问题。
2.3 第一个 Hello LLM:三行代码跑通对话
环境准备好了,第一个程序不用整复杂的,就跑一个最基础的文本对话。我用本地模型做例子,你只需要确保本地 Ollama 已经拉下来一个模型(比如qwen2.5:7b),然后启动服务。
import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.ollama.OllamaChatModel; public class HelloLlm { public static void main(String[] args) { ChatLanguageModel model = OllamaChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .temperature(0.7) .build(); String answer = model.chat("你好,请用一句话介绍你自己。"); System.out.println(answer); } }这段代码跑完,你就已经完成了 LangChain4j 的第一次真实调用。整个流程你感受一下:不需要自己拼 HTTP 请求,不需要管鉴权头,不需要解析流式响应,就一个.chat()方法。如果换成云端模型,本质也是一模一样的,只是配置里换成 API Key 和模型名。
注意:
model.chat("...")这种直接传 String 的重载,底层会自动把 String 包装成 UserMessage。实际项目里我更推荐显式构造消息对象,后面会讲为什么。
3. 实战一:带记忆的多轮对话助手
3.1 为什么直接调 API 解决不了“多轮对话”
很多刚接触大模型的同学会有一个疑问:我明明可以一次性把历史消息都传过去,为什么还需要一个“记忆组件”?答案是:可以,但代码会很痛苦。
大模型的 API 设计上本身是无状态的,你要跟它多轮对话,就必须每次请求时把所有历史消息重新发给它。这个逻辑看起来不复杂,但是一旦你的业务场景稍微复杂一点,问题就来了:用户对话中间可能插入身份信息、你可能需要动态改变系统提示词、你还要考虑历史消息太长时的截断策略。这些如果全部手写,代码会迅速膨胀,Bug 也容易出现在边缘情况里。
LangChain4j 的ChatMemory就是把你从这些琐碎里解放出来的东西。你只要在构建时告诉它“保留最近多少条消息”,它就会自动维护消息队列,往后再加和往前挤掉都是它的事。你唯一要做的,是在接口方法上把这个记忆对象配置进去。
3.2 用 AiServices + ChatMemory 实现带记忆的助手
直接上代码。这里我们用AiServices定义一个助手接口,让它拥有记忆能力:
import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.service.AiServices; interface Assistant { String chat(String userMessage); } public class MemoryDemo { public static void main(String[] args) { ChatLanguageModel model = OllamaChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .build(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build(); System.out.println(assistant.chat("你好,我叫老张。")); System.out.println(assistant.chat("你还记得我叫什么吗?")); } }这段代码的效果是:第二句“你还记得我叫什么吗?”模型能答出“老张”。第一次跑通的那一刻,你会隐约感觉到“这不就是一个有状态的 Service 吗”。确实是,LLM 应用一旦被抽象成服务接口,整个开发范式就回归到了 Java 开发者最熟悉的模式。
MessageWindowChatMemory.withMaxMessages(20)是滑动窗口实现。消息超过 20 条时,最早的会被挤出去。为什么是 20 条而不是 100 条?因为每条消息都要随请求一起发给模型,按 token 计费,历史越多成本越高,模型响应也越慢。20 条对大多数闲聊场景足够,但如果是复杂业务分析,你可能需要改成按 token 上限截断,甚至引入向量记忆,那就是后面 RAG 的范畴了。
3.3 流式输出:让对话体验“活”起来
非流式输出有一个很明显的问题:用户发出问题后,界面要等模型完整生成完所有内容才一次性显示。本地小模型还好,如果是大一点的模型,生成一段话可能要几十秒,没有用户愿意盯着空白界面干等。
LangChain4j 有专门的流式模型接口,改造起来不复杂:
import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import dev.langchain4j.model.chat.StreamingChatLanguageModel; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; StreamingChatLanguageModel model = OllamaStreamingChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .build(); model.chat("讲一个简短的冷笑话", new StreamingChatResponseHandler() { @Override public void onPartialResponse(String partialResponse) { System.out.print(partialResponse); } @Override public void onError(Throwable error) { error.printStackTrace(); } });提示:流式输出在真实产品里几乎是必选项。它不仅仅影响体验,还影响你能否在 Web 场景里做 SSE 推送。如果你接的是 Web 前端,后端通常会把流式返回转换成 SSE 事件流推给浏览器。
4. 实战二:RAG 让模型“读懂”你的私有文档
4.1 先理解 RAG 在解决什么问题
大模型的能力边界有两个致命短板:一是知识截止日期,训练完之后的世界变化它不知道;二是私有知识,你公司的内部文档、产品手册、售后话术,它从来没学过。直接拿这些问题去问模型,它要么一本正经地胡说八道,要么就是“我不了解相关信息”。
RAG 的思路非常直白:既然模型不知道,那我们就“查了之后再让它回答”。先把文档切块、转成向量存起来,用户提问时先检索最相关的几块内容,把这些内容拼进 Prompt 里,再让模型基于这些内容作答。这一套流程下来,模型回答的可信度会明显提升,而且不涉及模型重新训练,落地成本可控。
LangChain4j 对 RAG 的封装属于“把复杂留给自己,把简单留给开发者”的类型。整个链路:加载文档、切分、向量化、存储、检索、生成,每一步都有组件对应。
4.2 文档切分、向量化与检索的调参思路
先看文档切分。很多新手图省事,把整个文档直接塞给模型,理由很简单“这样信息最全”。但问题在于:上下文长度有限,塞完整的文档会挤占模型的输出空间;而且检索是按片段相似度找的,不是按文档为单位找的,一个大文档就是一个向量的话,检索精度会非常差。
我的经验是,第一次切分用 300 字左右的块,块与块之间重叠 30 到 50 个字。重叠的目的是防止一句话被拦腰截断在两个块里,导致检索时语义不完整。这个参数不是固定的,如果你的文档是结构化很强的技术规范,块可以放到 500;如果是口语化的 FAQ,200 到 300 更合适。实际项目需要试,但新手可以从这个默认值起步。
向量化的部分,务必注意中文场景的嵌入模型选型。英文世界很流行的嵌入模型,对中文的支持不一定好。我建议用对中文友好的模型,本地可以用nomic-embed-text或bge-m3这类,效果好、响应也快。
4.3 一组能直接跑的 RAG 代码
下面这段代码演示了:加载一段文档文本、切分、向量化、存入内存向量库,然后通过AiServices的contentRetriever把检索能力注入到助手接口里。
import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.ollama.OllamaEmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; public class RagDemo { public static void main(String[] args) { // 1. 准备模型 ChatLanguageModel chatModel = OllamaChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .build(); EmbeddingModel embeddingModel = OllamaEmbeddingModel.builder() .baseUrl("http://localhost:11434") .modelName("nomic-embed-text") .build(); // 2. 加载并切分文档 Document document = Document.from("这里放你的私有文档内容,比如公司售后政策:售后有效期30天,退货运费由买家承担……"); List<TextSegment> segments = DocumentSplitters.recursive(300, 30).split(document); // 3. 向量化并存储 EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>(); List<Embedding> embeddings = embeddingModel.embedAll(segments).content(); store.addAll(embeddings, segments); // 4. 构建检索器并注入 EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(3) .build(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .contentRetriever(retriever) .build(); // 5. 提问 System.out.println(assistant.chat("退货运费谁承担?")); } }注意maxResults(3)这个参数,意思是检索时最多取最相关的 3 个片段拼进 Prompt。取太少可能漏掉关键信息,取太多会撑爆上下文,3 到 5 是常见区间。
4.4 内嵌 VS 外部向量库:场景分水岭
上面用InMemoryEmbeddingStore是内存存储,它在两个场景下适用:一是本地联调,二是数据量很小、可以全量加载到单台机器内存的演示项目。但生产环境就不同了,你的知识库可能有几百 MB 甚至几个 GB,单进程内存装不下,而且服务重启数据就丢了,这就是外部向量库该上场的时候。
生产环境下,LangChain4j 对 PostgreSQL、Redis、Elasticsearch 等都有对应的存储适配模块。我个人的建议是:如果你的公司本来就在用 PostgreSQL,优先接它的向量扩展,少引入一个新组件;如果数据量很大且检索性能是核心瓶颈,再考虑独立的向量数据库。选型没有绝对值,重点是先跑通功能,再考虑性能。
5. 实战三:Function Calling 让模型替你调 Java 方法
5.1 模型怎么知道要调用你的方法
Function Calling(函数调用/工具调用)是 LLM 应用从“聊天机器人”进化为“智能代理”的关键一步。它的原理不复杂:你在代码里定义一个普通方法,并用@Tool注解告诉模型“这个方法能干什么”;用户问了一个你需要动手查数据、算结果的问题时,模型不直接硬答,而是输出一个“调用指令”;LangChain4j 收到指令后,在 Java 侧执行对应方法,再把方法的返回结果作为上下文交还给模型,由模型生成最终回答。
整个链路的核心是有两层:模型侧的推理决策,和框架侧的反射执行。LangChain4j 在框架侧做得很干净,你完全不用关心模型的 JSON 参数解析过程,框架会根据方法签名自动生成模型的工具描述,并在调用时完成参数解析和类型转换。
5.2 @Tool 注解写一个查天气的工具
下面是最简单的示例。我们定义一个查天气的工具方法,让模型在用户询问天气时主动调用它:
import dev.langchain4j.agent.tool.Tool; class WeatherTools { @Tool("根据城市名查询当前天气,输入城市名,返回天气描述") String getCurrentWeather(String city) { // 真实项目里这里会调气象服务商 API if (city.contains("北京")) { return "晴,气温 28 度,微风"; } if (city.contains("上海")) { return "多云,气温 25 度,南风3级"; } return "暂无该城市数据,请检查城市名"; } } public class ToolDemo { public static void main(String[] args) { ChatLanguageModel model = OllamaChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .build(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new WeatherTools()) .build(); System.out.println(assistant.chat("北京今天天气怎么样?")); } }@Tool注解里的描述非常关键。模型是靠这个描述来决定要不要调用工具的,描述越准确,调用决策越精准。如果你写出“根据城市名查询当前天气,输入城市名,返回天气描述”,模型就能精确理解参数含义和输出格式。
方法名、参数名、参数类型都会被框架用于生成模型侧的工具定义。方法名尽量用语义清晰的动词,参数类型尽量用基本类型或简单对象,避免复杂的嵌套结构,否则模型在生成参数时容易出错。
5.3 工具调用场景的扩展思路
工具调用的价值远不止于查天气。真实业务里,你可以把以下能力封装成工具:
- 企业内部 API 调用:查订单状态、查库存、查客户信息
- 业务计算逻辑:运费试算、折扣计算、税费估算
- 外部服务集成:查航班、查汇率、发邮件、创建工单
- 数据库查询:把“查询最近一周销售额”变成模型调用统计 SQL 的场景
一个务实的发展路线是:先用 RAG 解决“模型不知道的知识”,再用 Tool 解决“模型需要动手做的事”,两者结合,AI 应用的能力边界会一下子变得很宽。而且 LangChain4j 支持在一个工具类里写多个方法,记得每个方法都带上准确的@Tool描述,模型会自动选择合适的那个去调用。
注意:工具方法要保证幂等性和可控性。模型可能会因为指令被注入而触发意想不到的工具调用,涉及写操作、支付、删除类的工具方法,必须在方法内部做二次校验和权限控制。生产环境尤其要注意这一点。
6. 踩坑记录与调优建议
6.1 版本锁不对,代码全白给
LangChain4j 的 API 还在快速演进期。我在实践中碰到最坑的一次是:教程用的是 0.35 的 API,我引了 1.0.0-beta1,结果StreamingChatResponseHandler的抽象方法签名完全不同,编译直接报错。这类问题论坛一搜一大把,解法也很简单:依赖版本以 Maven Central 为准,IDE 里把对应 jar 的源码下下来对照着写,别依赖记忆。
我建议所有新手在项目初始阶段就锁死版本号,不要用LATEST或者RELEASE这种宽泛写法。你永远不知道下一次构建和上次构建拉到的版本差了多少,一旦 API 变动,排查成本极高。
6.2 上下文过长,成本翻倍响应变慢
很多人在跑对话和 RAG 的时候,喜欢把历史消息调得很大,觉得“模型能记住更多”。实际上大模型的注意力机制决定了,上下文越长,计算耗时越长,成本越高,而且模型对中间部分内容的关注度会下降。你的真实诉求是“该记住的记录下来”,不是“所有历史全部保留”。
我个人的实践方案是:普通对话窗口控制在 10 到 20 条消息以内;对需要长期记忆的业务场景,定期把历史消息摘要化,压缩成一段话塞进系统提示词,而不是无限堆原始消息。RAG 场景里,检索片段控制在 3 个左右,每个片段别超过 500 字,这个组合在大多数场景下性价比最高。
6.3 并发场景下的“记忆串台”
这是新手几乎必踩的坑:MessageWindowChatMemory默认实例是无状态存储的,如果你的接口方法被多个用户并发调用,共享同一个记忆对象,就会发生 A 用户的问题被 B 用户的历史干扰这种严重事故。
解决办法是按会话隔离记忆。LangChain4j 提供了ChatMemoryProvider机制,你可以根据会话 ID 为每个用户维护独立的记忆实例。在生产项目里,这一步是必须实现的,不然后果很尴尬:用户 A 问“我的订单到哪了”,模型因为看到了用户 B 的历史,给回答成了“您的快递三天后送到”。这种错误在产品体验上是灾难级的。
6.4 模型选型的现实建议
最后聊一下模型选型。本地模型最大的优势是数据不出内网、可离线调试、迭代成本低,但效果和云端头部模型相比还有差距,尤其是复杂推理和指令遵循场景。我实践下来比较稳的组合是:开发阶段用本地模型把逻辑调通,测试阶段切换成大参数量模型验证效果差异,生产阶段按业务数据敏感度决定跑本地还是云端。
这里有一条很实际的建议:把模型的接入配置做成环境变量驱动,不要写死在代码里。模型名、API Key、baseUrl、超时时间全部走配置中心,这样从本地切云端只需要改配置,代码一行不用动,省下的时间远超你写配置类的时间。
最后说点自己的体会
LangChain4j 这套框架我用下来的最大感受是:它不是把 LLM 开发变简单了,而是把 LLM 开发变得“像 Java 开发者熟悉的样子”了。接口、类型、依赖注入、模块化配置,所有沉淀了二三十年的 Java 工程化经验都能复用进来,这比具体 API 好不好用重要得多。遇到问题时的排查路径也清晰,因为你的代码是强类型的,报错信息指向明确,基本上 IDE 里就能解决掉一大半问题。
如果你现在正准备在 Java 项目里引入大模型能力,我的建议是别一上来就搞高深的 Agent 编排或复杂的多模型协同。先从本地模型跑通一个带记忆的对话,再接一个内部文档的 RAG,最后把现有的业务查询封装成工具方法喂给模型。三步走完,你已经具备在生产环境里做 AI 功能的基础能力了。后续想深入的话,可以继续研究AiServices的高级特性,比如并行工具调用、动态 Prompt 模板,以及接入 Spring Boot 后的完整工程化方案。路不难走,关键是先把第一步迈出去。