☰
Java开发者大模型实战:LangChain4j框架实现记忆、工具调用与RAG
2026/10/26 14:11:12 网站建设 项目流程

做了几年 Java 后端,从 Spring Boot 到微服务都在折腾,但真正让我觉得“Java 开发者也能体面地玩大模型”的,还是 LangChain4j 这套框架。如果你已经受够了网上铺天盖地的 Python LangChain 教程,想在 Java 生态里干净利落地构建对话式 AI 应用,那这篇 LangChain4j 实战教程就是为你准备的。它不是什么高深概念堆砌,而是我实际跑通项目后提炼出来的一套新手可复现路径:从依赖引入开始,逐步覆盖记忆管理、工具调用、RAG 落地,最后拼出一个带记忆和自定义工具的智能助手。

我在刚接触 LangChain4j 的时候,最大的困惑不是“能不能做”,而是“从哪下手”。官方文档偏散,示例大多是最小 Demo,一碰到真实业务就断层。这篇文章完全站在 Java 工程师视角,用 Spring Boot 外的纯 Java 方式先讲透原理,再落到代码,全程带你走一遍真实可运行的完整项目。不管你是刚入门 AI 应用开发,还是正在评估是否要在现有 Java 系统里引入 LLM 能力,这篇教程都会是一个扎实的起点。

1. LangChain4j 到底解决什么问题

1.1 从裸调 API 到智能应用,中间差了什么

很多 Java 开发者第一次接触大模型,拿到的代码往往是这样的:通过 HttpClient 调 REST 接口,把用户消息拼进 JSON,然后等返回。这种做法跑通一次对话没问题,但一旦要做带历史的聊天、让模型操作业务方法、从自家知识库检索答案,问题立刻暴露——对话上下文谁来维护?函数调用参数谁来解析?文档切块和向量化谁来做?全自己撸一遍,工程量不小,而且每个模型商家的接口格式还不一样。

LangChain4j 从设计上就把这些问题收敛了。它抽象出ChatLanguageModel统一建模对话接口,无论是 OpenAI、通义千问、智谱还是本地 Ollama,都是同一套调用方式。对话记忆这块它提供ChatMemory组件,不需要你自己在 Redis 里存消息数组再拼 Prompt;工具调用它用@Tool注解 + AiServices 的代理机制,把 Java 方法安全地暴露给模型;RAG 它则给了一套从文档解析、切分到向量检索的完整链路。换句话说,LangChain4j 帮你把这些“大模型之外的脏活”都包装好了。

需要特别强调一点:LangChain4j 的定位不是“把 LangChain 移植到 Java”,而是参照 LangChain 的核心思想,做了一套贴合 Java 生态的框架。所以不必抱着“LangChain 有什么,LangChain4j 就必须有什么”的心态去学。它的AiServices、ChatMemory、Tool这些概念,你用熟了之后会发现并不比 Python 生态难,反而因为 Java 的强类型,很多参数错误在编译期就被拦住了。

1.2 一个场景对比:无框架与有框架的差距

我拿一个最常见的“智能客服”场景来对比。假设用户问:“我想查一下订单 123456 的物流状态。”同时系统里有一个OrderService.getLogistics(String orderId)方法。

不用框架的做法是:先用正则或模型从用户话里抽取订单号,再写代码判断“用户想查物流”这个意图,最后调用方法拼结果返回。这套流程你的代码里全是if-else意图分支,维护成本极高。而用 LangChain4j 的@Tool,你只需要把getLogistics方法标注成工具,交给模型决定什么时候调用、传什么参数。算是一个相当实用的声明式编程体验转变:Java 工程师从“写代码控制流程”变成“定义能力,让模型编排流程”。

RAG 场景的差距同样明显。自己实现要经历:文档解析 → 文本清洗 → 调用 Embedding 模型接口 → 存向量数据库 → 查询时做相似度检索 → 拼接上下文。每一步都要处理不同供应商的差异。LangChain4j 把链路封装好了,核心操作就是配置EmbeddingModel和ContentRetriever,其余交给框架内部编排。不是说框架能解决所有定制需求,而是它把 80% 的通用路径走顺了,剩下的 20% 留给你基于 SPI 扩展。

1.3 LangChain4j 的核心模块速览

学习 LangChain4j 之前,我建议你脑子里先有一个模块地图,这样看官方文档不会迷路。整个框架从底层到上层大致分这几层:

层次核心组件作用
模型适配层ChatLanguageModel、EmbeddingModel、ImageModel屏蔽各家模型 API 差异
记忆管理层ChatMemory、ChatMemoryStore维护多轮对话上下文
工具增强层@Tool、ToolProvider把 Java 方法暴露给模型调用
业务抽象层AiServices把接口定义映射为 AI 能力服务
RAG 组件层DocumentSplitter、EmbeddingStore、ContentRetriever知识库检索增强生成

理解这个分层之后,你就能明白后面所有实战代码为什么要这么组织。AiServices是骨架,ChatMemory是上下文的载体,@Tool是模型触达业务代码的桥梁。这篇教程接下来的实战项目,就是把这几个模块串起来跑通。

2. 环境搭建与工程初始化

2.1 依赖引入与版本选择

LangChain4j 的版本演进很快,我在实际项目里踩过依赖版本不匹配的坑。现在比较稳妥的做法是看 GitHub Releases 选取0.35.0之后的稳定版本,并且尽量让核心包和模型适配包保持同一版本号。如果你用的是 Maven,最小依赖集合是这样的:

<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>

注意一个容易踩的坑:官方把不同模型厂商的适配器分散到独立 artifact 里,比如langchain4j-ollama、langchain4j-dashscope、langchain4j-zhipu-ai。如果你只想接一个厂商,就别把langchain4j-open-ai和langchain4j-dashscope同时引进来,因为两个适配器会同时注册模型类,某些版本下会出现类加载顺序导致的异常。我自己的建议是:初期只引入核心包 + 一家模型供应商的适配包,实战项目跑通后再横向扩展。

如果你使用 Spring Boot,可以引入langchain4j-spring-boot-starter,但这篇文章先用原生 Java 写,好处是能把框架本身的能力看得更清楚,不被自动装配遮住细节。等到需要整合进现有 Web 服务时,再考虑 Starter 也不迟。

提示:项目编译环境建议 JDK 17+。LangChain4j 官方已经全面转向 Java 17,如果你还在 JDK 8 上挣扎,直接劝退,因为部分模块用了var和record,低版本编译不过。

2.2 第一次与大模型对话的最小代码

所有 LangChain4j 应用的第一步,都是拿到一个ChatLanguageModel。下面这段代码是我建议每个新手最先跑通的“Hello World”:

import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.openai.OpenAiChatModel; public class FirstDemo { public static void main(String[] args) { ChatLanguageModel model = OpenAiChatModel.builder() .apiKey(System.getenv("LLM_API_KEY")) .modelName("gpt-4o-mini") .temperature(0.7) .build(); String answer = model.generate("用一句话解释什么是 Java 反射"); System.out.println(answer); } }

这里model.generate()只有一次调用,不携带任何历史信息。你只需要配置一个环境变量LLM_API_KEY,代码就能跑起来。需要留意的是,不同厂商的模型名参数差异不小,不要把 OpenAI 的模型名直接搬到智谱上。如果你用的是 ollama 本地模型,那构造方式换成OllamaChatModel.builder().baseUrl("http://localhost:11434").modelName("qwen2.5:7b")即可,连 API Key 都不用。

跑通这一步之后,我强烈建议你做一个小的抽象工具类,统一从配置中心读取模型参数,避免后续每个类都 new 一个 Model。比如我项目里通常是这样的套路:

public class ModelFactory { public static ChatLanguageModel createChatModel() { return OpenAiChatModel.builder() .apiKey(System.getenv("LLM_API_KEY")) .modelName(System.getenv("LLM_MODEL_NAME")) .build(); } }

这样才能给后续的多模型切换留出口子。后面接 RAG、工具调用时,所有代码都只需要面向ChatLanguageModel编程,不用关心底层供应商。

2.3 多模型接入策略

LangChain4j 让我比较满意的一点,是模型接入真的做到“换厂不换码”。我实际对接过 OpenAI 兼容协议、Ollama 本地模型、以及国内几家大模型服务,代码结构几乎一致。比如接 Ollama 只需要换 builder,接阿里云百炼就引入langchain4j-dashscope,再用DashScopeChatModel。

不过这里有一个现实问题:不同厂商对工具调用(Function Calling)的支持程度不一致,LangChain4j 在底层做了降级兼容,但我在实测中发现,部分模型传工具定义时 JSON Schema 格式要求很严格,一旦报错优先检查模型版本是否支持 Function Calling,而不是盲目改框架配置。

我还建议新手一开始别做“多模型动态路由”,那是进阶玩法。先把一个模型跑到顺,理解框架抽象层的行为,再尝试抽象自己的ChatModelProvider接口。这个顺序如果反过来,你会被不同模型的响应格式差异搞到心态崩溃。

3. 核心概念实战:记忆、工具调用与 RAG

3.1 对话记忆 ChatMemory:让模型记住上下文

