☰
Java开发者如何用LangChain4j快速构建大模型应用
2026/10/11 4:53:42 网站建设 项目流程

1. 为什么 Java 开发者现在该认真看一眼 LangChain4j

如果你是一个写了几年 Java 的后端,最近大概率会有一种微妙的焦虑:隔壁做 Python 的同事,三行代码就接上了大模型,搞出了问答机器人、文档助手、智能客服,而自己手里那套 Spring Boot 微服务,好像跟这波 AI 应用开发隔了一层。不是不想学,是生态确实不一样——Python 那边 LangChain、LlamaIndex 一抓一大把,Java 这边能拿得出手的、真正为 Java 工程师设计的 LLM 应用框架,其实并不多。

LangChain4j 就是在这个缝隙里长出来的东西。它的定位很直接:把大模型调用、提示词模板、对话记忆、工具调用、检索增强生成(RAG)这些能力,用 Java 开发者熟悉的方式封装起来,让你不用切语言、不用重搭技术栈,就能在现有的 Java 项目里把 AI 功能做进去。它不是一个玩具库,而是一套有清晰抽象层次的框架,核心目标是让 Java 工程师用自己习惯的编程范式去构建 LLM 应用。

这篇内容适合谁看?三类人。第一类是有 Java 基础、想入门 AI 应用开发但不知道从哪下手的后端工程师;第二类是在公司里被安排去调研“我们能不能用大模型做点什么”的技术负责人;第三类是用过 Python 方案、但项目主体是 Java、想找一个能落地的 Java 侧方案的开发者。我会从整体设计思路讲到具体实操,把每一步为什么这么做、参数怎么选、坑在哪里都讲清楚,尽量让你看完能直接动手。

需要先说明一点:LangChain4j 这个生态迭代很快,API 在不同版本之间会有调整。我下面讲的是基于常见稳定版本的实践思路,具体到你用的版本,类名和方法名可能有细微差异,但核心概念和设计逻辑是相通的。你照着思路走,遇到 API 变化自己对着官方文档微调即可,这才是真正能带走的能力。

2. LangChain4j 的整体设计与核心抽象拆解

2.1 它到底解决了什么问题

要理解 LangChain4j 的价值,得先看清楚 Java 开发者接大模型时的原始痛点。最裸的写法是什么?用 HttpClient 拼一个 JSON 请求体,POST 到某个模型服务的接口,然后解析返回的 JSON,从一堆嵌套字段里把生成的文本抠出来。这个流程本身不难,但一旦你要做稍微像样的应用,问题就来了:提示词要复用怎么办?多轮对话的历史怎么管理?模型返回的格式不稳定怎么兜底?要接不同的模型供应商,难道每个都写一套解析逻辑?

这些问题在 Python 生态里已经被 LangChain 这类框架解决过了,LangChain4j 做的事情本质上是把这些成熟经验用 Java 的方式重新表达。它提供了几个关键抽象:ChatLanguageModel统一了不同模型的调用入口,PromptTemplate管理提示词模板,ChatMemory负责对话记忆,EmbeddingModel和EmbeddingStore支撑向量检索,AiServices则把上面这些能力组合成一个可以直接调用的 Java 接口。

我个人的理解是,LangChain4j 最聪明的地方在于它的“声明式”设计。你定义一个 Java 接口,加上几个注解,框架就帮你把提示词拼接、模型调用、结果解析、记忆管理这一整套流程串起来了。你写的是接口,跑起来的是完整的 LLM 交互链路。这种设计对 Java 工程师特别友好,因为它贴合我们熟悉的面向接口编程思维。

2.2 核心模块的分层逻辑

把 LangChain4j 拆开看,大致可以分成四层,理解这个分层对你后续选型和排错很有帮助。

最底层是模型接入层,对应ChatLanguageModel、StreamingChatLanguageModel、EmbeddingModel这些接口。这一层负责跟具体的模型服务打交道,屏蔽不同供应商的协议差异。你换模型,理论上只需要换这一层的实现,上层代码不用动。

