☰
LangChain4j 入门实战:Java 开发者的大模型接入指南
2026/10/22 5:30:10 网站建设 项目流程

1. 从零上手 LangChain4j:Java 开发者的大模型接入第一课

如果你是一名 Java 后端开发者,最近想在自己的项目里接入大语言模型能力,却苦于 Python 生态的 LangChain 用不上、又不想为了一个功能去学一门新语言,那 LangChain4j 基本就是为你准备的。它是一个面向 Java 生态的大模型应用开发框架,核心目标很直接:让 Java 开发者用熟悉的 Maven 依赖、注解、接口和流式 API,把大模型能力接进现有的 Spring Boot 或普通 Java 工程里,而不是被迫切换到 Python 技术栈。

我最初接触它的时候,手上有一个已经跑了三年的订单管理系统,业务逻辑全在 Java 里,产品突然提了个需求:能不能让系统自动根据用户描述生成工单摘要,并且支持多轮追问。用 Python 单独搭一个服务再通过 HTTP 调用当然可行,但多了一套部署链路、多了一份运维成本,数据还要跨服务传递。LangChain4j 的价值就在这里——它把模型调用、提示词模板、对话记忆、工具调用、检索增强这些能力封装成 Java 接口,直接在你原有的工程里用。

这篇内容适合三类人:第一类是 Java 基础扎实但没接触过大模型应用开发的工程师;第二类是已经用过 Spring AI 或直接调过 HTTP 接口,想找一个更体系化框架的人;第三类是做企业级项目、对可维护性和类型安全有要求的团队开发者。我会从依赖引入、第一个可运行 Demo、核心概念拆解、常见踩坑到进阶方向,完整走一遍。需要说明的是,下面涉及的模型服务地址、密钥配置等,都以通用占位方式呈现,你替换成自己实际使用的服务即可。

在正式开始之前,先建立一个认知:LangChain4j 不是"另一个 HTTP 客户端封装",它的核心抽象是AiServices——你定义一个 Java 接口,用注解描述行为,框架在运行时生成代理实现。这个设计思路和 Spring Data JPA 的 Repository 非常像,你写接口不写实现,框架帮你把调用逻辑拼出来。理解这一点,后面的很多用法就顺了。

2. 环境搭建与第一个可运行 Demo 的完整落地

2.1 依赖选型:为什么是 langchain4j-open-ai 而不是全家桶

LangChain4j 的模块划分比较细,官方提供了langchain4j-core、langchain4j-open-ai、langchain4j-ollama、langchain4j-spring-boot-starter等多个 artifact。新手最容易犯的错是一上来就把所有依赖都引进来,结果版本冲突、日志框架打架。

我的建议是分两步走。第一步只引两个:

<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-open-ai提供的是符合 OpenAI 接口规范的模型接入能力。这里有个关键点很多人不知道:市面上大量模型服务都兼容 OpenAI 的接口协议,所以这个模块的适用范围远比名字看起来广。你只需要改baseUrl就能对接不同的服务端点。

版本选择上,0.35.0 是我实测下来比较稳定的一个版本,API 已经趋于固定,社区文档也跟得上。不建议用最新的快照版,LangChain4j 迭代速度很快,快照版经常出现方法签名变动,新手排查起来很痛苦。

第二步,等你跑通基础对话后,再按需引入langchain4j-spring-boot-starter做 Spring 集成,或者引入langchain4j-easy-rag做检索增强。不要提前引入,否则你会被一堆自动配置类搞晕。

2.2 最小可运行代码:五行核心逻辑

先看一个能直接跑起来的最小示例,这是理解整个框架的起点:

import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.chat.ChatLanguageModel; public class FirstDemo { public static void main(String[] args) { ChatLanguageModel model = OpenAiChatModel.builder() .baseUrl("你的服务地址") .apiKey("你的密钥") .modelName("你的模型名称") .temperature(0.7) .build(); String answer = model.generate("用一句话解释什么是Java的垃圾回收"); System.out.println(answer); } }

这五行构建逻辑里,每个参数都有讲究。baseUrl决定请求发往哪里,注意结尾不要带多余的斜杠,否则拼接出来的路径会变成双斜杠导致 404。apiKey是身份凭证。modelName必须和服务端支持的模型标识完全一致,大小写敏感,写错了会返回模型不存在的错误。

temperature这个参数值得单独说。它的取值范围通常是 0 到 2,数值越低输出越确定、越保守,数值越高输出越发散、越有创造性。做代码生成、数据抽取这类任务时,我一般设 0.1 到 0.3;做文案创意、头脑风暴时设 0.8 到 1.2。新手常犯的错是全部用默认值,结果要么回答死板,要么胡言乱语。

2.3 用 AiServices 把调用变成接口