多轮对话里模型本身是无状态的,每次请求都是独立事件。LangChain4j 解决记忆的思路是:把历史消息统一封装到ChatMemory,在生成新回复时自动组装成完整上下文。最简单的用法是MessageWindowChatMemory,它像一个滑动窗口,只保留最近 N 条消息:

ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(20); // 在 AiServices 中使用 Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(chatMemory) .build();

withMaxMessages这个参数需要认真对待。过小会遗忘关键信息,过大则不断堆积 Token 成本。我的经验是:如果模型上下文窗口在 8K 左右,20 条消息搭配系统 Prompt 已经能覆盖大部分业务;如果模型支持 128K 上下文,可以放到 50 条,但你还得考虑响应延迟,因为历史越长,每次请求的发送时间和首字延迟就越明显。

更贴近真实场景的是多用户隔离。如果同一个ChatMemory实例被多个用户共用,用户 A 的话会泄漏到用户 B 的上下文中,这是严重事故。LangChain4j 提供了@MemoryId注解,按用户 ID 区分记忆空间:

interface Assistant { String chat(@MemoryId String memoryId, @UserMessage String userMessage); }

配合PersistentChatMemoryStore,你甚至能把记忆持久化到数据库。我在生产项目里的做法是:内存级ChatMemory做单机测试,Redis 实现ChatMemoryStore做多实例共享。这里有个实现细节,ChatMemoryStore接口只有三个方法getMessages、updateMessages、deleteMessages,自己实现 Redis 版并不复杂。

3.2 工具调用 Tool Calling:让模型操作你的业务方法

LangChain4j 的工具调用机制,本质是把模型的 Function Calling 能力封装成了声明式注解。你不需要解析模型返回的 JSON 参数,框架会帮你完成参数映射和方法调用。下面是我项目里真实的订单查询工具:

@Slf4j public class OrderTools { private final OrderService orderService; public OrderTools(OrderService orderService) { this.orderService = orderService; } @Tool("根据订单号查询订单物流状态") public String getLogistics(@ToolParam("订单号") String orderId) { LogisticsInfo info = orderService.getLogistics(orderId); return "订单 " + orderId + " 当前状态:" + info.getStatus() + ",最新节点:" + info.getLatestNode(); } }

然后在 AiServices 里把它注册进去:

Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(chatMemory) .tools(new OrderTools(orderService)) .build();

用户输入“帮我查一下订单 123456”,模型内部会生成一个工具调用请求,框架自动精准调用getLogistics("123456"),然后把方法返回结果再次发给模型整理成自然语言回复。值得一提的是@ToolParam注解的说明文本非常重要,模型完全依靠这段描述来决定传什么参数,写得含糊就会出现参数映射错误。

我在实际项目中遇到的典型问题是多个工具方法名称相似导致模型选错。比如既有getLogistics又有getOrderInfo,如果描述都写“查询订单”,模型很可能混淆。解决办法是描述文字写清楚差异,比如一个写“查物流轨迹节点”,另一个写“查订单基本信息”。

还有一个细节值得新手注意:工具方法内部的异常不要直接往外抛,尤其不能抛到 AiServices 里把整个对话流程打断。我的做法是工具方法内部 try-catch,返回“查询失败,请稍后重试”这样的兜底文案,让模型把这些信息包装成友好回复。这样用户的对话体验不会因为一条失败调用就崩溃。

3.3 RAG 最小落地:让模型回答你私有知识

RAG 是当前 Java 后端接入知识库最可靠的路径。LangChain4j 提供了一套相对完整的 RAG 组件,从文档加载、文本切分、向量化到检索都覆盖了。我分享一个最小可运行版本。

第一步,准备好一个EmbeddingModel和一个向量存储。为了方便本地演示,可以用内存版InMemoryEmbeddingStore,生产环境再换MilvusEmbeddingStore或PGVectorEmbeddingStore:

EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .apiKey(System.getenv("LLM_API_KEY")) .modelName("text-embedding-3-small") .build(); EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();

第二步,加载文档并入库。LangChain4j 的Document对象比较轻量,可以直接从字符串构造,也可以用TextDocumentParser解析本地文件:

Document document = Document.from(""" LangChain4j 是一个 Java 生态的大语言模型集成框架。 它提供了 ChatMemory、Tool Calling、RAG 等能力。 版本最新动态请关注 GitHub Releases。 """); DocumentSplitter splitter = new DocumentByParagraphSplitter(300, 50); List<TextSegment> segments = splitter.split(document); for (TextSegment segment : segments) { Embedding embedding = embeddingModel.embed(segment).content(); embeddingStore.add(embedding, segment); }