往上一层是能力组件层,包括提示词模板、对话记忆、文档加载器、文本分割器、向量存储等。这些是可以独立使用的积木,你也可以不用 AiServices,自己手动把这些组件拼起来用,灵活性更高但代码量更大。

再往上是编排层,核心就是AiServices。它把模型、记忆、检索器、工具等组装成一个可调用的服务接口,是 LangChain4j 里最“魔法”也最省事的部分。

最上层是应用层,也就是你自己写的业务逻辑。理想情况下,你的业务代码只依赖编排层暴露的接口,底层的模型细节被完全隔离。

这个分层带来的好处是:你可以从最底层开始一点点往上用,也可以直接用最上层快速出活。新手建议先从 AiServices 入手跑通全流程,有了体感之后再往下钻,理解每一层在干什么。

2.3 为什么选它而不是自己造轮子

有人会问,这些封装我自己也能写,为什么要用框架?我的经验是,自己写一个能跑的 demo 很容易,但写一个能上生产的、考虑周全的方案很难。举几个 LangChain4j 已经帮你处理好的细节:模型返回的 JSON 格式不稳定时怎么重试和纠正、对话历史超出上下文窗口时怎么截断、流式输出时怎么处理分块和异常、工具调用的参数怎么从模型输出里安全解析。这些坑你迟早会踩,框架帮你踩过了,你就能把精力放在业务上。

当然,用框架也有代价,就是多了一层抽象,出问题时排查链路变长。所以我的建议是:先用框架快速验证,同时理解它背后的原理,等真正遇到框架解决不了的问题时,你有能力绕过它自己实现。这也是我写这篇内容想达到的效果——不只教你调 API,更让你理解这套东西是怎么转起来的。

3. 环境准备与第一个可运行示例

3.1 依赖引入与版本选择

LangChain4j 是模块化的,你不需要一次性引入所有东西。核心依赖是langchain4j-core,但实际开发中你通常直接引入具体模型供应商的 starter。以常见的 OpenAI 兼容接口为例,Maven 里大致是这样:

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>0.35.0</version> </dependency>

版本号这里我要提醒一句:LangChain4j 迭代快,不同版本 API 差异不小。0.3x 系列和更早的 0.2x 系列在 AiServices 的用法上就有变化。你引入依赖后,第一件事是去确认你用的版本对应的文档,别拿着旧教程硬套新版本,那是最容易浪费时间的地方。

如果你用的是 Spring Boot,还有对应的langchain4j-spring-boot-starter,能通过配置文件管理模型参数,更适合生产项目。新手阶段我建议先不用 starter,手动创建模型对象,这样每一步发生了什么你都看得见。

3.2 模型对象的创建与参数含义

创建模型对象是第一步。以 OpenAI 兼容接口为例,典型写法是这样:

ChatLanguageModel model = OpenAiChatModel.builder() .baseUrl("https://your-model-endpoint/v1") .apiKey("your-api-key") .modelName("your-model-name") .temperature(0.7) .timeout(Duration.ofSeconds(60)) .build();

这里几个参数值得展开说。baseUrl指向模型服务的地址,很多兼容 OpenAI 协议的服务都可以用这个方式接入。modelName是你要调用的具体模型标识。temperature控制输出的随机性,取值一般在 0 到 2 之间,越低越确定、越高越发散。做事实性问答、信息抽取这类任务,我一般设 0.1 到 0.3;做创意文案、头脑风暴,可以设到 0.8 以上。timeout一定要设,大模型响应慢是常态,不设超时你的线程可能一直挂着。

注意:apiKey 千万不要硬编码在代码里提交到仓库。用环境变量或者配置中心管理,这是最基本的安全习惯,我见过太多因为密钥泄露被刷爆额度的案例。

3.3 跑通第一段对话