上面那种model.generate()的写法适合验证连通性,但真实项目里不会这么用。LangChain4j 真正的杀手锏是声明式接口:

interface Assistant { String chat(String userMessage); } Assistant assistant = AiServices.create(Assistant.class, model); String reply = assistant.chat("帮我写一个冒泡排序");

AiServices.create在运行时用动态代理生成了Assistant的实现。你调用chat方法时,框架把参数包装成消息、发给模型、再把返回文本映射回 String。这个模式的好处是:业务代码里看不到任何模型相关的 API,全是普通 Java 接口调用,单元测试时可以轻松替换成 Mock 实现。

我第一次用的时候有个疑问:这样封装会不会丢失灵活性?实测下来不会。因为你可以通过注解在接口方法上叠加系统提示词、记忆、工具等能力,灵活性反而比手写 HTTP 调用更高,而且类型安全。

2.4 连通性验证与常见报错对照

第一次跑不通是常态,我把新手最常遇到的几类问题整理成表,方便你对照排查:

报错现象大概率原因处理方式
401 Unauthorized密钥错误或未生效检查密钥字符串是否有多余空格,确认服务端已激活
404 Not FoundbaseUrl 路径不对确认是否需要带/v1后缀,去掉结尾斜杠
模型不存在modelName 拼写错误对照服务端文档逐字符核对
连接超时网络或地址不可达先用 curl 验证地址连通性
返回乱码编码未指定确认请求头字符集,Java 侧统一用 UTF-8

提示:排查连通性问题时,先用最简单的 curl 命令验证服务本身是否可用,再回到 Java 代码。这样能把"服务问题"和"代码问题"快速隔离开,省下大量时间。

3. 提示词模板、对话记忆与结构化输出的实战拆解

3.1 提示词模板:别把字符串拼接到处散落

真实项目里,提示词往往很长,还带变量。如果到处用字符串拼接,改一个措辞要翻遍整个工程。LangChain4j 提供了PromptTemplate:

PromptTemplate template = PromptTemplate.from( "你是一名{{role}},请用{{tone}}的语气回答:{{question}}" ); Map<String, Object> vars = new HashMap<>(); vars.put("role", "资深Java架构师"); vars.put("tone", "简洁专业"); vars.put("question", "什么是依赖注入"); String prompt = template.apply(vars).text();

{{}}是占位符语法,apply时传入 Map 做替换。这里有个细节:如果某个变量没传,框架会保留原始占位符而不是报错,所以上线前一定要做变量完整性校验,否则模型会收到带{{question}}字样的提示词,输出质量直接崩掉。

我习惯把提示词模板统一放在resources/prompts/目录下,用文件加载的方式管理。这样产品和运营改措辞时不用动 Java 代码,重新加载配置即可。对于提示词频繁迭代的项目,这个习惯能省下大量重新打包部署的时间。

3.2 对话记忆:多轮上下文是怎么维持的

单轮问答很简单,但"多轮追问"才是大模型应用的刚需。模型本身是无状态的,每次请求都是独立的,所谓"记忆"其实是框架帮你把历史消息一起发过去。

LangChain4j 提供MessageWindowChatMemory:

ChatMemory memory = MessageWindowChatMemory.withMaxMessages(10); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .build();

withMaxMessages(10)表示只保留最近 10 条消息。为什么要有上限?因为模型的上下文窗口是有限的,历史消息越多,占用的 token 越多,成本和延迟都会上升,超过窗口还会直接报错。10 是一个经验值,对于日常对话够用;如果是长文档分析场景,需要配合摘要压缩策略。

这里有个容易踩的坑:记忆是按会话隔离的。如果你用同一个 memory 实例服务所有用户,A 用户的对话历史会串到 B 用户那里,这是严重的隐私问题。正确做法是为每个会话(比如每个用户 ID 或每个对话 ID)创建独立的 memory 实例,通常用一个ConcurrentHashMap<String, ChatMemory>来管理。

3.3 结构化输出:让模型返回 Java 对象而不是一段文本

让模型返回一段自然语言,程序很难处理。真实业务里我们想要的是结构化数据。LangChain4j 支持直接把模型输出映射成 Java 对象:

record OrderInfo(String productName, int quantity, String address) {} interface OrderParser { OrderInfo parse(String userInput); } OrderParser parser = AiServices.create(OrderParser.class, model); OrderInfo info = parser.parse("我要买3台显示器,送到科技园区A座");

框架会自动在提示词里加入格式说明,并把模型返回的 JSON 反序列化成OrderInfo。实测下来,用 record 类型比普通 class 更稳,因为字段不可变、构造清晰,反序列化出错率更低。

