在Java生态里接大模型,过去给我的第一反应就是麻烦。Python那边有丰富的AI工具,可Java项目里想把这些能力揉进已有的Spring服务,总得自己做HTTP客户端、JSON解析、上下文管理这些杂活。LangChain4j的出现,算是把这条路给修平了。它是一个面向大语言模型应用的Java框架,提供统一模型抽象、对话记忆、结构化输出和函数调用能力,尤其在Spring体系下用起来非常顺手。
这篇稿子不是照搬官方文档,而是我从配置Maven依赖、跑通聊天接口、加记忆、做流式输出,再到集成Function Calling的全过程记录。适合那些已经在写Java、但还没在项目里碰过大模型的开发者;如果你对LLM只有基础概念,跟着操作也能把环境弄起来。下面所有示例我都会以本地模型服务为主来演示,免得卡在接口额度或者Key申请上。
1. 为什么LangChain4j会成为Java侧的LLM开发首选
1.1 生态背景:Python的“脚手架”思路搬到了Java
LangChain在Python生态里火起来之后,很多Java开发者第一反应是“我们也要一个”。但真正照着Python项目玩过一遍就会明白,两者开发习惯差距太大了。Python可以做动态代理、随意改方法签名,Java讲究编译期检查、强类型建模、依赖注入,直接把那套代码翻译过来并不舒服。LangChain4j解决的问题不是“模仿LangChain”,而是把“链式调用、模型切换、上下文管理、工具调用”这些理念用Java的语法重新做了一遍。
这个框架把大模型聊天、图片描述、嵌入向量、文本拆分、Prompt模板、记忆管理、RAG检索全部拆成了独立模块。你只需要引入需要的模块,不会一上来就被一堆依赖压得喘不过气。以我自己的经验,新手从聊天开始最合适:一个核心依赖加一个模型接入依赖,代码就能跑。
1.2 类型安全与“接口即服务”的体验
Java开发者普遍对字符串拼接型代码有心理阴影,而很多大模型应用一开始恰恰是“拼Prompt → 调接口 → 拿文本 → 自己解析”。LangChain4j里有一层叫AiServices的抽象,它允许你定义一个普通的Java接口,框架自动把接口方法翻译成大模型调用。
interface ChatAssistant { String chat(String message); }你不需要关心消息是怎么组装的。调用assistant.chat("你好")时,框架会替你完成“把消息变成HTTP请求、调用模型、拿回文本”的全部过程。再配合ChatMemory,连续对话里该传哪些历史消息也不用自己维护。这种开发体验很接近Spring里的Service接口,Java老手几乎零成本上手。
1.3 模块化依赖,不逼你全家桶
LangChain4j的核心设计是“按需引入”。搞聊天只需要聊天模型模块,做RAG再去加嵌入和向量存储模块。如果以后想从A厂商模型切到B厂商模型,改动集中在构建Model那几行,业务代码基本不动。下面实战里你会在每个示例里看到同一个名字反复出现:ChatLanguageModel。这是所有聊天模型的统一抽象,也是新手最先要记住的接口。
2. 跑通第一个聊天Demo:环境、依赖与最小调用
2.1 Maven依赖怎么加
我先用Maven创建了一个空项目,JDK用的17。LangChain4j对JDK版本要求不算苛刻,8以上能跑,但如果你想用后面那些新特性,建议直接上17或21。核心依赖和本地模型接入依赖分别是langchain4j和langchain4j-ollama。我写文时常用的版本是0.35.0,你下载时可以去项目首页看最新版本,把版本号替换掉就行。
<properties> <langchain4j.version>0.35.0</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>如果你的项目要用某个云厂商模型,对应也有单独的模块名。这里统一用本地模型的好处是:不需要注册账号,不需要申请Key,断网也能玩,适合新手搞明白框架本身的调用链路。
2.2 准备一个能跑的本地模型服务
本地模型方案很多,我用的是Ollama这个工具。安装完启动服务,默认端口是11434。然后拉一个模型,比如Qwen2.5 7B,这一步取决于机器配置,7B量化模型在普通16G内存机器上也能跑,只是速度偏慢。
ollama run qwen2.5:7b看到终端出现输入提示符,说明模型已经就绪。新手第一反应是直接在这里对话,但我们后面要用Java去调用它,所以要先确认这个服务对外暴露了HTTP接口。浏览器访问http://localhost:11434能返回内容,就说明服务正常。
2.3 用Java代码完成首次问答
依赖和模型都准备好之后,写第一段代码。我建议建一个干净的Main类,先跑通最小链路再往下加功能。
import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.ollama.OllamaChatModel; public class QuickStart { public static void main(String[] args) { ChatLanguageModel model = OllamaChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .temperature(0.8) .build(); String answer = model.generate("用不超过50个字解释什么是依赖注入"); System.out.println(answer); } }这段代码的效果是:程序把问题发给本地模型,模型生成答案后原样打印。OllamaChatModel.builder()是LangChain4j里常见的构造方式,所有模型接入几乎都是这个套路:指定地址、模型名、参数,然后build()得到统一接口实例。
2.4 第一次请求背后的调用逻辑
很多人把model.generate想得很神秘,其实它内部就是一次HTTP请求。聊天模型接收到你的文本,返回一个完整字符串。官方为了易用性做了简化,把响应中的文本直接抽出来。真正进入业务后,你会需要更细的响应结构,去拿Token数量、停止原因这些信息,但在入门阶段,先记住一句话:ChatLanguageModel.generate()返回的是模型生成的文本。
temperature是新手最先碰到的模型参数。它控制随机性,0到1之间,调得越低,模型回答越保守和确定;调得越高,回答越发散。做客服系统、数据抽取这类确定性任务,我会降到0.2甚至0;做创作类任务再拉到0.8以上。第一个Demo里用0.8,只是为了让你感受到不同答案的变化。如果本地模型生成很慢,先不要怀疑代码有错,多半是模型参数量大、机器算力有限或者同时有别的程序在占用资源。
3. 从“一问一答”到“连续聊天”:消息历史与流式输出
3.1 消息模型与系统提示词
跑通单轮对话后,新手马上会碰到的第一个坑是:模型记不住上下文。你问“我叫小明”,下一轮问“我叫什么名字”,它答不上来。原因很简单,模型本身无状态,每次generate都是独立请求。想要连续聊天,必须把之前的对话消息一起发给模型。
LangChain4j把对话消息抽象成ChatMessage的子类。常用的有三位:
SystemMessage:系统提示词,用来设定角色和行为规则。UserMessage:用户输入。AiMessage:模型生成的内容。
手动维护消息列表时,推荐用ArrayList。
List<ChatMessage> messages = new ArrayList<>(); messages.add(SystemMessage.from("你是一个简洁的助手")); messages.add(UserMessage.from("我叫小明")); Response<AiMessage> response = model.generate(messages); messages.add(response.content()); messages.add(UserMessage.from("我叫什么名字?")); response = model.generate(messages); System.out.println(response.content().text());注意一个顺序:模型每次回答后,要立刻把这条AiMessage放回列表,下一次请求才能带上。如果漏了这一步,模型仍然看不到自己说过什么。手动维护在小场景里够用,但它有两个问题:一是历史消息无限增长,迟早超过模型上下文限制;二是业务代码里到处穿插消息列表操作,很丑。
3.2 让体系自动管理上下文:ChatMemory
LangChain4j提供了ChatMemory接口,用来解决“谁帮我存历史消息”的问题。最常用的是滑动窗口式实现,叫MessageWindowChatMemory,只保留最近N条消息,超出后自动丢弃旧数据。
import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory; ChatMemory memory = MessageWindowChatMemory.builder() .maxMessages(20) .build();但光有memory还不够,它和模型之间需要一座桥,这座桥就是AiServices。先定义一个接口。
interface ChatAssistant { String chat(String message); }然后通过AiServices把这个接口和模型、记忆绑定。
import dev.langchain4j.service.AiServices; ChatAssistant assistant = AiServices.builder(ChatAssistant.class) .chatLanguageModel(model) .chatMemory(memory) .build(); String first = assistant.chat("我叫小明"); System.out.println(first); String second = assistant.chat("我叫什么名字?"); System.out.println(second);这段代码的运行效果是:第一次提问时,框架自动创建一条UserMessage;拿到模型回答后,自动把用户消息和AI消息写入memory。第二次提问时,它会从memory里读出最近的消息一起发给模型。对于新手来说,这是最容易理解的连续对话写法。你只管调接口方法,不用关心消息列表怎么组装。
3.3 流式输出:让等待不再干瞪眼
同步调用model.generate有个体验问题:模型要全部生成完,一次性返回。一个长回答可能要等十几秒,中间界面完全没有反应。做聊天机器人、客服助手时这很难接受。LangChain4j对流式输出支持得很好,做法是换一个模型类型,并把StreamingResponseHandler回调实现出来。
import dev.langchain4j.model.StreamingResponseHandler; import dev.langchain4j.model.output.Response; import dev.langchain4j.model.chat.StreamingChatLanguageModel; import dev.langchain4j.model.ollama.OllamaStreamingChatModel; StreamingChatLanguageModel streamModel = OllamaStreamingChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .build(); streamModel.generate("讲一个程序员下班回家打不开门的段子", new StreamingResponseHandler<AiMessage>() { @Override public void onNext(String token) { System.out.print(token); } @Override public void onComplete(Response<AiMessage> response) { System.out.println(); System.out.println("生成完成"); } @Override public void onError(Throwable error) { error.printStackTrace(); } });onNext是核心,模型每产出一小段文本就会触发一次,打印出来就像打字机效果。onComplete是所有内容生成完之后触发,适合做日志记录、把完整结果落库之类的收尾工作。onError必须实现,网络抖动、模型超时都会走到这里。新手容易犯的错是只处理onNext,等线上出现异常才发现整个线程静默挂掉,排查半天。
3.4 模型参数配置:不只是temperature
除了temperature,还有几个参数值得一开始就搞清楚。
ChatLanguageModel model = OllamaChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .temperature(0.3) .timeout(Duration.ofSeconds(60)) .maxRetries(2) .logRequests(true) .logResponses(true) .build();timeout:单次请求的超时时间,本地模型推理慢,建议给足。maxRetries:请求失败后的自动重试次数,默认值通常很小,生产环境可以根据模型厂商限制调整。logRequests和logResponses:把请求体和响应体打印到日志里。新手强烈建议打开,尤其当模型回答不符合预期时,看日志比猜快得多。- 最大输出Token数:不同模型实现里方法名不同,一般是
maxTokens,限制单次回答长度。
另外,同一套参数在不同模型上的支持度不一样,我见过有的本地模型完全忽略temperature,怎么调输出都一样。遇到这类问题先查模型接入模块的文档,不要想当然。
4. 进阶一步:结构化输出与Function Calling
4.1 直接让模型返回JSON的痛点
聊天跑通以后,你很快会想把它嵌入业务,比如从用户留言里提取订单信息、把客服对话分类成“投诉/咨询/表扬”。第一个想法往往是“我让模型返回JSON,然后用Jackson解析”。想法没错,但实际写起来很痛苦:模型会好心加一句“好的,这是你要的JSON”,或者字段顺序变化,或者字段值带单位。任何一点变化都可能让解析崩溃。
LangChain4j对此有一个更稳的思路:结构化输出。它利用模型对JSON Schema的理解,把大模型输出直接映射成Java对象。你只需要定义一个POJO和一个接口,剩下交给框架。
4.2 用AiServices实现结构化提取
假设要从一段聊天记录里提取购买信息,我定义这样一个POJO:
class PurchaseInfo { String customerName; String product; BigDecimal amount; String purchaseDate; // 建议补上getter/setter和toString }再定义一个带@SystemMessage的接口:
import dev.langchain4j.service.SystemMessage; interface PurchaseExtractor { @SystemMessage("从对话文本中提取购买信息,字段缺失时填空字符串或null") PurchaseInfo extract(String text); }然后构建并调用:
PurchaseExtractor extractor = AiServices.builder(PurchaseExtractor.class) .chatLanguageModel(model) .build(); PurchaseInfo info = extractor.extract("上周李雷在店里花128元买了一双运动鞋"); System.out.println(info.customerName + " - " + info.product + " - " + info.amount);这个工作机制不是魔法。框架会把“这个接口要返回什么类型”“这个类型有哪些字段”翻译成Prompt发送给模型,要求模型只返回特定结构的JSON,再用Jackson反序列化成对象。和手写Prompt比,最大优势是生成结构和字段类型稳定,你获得的不是碰运气的字符串,而是真正编译期可见的Java对象。如果模型不支持结构化输出,代码可能直接报错或返回null,这也是判断模型能力的好机会。
4.3 Function Calling:让模型调用本地方法
大模型的训练数据有截止时间,也不可能知道你系统的实时库存、用户数据库或内部API。想让模型具备“查数据库”的能力,最正式的做法不是把数据写进Prompt,而是函数调用。LangChain4j里用@Tool注解标记一个普通方法,模型在需要时会“请求”执行这个方法。
先写一个工具类:
import dev.langchain4j.agent.tool.Tool; import java.util.Random; class InventoryTools { @Tool("查询某个商品的实时库存,返回数量") public int queryStock(String productName) { return new Random().nextInt(100); } }然后把这个工具挂到AiServices上:
interface StoreAssistant { String chat(String message); } StoreAssistant assistant = AiServices.builder(StoreAssistant.class) .chatLanguageModel(model) .tools(new InventoryTools()) .build(); String answer = assistant.chat("我要买三双运动鞋,现在有货吗?"); System.out.println(answer);模型看到问题后,如果判断需要查询库存,会生成一个函数调用请求。框架接收请求后执行queryStock方法,把结果再发回给模型,模型根据这个结果组织成自然语言答复。通过日志,你能看到一次用户问题竟然对应两到三次内部调用。新手容易犯的错是给工具方法起名太随意、描述不清楚。比如方法名a()和描述“do something”,模型根本不知道该在什么时候调用它。方法要public,描述里写清参数含义和返回值含义,Java泛型、复杂对象最好别出现在工具签名里,老老实实用基础类型和字符串。
4.4 与Spring Boot整合时的补充
如果你已经用Spring Boot管理项目,LangChain4j有对应的starter模块,它能帮你把模型对象自动注册成Bean。在配置类里写一个@Bean方法,返回ChatLanguageModel,其他代码直接注入使用即可。
@Configuration public class LLMConfig { @Bean public ChatLanguageModel chatLanguageModel() { return OllamaChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .temperature(0.3) .build(); } }然后在业务类里注入:
@Service public class ChatService { private final ChatAssistant assistant; public ChatService(ChatLanguageModel model) { this.assistant = AiServices.builder(ChatAssistant.class) .chatLanguageModel(model) .build(); } public String talk(String message) { return assistant.chat(message); } }这种写法的好处是模型实例全局只有一个,不会每次请求都新建连接。Spring环境下,还要注意依赖扫描范围,AiServices接口不要放在springBoot无法扫描到的地方,否则不会被自动处理。
5. 新手避坑指南:常见问题与排查思路
5.1 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 启动报NoSuchMethodError或类冲突 | langchain4j核心与模型模块版本不一致 | 把所有相关依赖统一到一个版本,不要混用 |
| Connection refused | 本地模型服务没启动,或baseUrl端口写错 | 浏览器访问对应地址确认服务状态 |
| 请求正常但回答超时 | 模型参数过大、机器负载高、timeout太短 | 换更小模型或延长timeout,先关闭其他高CPU程序 |
| 连续对话答非所问 | 没有使用ChatMemory | 改用MessageWindowChatMemory和AiServices |
| 流式输出没有反应 | 忘了实现onError,回调线程看不到异常 | 加上错误日志,用logRequests观察真实请求 |
| 返回JSON但解析失败 | 模型输出带解释语气 | 用AiServices结构化输出代替纯字符串Prompt |
| Function Calling不触发 | 模型不支持工具调用,或@Tool描述太含糊 | 换支持工具调用的模型,补全方法和参数描述 |
| 中文乱码 | 项目文件编码不是UTF-8 | pom里设置project.build.sourceEncoding=UTF-8 |
| 日志全是请求体,性能变差 | logRequests/logResponses在生产环境开着 | 生产关闭,调试打开,切环境时检查配置 |
5.2 我踩过的坑和习惯
第一,先本地调试,再上云端。本地模型慢,但配置简单、不花钱,适合验证逻辑。把业务链路跑通了,再换成高并发生产模型会省很多钱和精力。
第二,不要把关键逻辑押在大模型的自由发挥上。比如金额计算、日期判断,该用代码算的就用代码算。需要结构化的数据就用结构化输出,需要动态实时数据就用Function Calling,不要把信息塞进Prompt里全靠模型“理解”。
第三,日志是你最好的老师。我几乎每次遇到诡异问题,第一步都是打开日志,看请求里到底发了什么。LangChain4j的logRequests(true)能输出完整请求体,字段是不是对、历史消息有没有带上,一眼就能看出来。
第四,不要盲目追求最新版。这个框架迭代很快,有些版本重构了包路径和方法签名。我在一次升级后就遇到过某类从dev.langchain4j.service挪到了别的位置,花了不少时间才解决。团队项目里最好固定版本,评估过再升。
第五,异步化要趁早设计。大模型接口天然就是慢接口,几十秒都可能。在Spring里如果用同步调用占住连接池,流量稍微大一点就会雪崩。建议从第一个Demo开始就习惯CompletableFuture或响应式写法,别等上线再改。
最后分享一个小经验:如果本地模型回答质量太差,先别急着调Prompt,先确认模型的参数版本。我自己遇到过同一句话在不同量化级别下水平差距很大,换一个大一点或新一点的模型,往往比疯狂堆Prompt更有效。LangChain4j把模型抽象得很好,切换模型只是换builder参数,这一点很友好,值得好好利用。