第三步,把检索器注入 AiServices,实现“先检索后生成”:

ContentRetriever retriever = EmbeddingStoreContentRetriever.builder() .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .maxResults(3) .minScore(0.6) .build(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .contentRetriever(retriever) .build();

这样用户提问时,框架会先从向量库中检索相关段落,再把段落拼进 Prompt 一起发送。需要提醒的是,minScore这个阈值我一开始设成 0.8,结果很多模糊问题检索不到内容,调到 0.6 之后覆盖率明显变好。这个值说白了要看你的 Embedding 模型和文档分布,别照抄网上的参数,一定要用自己知识库样本实测。

RAG 的坑主要集中在文本切分上。按段落切分适合规则清晰的文档,但遇到表格、代码块就无法保证语义完整。我的建议是:如果你的知识库以 PDF 和 Word 为主,优先依靠DocumentByParagraphSplitter,并且切分窗口不要超过模型上下文的一半,否则检索结果塞进去后,留给模型生成回复的空间就不够了。

4. 完整实战:带记忆和工具的智能客服助手

4.1 需求场景与接口设计

把前面讲的能力拼起来,我做了一个很小的“智能客服助手”项目。需求很简单:用户可以通过自然语言查询订单物流、获取常见售后问题解答,并且助手要能记住同一用户的多轮上下文。这个项目覆盖了 LangChain4j 80% 的核心 API,跑通它,你基本上就入门了。

我先定义 AI 服务接口。这里用到@SystemMessage设定角色,@MemoryId隔离不同用户记忆:

public interface CustomerServiceAssistant { @SystemMessage(""" 你是某电商平台的智能客服助手。 你可以查询订单物流信息,也可以回答常见售后问题。 回答要简洁友好,不确定时不要编造。 """) String chat(@MemoryId String userId, @UserMessage String userMessage); }

@SystemMessage这个注解值得多说一句,它是设定模型人设的最直观方式,比每次拼接系统 Prompt 干净得多。而且它支持%s占位符,可以从外部传入动态参数,比如员工工号、当前时间等。

4.2 核心代码实现

完整的服务装配代码如下:

ChatLanguageModel model = OpenAiChatModel.builder() .apiKey(System.getenv("LLM_API_KEY")) .modelName("gpt-4o-mini") .temperature(0.3) .build(); OrderTools orderTools = new OrderTools(new OrderService()); EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .apiKey(System.getenv("LLM_API_KEY")) .modelName("text-embedding-3-small") .build(); EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>(); ContentRetriever retriever = EmbeddingStoreContentRetriever.builder() .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .maxResults(3) .minScore(0.6) .build(); CustomerServiceAssistant assistant = AiServices.builder(CustomerServiceAssistant.class) .chatLanguageModel(model) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(12)) .tools(orderTools) .contentRetriever(retriever) .build();

注意到我这里用的是chatMemoryProvider而不是chatMemory。两者区别很大:chatMemory是全局共享一个记忆实例,不适合多用户;chatMemoryProvider则根据@MemoryId动态创建各自的记忆窗口。新手很容易忽略这一点,直接复用官网的chatMemory示例,上线后就会串话。

客服场景还需要考虑一个细节,temperature要调低。客服回复讲究确定性,温度太高模型会自由发挥,编造出不存在的物流节点。我这边设的是 0.3,如果你希望回复更保守,可以调到 0.1 甚至 0。

4.3 运行效果与实测心得

我实际跑了一组对话,记录如下:

  • 用户 A 问:“我的订单 10001 到哪了?”模型调用工具,返回“订单已到达上海中转站”。
  • 用户 A 追问:“大概什么时候能送到?”模型结合上一轮的工具结果,回答“目前在上海中转站,预计明天下午送达”。
  • 用户 B 问:“我的订单 10001 到哪了?”模型在自己的独立记忆中执行工具查询,互不干扰。

这组测试验证了两个关键点:工具结果会被模型作为上下文继续推理,所以连续追问不会丢失关键信息;不同@MemoryId之间确实完全隔离。

还有一个我在实测中发现的体验问题:工具返回内容比较长时,模型的总结有时会把关键细节丢掉。解决办法是在工具方法的返回字符串中做结构化处理,比如把状态码和节点名称显式拼进返回文本,模型总结时的出错率会明显下降。

这个版本的项目虽然不复杂,但它是可扩展的。后面我在此基础上加了工单创建、退款进度查询工具,只需要在OrderTools里新增方法并注册,模型就能自动发现并使用,完全不用改调用链。这种低耦合的扩展方式,是 LangChain4j 工具机制真正的价值所在。