但要注意:结构化输出的成功率不是 100%。模型偶尔会返回多余的解释文字,导致 JSON 解析失败。生产环境里必须加兜底逻辑——捕获解析异常后重试一次,或者在提示词里强调"只返回 JSON,不要任何其他文字"。我在一个订单解析项目里就遇到过,加了"只返回JSON"的约束后,成功率从 85% 提到了 98% 以上。

3.4 流式输出:改善长回答的等待体验

模型生成一段长文本可能要好几秒,用户盯着空白页面体验很差。流式输出让内容边生成边显示:

interface StreamingAssistant { TokenStream chat(String message); } StreamingAssistant assistant = AiServices.create(StreamingAssistant.class, model); assistant.chat("讲一个关于程序员的笑话") .onNext(token -> System.out.print(token)) .onComplete(response -> System.out.println("\n完成")) .start();

onNext每收到一个片段就回调一次,onComplete在结束时触发。这个模式在 Web 场景里配合 SSE(Server-Sent Events)推送给前端,用户就能看到文字逐字蹦出来的效果。

踩过的坑:流式模式下如果中途发生异常,onError回调一定要处理,否则连接会挂在那里不释放。另外,流式输出的 token 是碎片化的,不要在每个onNext里做数据库写入,那样会产生大量小事务,性能很差。正确做法是先在内存里拼接,onComplete时一次性落库。

4. 工具调用与检索增强:让模型能"动手"和"查资料"

4.1 工具调用:把 Java 方法暴露给模型

模型本身只会生成文本,不能查数据库、不能调接口。工具调用(Function Calling)机制让模型可以"决定"调用你提供的 Java 方法:

class WeatherTools { @Tool("查询指定城市的天气") String getWeather(@P("城市名称") String city) { return weatherService.query(city); } } Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new WeatherTools()) .build();

@Tool注解描述方法用途,@P描述参数含义。当用户问"北京今天天气怎么样"时,模型会判断需要调用getWeather,框架执行方法后把结果回传给模型,模型再组织成自然语言回答。

这里的关键理解是:模型不直接执行你的代码,它只是输出一个"我想调用某个工具"的意图,真正执行的是框架。所以工具方法里的权限校验、参数校验一个都不能少,不能因为"是模型调用的"就放松安全检查。

工具描述写得好不好,直接决定调用准确率。我试过把描述写成"获取天气",模型经常在不需要的时候也调用;改成"当用户明确询问某城市当前天气时调用,其他情况不要调用",误调用率明显下降。描述要写清楚"什么时候用"和"什么时候不用"。

4.2 检索增强:给模型接上你的私有知识

模型的知识有截止日期,也不了解你公司的内部文档。检索增强生成(RAG)的思路是:先把文档切块、向量化、存进向量库,用户提问时先检索出相关片段,再连同问题一起发给模型。

LangChain4j 的EmbeddingStoreIngestor把整个流程封装得很简洁:

EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>(); EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() .embeddingModel(embeddingModel) .embeddingStore(store) .build(); ingestor.ingest(Document.from("你的文档内容"));

InMemoryEmbeddingStore适合验证阶段,重启就丢数据。生产环境要换成持久化的向量库实现。文档切块的大小是个关键参数,默认按固定长度切,但更好的做法是按语义切——比如按段落、按标题层级切,这样每个块的内容更完整,检索质量更高。

我踩过的一个坑:文档里如果有大量表格,直接切块会把表格切得七零八落,检索出来的片段毫无意义。处理办法是先把表格转成自然语言描述再入库,或者对表格单独做结构化处理。这个细节官方文档不会强调,但实际项目里非常影响效果。

4.3 检索质量调优:召回数量与相似度阈值

检索出来的片段不是越多越好。召回太多会引入噪声,还会撑爆上下文窗口;召回太少可能漏掉关键信息。一般从 3 到 5 个片段起步,根据效果调整。

相似度阈值也很重要。低于某个阈值的片段说明和问题关系不大,应该过滤掉。如果不设阈值,用户问一个文档里完全没有的问题时,系统也会硬塞几个不相关的片段给模型,导致模型"一本正经地胡说"。加上阈值后,检索不到相关内容时可以直接回复"知识库中没有相关信息",这比编造答案要诚实得多。

5. 新手最容易踩的五个坑与排查思路

5.1 坑一:把密钥硬编码进代码提交到仓库

这是最危险也最常见的错误。密钥一旦泄露,可能被人盗用产生费用。正确做法是通过环境变量或配置中心注入:

String apiKey = System.getenv("MODEL_API_KEY");

Spring 项目里用@Value("${model.api-key}")从配置文件读取,配置文件本身加入.gitignore。团队协作时,提供一个application-example.yml模板,真实配置各自本地维护。