模型对象建好之后,最简单的调用就是发一条消息:

String answer = model.generate("用一句话解释什么是向量数据库"); System.out.println(answer);

generate方法接收一个字符串,返回模型生成的文本。这一步跑通,说明你的网络、密钥、模型名都没问题。如果报错,优先检查三件事:baseUrl 是否可达、apiKey 是否有效、modelName 是否拼写正确。这三个是最常见的翻车点。

跑通单轮之后,你可以试试多轮。但这里有个关键认知:generate是无状态的,它不记得你上一句说了什么。要实现多轮对话,你得自己把历史消息带上,或者用 LangChain4j 的记忆组件。这就是下一节要讲的内容。

4. 对话记忆与 AiServices 声明式开发

4.1 为什么需要对话记忆

大模型本身是无状态的,每次调用都是独立的。你跟它说“我叫张三”,下一句问“我叫什么”,它答不上来,因为第二次调用它根本没看到第一句。要实现连贯对话,就得在每次请求时把之前的对话历史一起发过去。

手动管理历史很烦:你要维护一个消息列表,每次调用前把历史拼进去,还要控制总长度别超出模型的上下文窗口。LangChain4j 的ChatMemory就是干这个的。最常用的是MessageWindowChatMemory,它保留最近 N 条消息:

ChatMemory memory = MessageWindowChatMemory.withMaxMessages(20);

withMaxMessages(20)表示最多保留 20 条消息,超出的旧消息会被丢弃。这个数字怎么定?取决于你的模型上下文窗口大小和单条消息的平均长度。上下文窗口是模型一次能处理的最大 token 数,历史消息、当前问题、模型回答都算在里面。如果你保留太多历史,可能挤占当前问题的空间;保留太少,对话又接不上。20 条是个常见的起步值,实际用的时候根据场景调。

4.2 AiServices 的声明式玩法

手动拼记忆、调模型、解析结果,代码写起来还是啰嗦。AiServices 把这套流程封装成了一个接口。你定义一个接口:

interface Assistant { String chat(String userMessage); }

然后用 AiServices 把它组装出来:

Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .build();

之后你就可以像调普通 Java 方法一样调它:

String reply = assistant.chat("你好,我叫张三"); String reply2 = assistant.chat("我叫什么名字?");

第二次调用时,框架会自动把第一次的对话历史带上,模型就能答出“你叫张三”。这就是声明式开发的威力——你只描述“我要一个能对话的助手”,具体怎么拼提示词、怎么管记忆、怎么调模型,框架全包了。

我实测下来,这套机制在快速原型阶段效率极高。但它也有代价:你对底层流程的控制变弱了。所以我的建议是,原型阶段用 AiServices 快速验证,等需求明确、要精细化控制时,再考虑手动组装组件。

4.3 系统提示词与角色设定

一个只会聊天的助手没什么用,你得给它设定角色和边界。LangChain4j 支持通过注解或者系统消息来设定。用注解的方式:

interface Assistant { @SystemMessage("你是一个专业的 Java 技术顾问,只回答 Java 相关问题,其他问题礼貌拒绝。回答要简洁,多用代码示例。") String chat(String userMessage); }

@SystemMessage里的内容会作为系统提示词发给模型,相当于给模型定规矩。这个提示词写得好不好,直接决定助手的表现。我的经验是,系统提示词要具体、可执行,别写“你要专业”这种空话,要写“回答控制在 200 字以内”“涉及代码时给出可运行的示例”“不确定的问题要说明不确定,不要编造”。越具体,模型越听话。

提示:系统提示词不是越长越好。太长的提示词会占用上下文空间,还可能让模型抓不住重点。把最关键的约束放在前面,次要的往后放。

5. 检索增强生成(RAG)实战拆解

5.1 RAG 要解决的核心问题

大模型有两个硬伤:一是知识有截止日期,训练之后发生的事情它不知道;二是它不知道你私有的数据,比如你公司的内部文档、产品手册。你直接问它这些,它要么答不上来,要么一本正经地胡说八道。