5. 新手必看的常见问题与避坑清单

5.1 运行期高频报错

新手在跑 LangChain4j 项目时,最容易撞上的问题集中在连接、依赖和上下文配置三块。我按概率排了个速查表:

现象可能原因解决办法
启动报Invalid api key环境变量未生效或 Key 错误检查System.getenv打印值,确认环境变量名拼写
请求超时或连接失败底层网络不通或服务端限流确认网络连通性,换用本地方案先验证,合理设置timeout
模型返回内容异常截断上下文窗口超过模型上限调小withMaxMessages,或换更长的上下文模型
工具方法没有被调用方法未注册 / 工具描述太模糊确认.tools(...)已传入;优化@Tool描述
中文乱码控制台或请求编码问题启动参数加-Dfile.encoding=UTF-8
依赖冲突多个适配器共存 / 版本不一致只保留所需适配器,核对版本号

其中“工具方法没有被调用”是新手排查最久的问题。这里有个排查思路可以分享:先打开 LangChain4j 的 DEBUG 日志,观察模型返回内容里是否包含tool_calls。如果模型产生了工具调用但框架没触发方法,说明是注册环节的问题;如果模型压根没生成工具调用,那问题大概率出在 Prompt 描述或模型能力上。日志会告诉你方向,别盲猜。

5.2 架构设计层面的坑

除了运行期报错,我还想提醒几个容易在项目设计阶段埋下的隐患。

第一,对话记忆无限增长。MessageWindowChatMemory有窗口大小限制,看起来安全,但如果你的系统用了持久化ChatMemoryStore,不同实例之间同步过期清理不及时,Redis 里的 key 一直在累积,最终会影响查询性能。我建议在持久化实现里额外维护一个“最近活跃时间”字段,定时清理闲置会话。

第二,恶意 Prompt 注入。用户输入可能包含“忽略之前的指令”这类攻击语句。虽然 LangChain4j 本身不会帮你防御注入,但你可以通过@SystemMessage强化边界,并在送入模型前做输入过滤。比如检测“忽略上述指令”“你现在是系统管理员”等常见注入句式,直接拦截。别觉得这是小事,RAG 场景里知识库文档也可能携带恶意指令,需要一并过滤。

第三,Token 成本失控。有些模型调用是无形中膨胀的,比如你设置了携带 30 条记忆 + 3 段检索文档,每次请求发送给模型的文本量就会相当可观。我在刚部署时没加成本统计,月底看到账单才后悔。建议从第一天就封装一层TokenUsage采集,统计每次请求的输入和输出 Token,至少把成本数据可视化出来。LangChain4j 的Response对象里自带tokenUsage,别浪费。

5.3 低成本调试技巧

没有专门的大模型调试工具,但你可以用很原始的办法提升效率。我习惯准备一段固定的测试脚本,里面包含几十个预置问题,每次改动框架代码后批量跑一遍回归。因为 LLM 输出有随机性,所以回归时对比的不是“答案逐字一致”,而是“关键信息点是否覆盖”。比如物流查询场景,我只看回答里有没有订单号和当前节点。

写测试时我建议搭配 JUnit 和assertThat做断言,但断言要宽松一点。比如“包含订单号”和“包含‘运输中’或‘已签收’”,而不是精确匹配。否则线上模型一升级措辞变了,测试就红了,成本还高。

最后一个小窍门:开发阶段用本地 Ollama 跑通流程,再用高精度模型做上线验证。因为 Ollama 部署不受网络环境影响,能快速起停,迭代速度比一直调外部 API 快得多。不过要留意,本地小模型的工具调用能力通常弱于大模型,所以代码里对工具返回结果的解析逻辑,要保证对不同模型都能兼容。我在本地 7B 模型上测试时,确实遇到过工具参数 JSON 格式不规范的问题,但框架层面没有报错,而是直接返回了原始 JSON 给模型重新理解。

写到这里,我回头看这套实战项目的价值:它让你从一个只会在控制台打印字符串的model.generate()调用,进阶到能够构建带多用户记忆、能操作业务方法、能绑定私有知识库的完整 AI 服务。LangChain4j 真正讨喜的地方在于它尊重 Java 生态的习惯——强类型、注解驱动、依赖注入友好,而不是把 Python 那套随心所欲搬过来。如果你接下来想继续深入,我建议沿着两条线走:一是把ChatMemoryStore替换成 Redis 实现并接进真实业务,二是把InMemoryEmbeddingStore换成 Milvus 或 PGVector,处理十万级以上的文档规模。无论选哪条,当你动手改造的那一瞬间,前面学的核心概念都会真正内化成你自己的东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询