5.2 坑二:忽略超时和重试,一个慢请求拖垮整个线程池

模型调用是网络请求,延迟波动很大。如果不设超时,一个卡住的请求会一直占着线程。LangChain4j 支持配置超时:

OpenAiChatModel.builder() .timeout(Duration.ofSeconds(30)) // 其他配置 .build();

30 秒是个比较合理的值,长文本生成可以放宽到 60 秒。重试策略上,对于网络抖动导致的失败可以重试,但对于参数错误、密钥错误这类问题重试没有意义,反而浪费配额。建议只对超时和 5xx 错误做有限次重试。

5.3 坑三:token 用量失控导致成本飙升

每次调用都会消耗 token,输入和输出都算。如果提示词写得很长、历史消息不清理、检索片段召回过多,token 用量会快速累积。我见过一个项目因为把整本手册塞进系统提示词,单次调用成本是正常情况的几十倍。

控制手段有几个:系统提示词精简,只保留必要约束;历史消息设上限并定期摘要压缩;检索片段数量和长度都设上限。上线前一定要估算日均调用量和单次 token 消耗,算出月度成本,心里有数。

5.4 坑四:并发场景下共享可变状态

前面提到的 memory 共享问题就是典型。除此之外,如果你自定义了工具类,工具类里的成员变量如果被多个请求共享且会修改,就会出现数据错乱。工具类应该设计成无状态的,所有需要的数据通过参数传入或从线程安全的容器里取。

5.5 坑五:不做降级,模型服务一挂整个功能不可用

模型服务不是 100% 可用的,网络会抖、服务会维护。如果这个功能是核心链路,必须做降级。比如摘要生成失败时,退回到用规则提取关键词;问答失败时,提示用户稍后重试。降级逻辑要在设计阶段就想好,不要等出故障了才临时加。

6. 从 Demo 到生产:工程化落地的几个关键决策

6.1 模型配置外置化与多环境管理

开发、测试、生产用的模型服务地址和模型名称往往不同。把这些配置全部外置到配置文件,通过 Spring 的 profile 机制切换。不要把任何环境相关的值写死在代码里,否则每次换环境都要改代码重新打包。

6.2 可观测性:日志、指标与链路追踪

生产环境必须能回答三个问题:调用了多少次、每次耗时多少、失败率多少。LangChain4j 提供了ChatModelListener接口,可以在请求前后埋点:

OpenAiChatModel.builder() .listeners(List.of(new LoggingChatModelListener())) // 其他配置 .build();

在监听器里记录请求参数(注意脱敏)、响应耗时、token 用量。这些数据接入现有的监控体系后,你才能发现"最近响应变慢了""某个提示词导致失败率上升"这类问题。

6.3 提示词的版本管理与灰度

提示词是影响输出质量的核心变量,应该像代码一样管理。每次修改都要记录版本、修改原因、预期效果。上线新提示词时先小流量灰度,对比新旧版本的输出质量和成本,确认没问题再全量。我见过团队直接在生产环境改提示词,结果输出格式变了导致下游解析全部失败,回滚都来不及。

6.4 测试策略:Mock 模型与回归用例

单元测试里不应该真的调用模型服务,一是慢,二是结果不确定。把ChatLanguageModel接口 Mock 掉,返回固定响应,测试业务逻辑是否正确。同时维护一批回归用例——固定的输入和期望的输出特征,每次改提示词或换模型后跑一遍,确保没有退化。

7. 我在这条路上积累的几点真实体会

LangChain4j 最让我认可的地方,是它没有试图把大模型包装成一个"魔法黑盒",而是用 Java 开发者熟悉的方式——接口、注解、构建器——把能力暴露出来。你依然能控制每一个参数,依然能用调试器打断点,依然能写单元测试。这种"可控感"对于企业级项目来说非常重要。

新手最容易陷入的误区是追求"一步到位",上来就想做复杂的 Agent、多工具编排。我的建议是老老实实从单轮对话开始,跑通了再加记忆,再加工具,再加检索,每一步都验证清楚再往下走。大模型应用的不确定性本来就高,如果多个变量同时引入,出了问题根本不知道是哪一层导致的。

另外一个体会是:提示词工程不是玄学,是可以通过实验优化的工程问题。同一个任务,换一种表述、加一个示例、调整一下约束,效果可能差很多。养成记录实验的习惯,把每次改动和效果对应起来,几轮下来你就能摸清模型的脾气。

最后说一个实际项目里的经验:不要指望模型输出 100% 准确。设计系统时就要假设它会出错,在关键环节加校验、加兜底、加人工确认。把模型当成一个能力很强但偶尔会犯迷糊的助手,而不是一个绝对可靠的函数。这个心态摆正了,很多架构决策就清晰了。

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

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

立即咨询