RAG 的思路很朴素:既然模型不知道,那我就在提问的时候,把相关资料一起塞给它,让它基于资料回答。具体流程是:把文档切块、转成向量存起来;用户提问时,把问题也转成向量,去向量库里找最相似的几块资料;把这些资料和问题一起拼成提示词发给模型。模型看到资料,就能给出有依据的回答。

这个流程听起来简单,但每一步都有讲究。切块切多大、向量模型选哪个、检索返回几块、怎么拼提示词,都会影响最终效果。下面我拆开讲。

5.2 文档加载与切分的关键参数

第一步是把文档读进来。LangChain4j 提供了各种DocumentLoader,能读文本、PDF、网页等。读进来之后是切分,这一步最容易被忽视,但影响巨大。

为什么要切分?因为模型上下文窗口有限,你不可能把一整本书塞进去。而且检索的粒度太粗,找出来的资料会包含大量无关内容,干扰模型判断。切分的核心参数是块大小(chunk size)和重叠大小(overlap)。

块大小一般设 300 到 800 个字符(或 token,看你的分割器按什么算)。太小,单块信息不完整;太大,检索精度下降。重叠大小一般设块大小的 10% 到 20%,目的是让相邻块之间有内容重叠,避免一个完整的句子被硬生生切断,导致语义丢失。

DocumentSplitter splitter = DocumentSplitters.recursive(500, 50); List<Document> chunks = splitter.split(document);

这里recursive表示递归分割,它会优先按段落分,段落太大再按句子分,句子还大再按字符分。这种策略比单纯按固定长度切要合理得多,因为它尽量保持语义单元的完整。

5.3 向量化与检索的实操要点

切好的块要转成向量存起来。向量化用EmbeddingModel,存储用EmbeddingStore。内存版的InMemoryEmbeddingStore适合测试,生产环境一般用专门的向量数据库。

EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel(); EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>(); for (Document chunk : chunks) { Embedding embedding = embeddingModel.embed(chunk.text()).content(); store.add(embedding, chunk.textSegment()); }

检索的时候,把用户问题转成向量,去库里找最相似的:

Embedding queryEmbedding = embeddingModel.embed(question).content(); List<EmbeddingMatch<TextSegment>> matches = store.findRelevant(queryEmbedding, 3);

findRelevant的第二个参数是返回的匹配数量,一般设 3 到 5。返回太多会塞进无关内容,返回太少可能漏掉关键信息。这个值需要根据你的文档特点调,没有万能答案。

注意:向量模型和检索必须用同一个模型。用 A 模型生成的向量,拿 B 模型的问题向量去检索,结果会完全错乱。这是新手常犯的错误,一定要记住。

5.4 把 RAG 接进 AiServices

手动检索再拼提示词也可以,但 LangChain4j 提供了ContentRetriever抽象,能直接接进 AiServices:

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

这样配置之后,你每次调用 assistant,框架会自动去检索相关资料,拼进提示词。minScore是相似度阈值,低于这个分数的资料会被过滤掉,避免把不相关的内容塞给模型。这个阈值设多少要看你的向量模型,一般 0.6 到 0.8 之间试。

我踩过的一个坑是:一开始没设 minScore,结果检索出来一堆勉强相关的块,模型被这些噪音干扰,回答质量反而下降。加上阈值过滤之后,效果明显改善。所以别偷懒,这个参数值得花时间调。

6. 工具调用与常见问题排查

6.1 让模型调用你的 Java 方法

大模型再强,也没法直接查你的数据库、调你的接口。工具调用(Tool Calling)就是解决这个问题的:你把一些 Java 方法暴露给模型,模型在需要的时候会告诉你“我要调这个方法,参数是这些”,你的代码执行完再把结果返回给模型,模型基于结果继续回答。

在 LangChain4j 里,给方法加@Tool注解就行:

class WeatherService { @Tool("查询指定城市的当前天气") String getWeather(@P("城市名称") String city) { // 实际查询逻辑 return "晴,25度"; } }

然后在 AiServices 里注册:

Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new WeatherService()) .build();

之后你问“北京天气怎么样”,模型会识别出需要调用getWeather,框架自动执行方法并把结果回传。@Tool里的描述很重要,模型靠它判断什么时候该用这个工具,所以要写清楚工具是干什么的。@P注解描述参数含义,帮助模型正确填参。

提示:工具方法的描述要像写给同事看的接口文档一样清楚。模型不理解你的业务,它只能靠描述来判断。描述模糊,模型就会乱调或者不调。

6.2 常见问题速查表

实际开发中遇到的问题,八成集中在下面这几类。我整理成表,方便你对照排查。

问题现象可能原因排查方向
调用超时网络不通或模型响应慢检查 baseUrl 可达性,调大 timeout
返回 401/403密钥无效或权限不足核对 apiKey,确认账号额度
回答答非所问提示词不清晰或温度过高优化系统提示词,降低 temperature
多轮对话失忆记忆未配置或消息数太少检查 ChatMemory 配置,调大窗口
RAG 检索不准切块不合理或阈值不当调整 chunk size 和 minScore
工具不被调用工具描述不清重写 @Tool 描述,说明使用场景
中文乱码编码不一致统一用 UTF-8

这张表覆盖了我遇到的大部分情况。排查的核心思路是:先确认基础链路通不通(网络、密钥、模型名),再确认配置对不对(记忆、检索、工具),最后才是优化效果(提示词、参数)。

6.3 几个我踩过的坑

第一个坑是上下文窗口溢出。有一次我保留了很长的对话历史,加上 RAG 检索的资料,总 token 数超过了模型上限,结果请求直接报错。后来我加了历史截断和资料数量限制,问题解决。教训是:任何往提示词里塞内容的地方,都要考虑总量控制。

第二个坑是流式输出的异常处理。流式输出体验好,但分块传输过程中如果网络抖动,处理起来比一次性返回麻烦。我的做法是给流式接口加完整的异常回调和超时控制,别假设网络永远稳定。

第三个坑是向量模型选型。一开始我随便选了个向量模型,检索效果很差。换了一个针对中文优化的模型之后,效果提升明显。向量模型对中文的支持差异很大,做中文 RAG 一定要选中文效果好的,别想当然。

第四个坑是提示词里的变量注入。如果你把用户输入直接拼进提示词,用户可能输入一些奇怪的内容干扰模型。虽然 LangChain4j 的模板机制有一定隔离,但涉及敏感操作时,还是要在业务层做输入校验。

7. 从 Demo 到可用:一些工程化建议

跑通 demo 只是第一步,真要放到项目里用,还有几件事得考虑。第一是配置外置,模型地址、密钥、参数都别写死在代码里,用配置文件或配置中心管理,方便切换环境。第二是降级方案,模型服务可能不稳定,要有兜底逻辑,比如超时后返回缓存结果或友好提示,别让整个功能挂掉。第三是成本控制,大模型调用是按量计费的,要监控调用量和 token 消耗,设置合理的限流和预算告警。第四是日志与可观测,把每次调用的输入输出、耗时、token 数记下来,出问题时才有据可查。

我个人的体会是,LangChain4j 帮你解决了“怎么调模型”的问题,但“怎么把 AI 功能稳定地跑在生产环境”这个问题,框架只能帮你一部分,剩下的得靠工程经验。这两件事分开看,你就不会对框架有不切实际的期待。

最后分享一个我常用的调试技巧:当你觉得模型回答不对劲时,先把实际发给模型的完整提示词打印出来看看。很多时候问题不在模型,而在你拼进去的提示词本身就有问题。看到真实的输入,问题往往一目了然。这个习惯帮我省了大量瞎猜的时间。

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

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

立即咨询