我主力语言是Java,平时写业务系统、中间件,去年开始琢磨怎么把大模型塞进手里这些老项目里。试了一圈Python生态的LangChain,总觉得绕个弯还得另起一套服务。后来发现 LangChain4j——一个为Java量身定做的LLM编排框架,真有种“总算有正经工具了”的感觉。这篇教程不讲虚的,我会从零搭一个可运行的LangChain4j项目,带你跑通调用、对话记忆、结构化输出和Spring Boot整合,最后把常见的坑都摊开说清楚。
如果你是Java工程师,想给自己项目加AI能力,或者正在做技术选型,这篇文章可以直接当作业抄。我用的本地模型Ollama做演示,不依赖外部API Key,跑起来零成本。当然,框架本身也支持市面上主流大模型服务,换起来就是改个依赖的事儿。
1. 为什么Java开发者要关注LangChain4j
1.1 从Python生态到Java的主流选择
过去两年大模型应用开发几乎被Python包圆了,Java开发者想跟风,最难受的不是不会写Prompt,而是那些链式调用、向量检索、Agent编排的库全是Python版。你当然可以用Python写个独立服务,再把接口暴露给Java后端,但多了一个进程、多了一套部署、多了一堆链路排查问题。LangChain4j就是冲着这个痛点来的,它把大模型调用、记忆、工具调用、RAG检索这些能力抽象成Java接口,你可以在Spring Boot里一边写业务,一边让模型当你的“协作者”。
它之所以在Java圈里口碑不错,一是API设计贴合Java开发者的习惯,Builder模式、类型安全、注解声明工具,很眼熟;二是模块化做得细,核心包只有少量依赖,其他模型适配器按需引入。对比过Spring AI和LangChain4j之后,我个人更偏向后者,文档和Issue活跃度更高,代码风格也更像好维护的Java库。
1.2 LangChain4j核心组件一览
把LangChain4j拆开看,核心其实就这几块:
- ChatLanguageModel:统一的大模型对话接口。不管底层是Ollama、OpenAI还是Azure,构建好对象之后调用方法就行。
- ChatMemory:对话记忆管理。默认支持窗口滑动、缓存持久化,解决“模型不记得上下文”的问题。
- AiServices:面向任务的门面。它能把普通Java方法包装成AI服务,加上@Tool注解还能让模型调用你的Java方法。
- ContentRetriever:检索增强(RAG)的入口。配合嵌入模型做知识库问答。
刚开始接触别急着把所有概念都搞明白,先跑通“模型生成字符串”这一步,后面的记忆和工具调用都是一层一层叠加的。
2. 快速搭建第一个LangChain4j项目
2.1 环境准备与依赖引入
在动手之前,先把基础环境确认掉:
- JDK 17或更高版本,我用的JDK 21;
- Maven 3.8+,如果你用Gradle也一样;
- 本地跑Ollama,并提前拉一个模型,比如
ollama pull qwen2.5:7b。
新建一个空Maven项目,主类随便写个Main。引入两个关键依赖:
<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-ollama</artifactId> <version>0.35.0</version> </dependency>注意版本号尽量用Maven Central上最新稳定版。我这里写0.35.0只是当时我用的版本,框架迭代很快,API偶尔会有细微差异,但你照着官方文档的案例调一下就行,大方向不变。
如果你要和Spring Boot集成,还需要引入langchain4j-spring-boot-starter,这个我放到后面单独讲。
2.2 写一段可运行的LLM调用代码
依赖引入后,直接开写。核心动作就是构建一个ChatLanguageModel,然后调用generate方法。
import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.ollama.OllamaChatModel; public class Main { 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.generate("用一句话说明LangChain4j是什么"); System.out.println(answer); } }运行前确认Ollama服务已启动,命令是ollama serve,然后在另一个终端里拉模型。第一次跑会有一点模型加载时间,之后本地调用基本几百毫秒到几秒不等。
这段代码其实就是LangChain4j的“Hello World”。OllamaChatModel是面向Ollama的实现,它会把你的请求转换成Ollama的API格式,把响应解析回Java对象。如果你换用别的模型服务,比如OpenAI,只要改成OpenAiChatModel.builder()...,连model.generate的调用方式都不变。这就是统一抽象的好处。
2.3 关键参数:模型、温度、超时
新手最容易忽略的是参数调优。temperature控制随机性:做代码生成和JSON抽取时我习惯调到0.1~0.3,追求稳定;做头脑风暴就调到0.8以上,让模型更大胆。maxRetries用于网络抖动时重试,一般设2;timeout默认为60秒,但如果跑大模型长文本生成,建议调到120秒,否则容易超时中断。
还有modelName这个参数,别看它不起眼,踩坑率极高。你本地Ollama拉了什么模型,这里就必须写什么名字,比如qwen2.5:7b少写了:7b,或者大小写不一致,都会报模型不存在。好消息是LangChain4j的日志会直接告诉你Ollama返回的错误信息,照着改就行。
3. 实战:多轮对话与记忆管理
3.1 会话记忆原理
先想一想,为什么连续对话经常“失忆”?因为底层模型接口是无状态的,每次请求只根据你传入的消息列表生成回复。比如你第一次问“我的名字是张三”,第二次如果只发“我叫什么”,模型没有任何上下文,自然答不上来。
LangChain4j的做法是维护一个ChatMemory对象,你可以把前几轮消息存进去,每次调用前把历史消息和当前用户问题一起塞给模型。这样模型能看到前面的对话,才能正确回答。这个原理理解透了,后面看源码都不费劲。
3.2 使用ChatMemory与MessageWindow
实现记忆有两种路径:一种是手动管理消息列表,另一种是用框架自带的MessageWindowChatMemory。我建议新手直接用后者,省心。
import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.data.message.UserMessage; ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10); UserMessage first = UserMessage.from("我的名字是李明"); chatMemory.add(first); UserMessage second = UserMessage.from("我叫什么名字?"); chatMemory.add(second); System.out.println(chatMemory.messages());你可能会问,messages()出来之后怎么让模型结合历史回答?用AiServices最方便。把ChatLanguageModel和ChatMemory交给一个服务代理,你只管调用自己的业务方法。
import dev.langchain4j.service.AiServices; interface Assistant { String chat(String userMessage); } Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build(); String answer = assistant.chat("我叫李明"); assistant.chat("我喜欢Java"); String recall = assistant.chat("我是谁,我喜欢什么?"); System.out.println(recall);这里有个很巧妙的点:AiServices会自动把chatMemory的历史消息和每次传入的用户消息组合起来,你自己不用拼字符串。调用三次接口,第三次时模型已经能看到前两轮的上下文。
3.3 流式输出实现
用户在和AI对话时最不想看到“转圈圈等待半天”。流式输出能把模型一个字一个字吐出来的过程实时展示在界面上。LangChain4j用StreamingChatLanguageModel接口做这件事。
import dev.langchain4j.model.ollama.OllamaStreamingChatModel; StreamingChatLanguageModel streamingModel = OllamaStreamingChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .build(); streamingModel.generate("讲个程序员冷笑话", new StreamingResponseHandler<>() { @Override public void onNext(String token) { System.out.print(token); } @Override public void onComplete(Response<AiMessage> response) { System.out.println("\n[完成]"); } @Override public void onError(Throwable error) { error.printStackTrace(); } });这里要说一下:流式回调的onNext会在每个token生成后立即触发,SSE推送通常也是这么实现的。如果你做Web应用,可以把onNext里的内容通过WebSocket或者SSE转发到前端,体验和ChatGPT几乎一样。
3.4 记忆持久化方案
默认的MessageWindowChatMemory把记忆放在内存里,服务重启就丢了。生产环境需要持久化,LangChain4j提供了ChatMemoryStore接口,你可以在save和delete方法里实现写入数据库。
ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(20) .chatMemoryStore(new MyChatMemoryStore()) .build();MyChatMemoryStore需要实现:
class MyChatMemoryStore implements ChatMemoryStore { @Override public List<ChatMessage> getMessages(Object memoryId) { // 从Redis或MySQL读 } @Override public void updateMessages(Object memoryId, List<ChatMessage> messages) { // 覆盖保存 } @Override public void deleteMessages(Object memoryId) { // 删除会话 } }我实践下来有个心得:不要无限存历史,模型上下文窗口是有限的。如果你搞一个超长对话,反而会因为截断丢失关键信息。控制好maxMessages,或者用摘要压缩历史,才是稳定的做法。
4. 进阶:结构化输出与函数调用
4.1 让模型返回JSON:使用Response Extractor
很多时候我们要的不是一段散文,而是能直接落到数据结构里的结果。比如从商品评论里抽取“满意度、关键词、是否推荐”。LangChain4j支持把大模型输出直接映射成POJO。
假设我们想抽取用户评论里的属性:
public class ReviewSentiment { private String sentiment; private List<String> keywords; private Boolean recommended; }然后这样调用:
import dev.langchain4j.model.output.structured.StructuredOutputException; String comment = "物流很快,但包装有点简陋,整体满意,推荐购买"; ReviewSentiment result = model.generate(comment, ReviewSentiment.class); System.out.println(result.sentiment);这里有个坑:让模型输出结构化JSON,依赖模型的JSON模式能力。Ollama本地模型通常支持,但要确保模型是支持JSON格式的版本。如果模型乱输出,可以启用withStructuredOutputs(true)这类开关,或者在Prompt里明确指定格式。
另外,字段命名建议直接用英文,类的属性名最好与Prompt中描述一致。Java Bean没有setter也能解析,但为了保险我还是用record类型,真省事。
public record ReviewSentiment(String sentiment, List<String> keywords, Boolean recommended) {}4.2 函数调用/工具调用机制
函数调用是Agent的基础。模型本身不会算算术、查数据库,但它可以“决定”调用你提供的函数,再把函数结果融入后续回答。
用LangChain4j定义工具很简单,写一个普通方法加上@Tool注解:
import dev.langchain4j.agent.tool.Tool; public class Calculator { @Tool("计算两个整数的和") public int add(int a, int b) { return a + b; } }然后把它注册到AiServices:
Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new Calculator()) .build(); String result = assistant.chat("请计算 12345 加 54321 等于多少"); System.out.println(result);你可以打开日志观察,模型会先输出一个工具调用请求,框架自动执行你的add方法,再把结果返回给模型生成最终回复。这个能力很上头,相当于让Java方法变成模型的手脚。做查询订单状态、库存校验、告警处理,都是这个套路。
4.3 与Spring Boot整合
业务系统几乎都是Spring Boot,LangChain4j也做了官方starter。官方starter的好处是:你不光能注入现成的Bean,还能通过配置文件直接改模型参数。
引入依赖后再application.yml里配置:
langchain4j: ollama: chat-model: base-url: http://localhost:11434 model-name: qwen2.5:7b temperature: 0.6然后在配置类里声明:
@Configuration public class LLMConfig { @Bean ChatLanguageModel chatLanguageModel() { return ChatLanguageModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .temperature(0.6) .build(); } }更省事的是直接用@Autowired注入官方starter自动配置的模型Bean。Spring Boot的好处就是干完这些杂活之后,你的业务代码能非常清爽,Controller里只管接收参数、调服务、返回结果,模型细节全被隔离在底层。
我的实际建议是:项目初期可以把模型配置放在配置文件里,方便切环境。真到了生产环境,模型实例建议用单例缓存,避免每次请求都重建连接,消耗不必要的资源。
5. 常见问题与排查
5.1 环境依赖冲突
Java项目最烦的就是依赖冲突,LangChain4j又依赖了okhttp、gson这类常见库。如果你在Spring Boot里遇到NoSuchMethodError,多半是版本冲突。
我的排查步骤是:先用mvn dependency:tree -Dincludes=dev.langchain4j看依赖树,再执行mvn dependency:tree -Dincludes=com.squareup.okhttp3看okhttp版本。如果发现版本不理想,在pom里显式声明需要的版本。另一个提醒是,LangChain4j不同小版本之间API命名变化挺多,如果你照着文章里的代码报错,先看看你实际引入的版本和文档是否一致,优先参考对应版本的JavaDoc。
5.2 网络超时与API错误
本地Ollama最常见的报错是ConnectException: Connection refused。这时候别慌,先检查三个点:Ollama服务是否真的启动,ollama serve别关;baseUrl端口是不是11434;有没有设过代理。如果你在公司网络环境,还要看是不是代理拦截了localhost请求。
远程模型服务的报错就更典型了,比如401表示密钥无效,429表示限流。我处理429的经验是:先确认并发量,再调大maxRetries,同时用指数退避策略。如果你是单线程测试,基本不会触发限流;但一上生产,并发一多,就一定要加熔断降级,否则模型服务一抖动,整个接口全挂。
5.3 中文乱码与模型协议
有些模型在结构化输出时,中文注释或字段名会乱码。我遇到过的原因多半是代码文件编码不是UTF-8,尤其Windows下默认GBK。建议在pom里设置project.build.sourceEncoding为UTF-8,并且IDE项目编码也改一下。
模型协议相关的问题比较隐蔽。比如用一个本来不支持函数调用的模型去做工具调用,模型就会一直输出纯文本,框架怎么解析都不对。如果你用Ollama,建议选支持tools和json参数的模型,比如qwen2.5、llama3.1。本地模型版本太老,换新模型大概率就解决了。
5.4 排查工具与学习路径
我调试AI应用时最常用的三板斧:
- 开启LangChain4j日志,看请求和响应完整内容;
- 直接用Ollama自带的命令行先测一次,排除模型本身问题;
- 写最小复现代码,把框架一层层剥掉,定位是模型问题还是封装问题。
日志调整方式很简单,在logback或log4j2配置里把dev.langchain4j的级别设为DEBUG。这样你能看到发送给模型的完整Prompt和返回来的Raw文本。很多新手疑惑“为什么模型答非所问”,其实看日志就知道是不是历史消息拼错了。
学习路径嘛,我的建议是先跑通本教程的基础代码,然后去GitHub上把官方示例仓库里的demo都读一遍,尤其是rag和agents这两个目录。别急着上复杂框架,把ChatLanguageModel、ChatMemory、AiServices这几个核心类的用法吃透,后面看任何LangChain4j项目都不会慌。
我个人用下来的体会是:LangChain4j还没有Python生态那么成熟,但Java领域的LLM应用落地,它已经是实用度最高的选项。如果你的目标是快速把大模型接进业务系统,顺着我上面几条路走,半天时间就能跑出一个不错的雏形。后面再往深挖,无非是RAG、Agent、向量数据库这些方向——工具变了,但工程化的思维是相通的。