Java 程序员的 AI 进化论 | LangChain4j 入门,Java 开发者的 AI 应用框架
上个月接了个内部需求------给我们的运维平台加个 AI 助手,能查告警、能问系统状态、能记住上下文。我用 RestTemplate 硬怼了一版 OpenAI API,写了 300 多行胶水代码,光是管理对话历史和工具调用的逻辑就搞了两天。后来同事推荐了 LangChain4j,试了一下,同样功能 60 行代码搞定。
说实话,Java 生态在 AI 方面确实比 Python 慢半拍,但 LangChain4j 这个框架真的补上了短板。今天把我这两周用的经验整理一下,从核心概念到 Spring Boot 集成,再到对话记忆和工具调用,一条龙走完。
一、为什么需要 LangChain4j
先说个直接的对比。不用框架的时候,你调大模型大概是这样的:
java
// 裸调 API 的典型代码:拼 JSON、管 token、手动维护历史
String requestBody = """
{
"model": "gpt-4o-mini",
"messages": [
{"role": "system", "content": "你是运维助手"},
{"role": "user", "content": "查一下 CPU 使用率"}
]
}
""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.openai.com/v1/chat/completions"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
// 还没完------你得手动解析返回、拼接历史消息、处理 function calling...
这段代码有几个硬伤:对话历史要自己存在内存或 Redis 里、工具调用(function calling)的 JSON schema 要手拼、流式响应要自己 parse SSE、不同模型 provider 的 API 格式还不一样。
LangChain4j 干的事就是把这一层全封装了,你只需要定义一个 Java 接口,剩下的交给框架。下面这张表是我这两周的真实体感对比:
| 对比维度 | 裸调 API | LangChain4j |
|---|---|---|
| 对话历史管理 | 手动拼接 messages 数组 | ChatMemory 自动维护 |
| 工具调用 | 手写 JSON schema + 回调 | @Tool 注解 + 方法签名自动推断 |
| 流式响应 | 自己 parse SSE 数据流 | 一行 StreamingChatLanguageModel 搞定 |
| 换模型 provider | 改 URL + 请求体结构 | 换一个依赖包 + 配置 |
| 代码量 | 300+ 行胶水 | 60 行业务代码 |
差了 5 倍代码量,而且裸调那版我改了三次 bug 才跑通工具调用,LangChain4j 这版一遍过。
二、核心概念速览
LangChain4j 的核心就四个东西,搞明白这四个,剩下的都是组合。
| 概念 | 一句话解释 | 类比 |
|---|---|---|
| ChatLanguageModel | 封装大模型 API 调用 | 相当于 JdbcTemplate |
| ChatMemory | 管理对话历史窗口 | 相当于 Session |
| AI Service | 声明式接口,像调本地方法一样调 AI | 相当于 MyBatis Mapper |
| Tool | 让 AI 能调用你的 Java 方法 | 相当于注册一个 RPC 回调 |
AI Service 是核心中的核心。你定义一个 Java 接口,框架在运行时用动态代理生成实现类,把方法参数自动变成 prompt,把 AI 返回自动变成方法返回值。跟写 MyBatis Mapper 一样丝滑。
三、Spring Boot 集成实战
3.1 依赖配置
直接用 Spring Boot starter,不用手动 new 对象:
| 依赖 | groupId | artifactId | 版本 | 作用 |
|---|---|---|---|---|
| LangChain4j 核心 | dev.langchain4j |
langchain4j-spring-boot-starter |
0.36.2 | AI Service、ChatMemory 核心能力 |
| OpenAI 接入 | dev.langchain4j |
langchain4j-open-ai-spring-boot-starter |
0.36.2 | OpenAI 模型 provider |
| Spring Boot Web | org.springframework.boot |
spring-boot-starter-web |
3.2.x | Web 框架 |
3.2 配置文件
properties
# application.properties
langchain4j.open-ai.chat-model.api-key=${OPENAI_API_KEY}
langchain4j.open-ai.chat-model.model-name=gpt-4o-mini
langchain4j.open-ai.chat-model.temperature=0.7
langchain4j.open-ai.chat-model.timeout=PT30S
那个 timeout 用的是 ISO 8601 持续时间格式,PT30S 就是 30 秒。我刚写的时候填了个 30000,框架直接报格式错误,查了文档才知道。
3.3 定义 AI Service 接口
这是 LangChain4j 最爽的地方------你写个接口,不用写实现:
java
package com.example.assistant;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
@AiService
public interface OpsAssistant {
@SystemMessage("你是运维平台助手,回答简洁准确,不超过 200 字")
@UserMessage("{{query}}")
String chat(String query);
}
然后在 Controller 里直接注入调用:
java
package com.example.assistant;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/assistant")
public class AssistantController {
private final OpsAssistant assistant;
public AssistantController(OpsAssistant assistant) {
this.assistant = assistant;
}
@PostMapping("/chat")
public String chat(@RequestBody ChatRequest request) {
return assistant.chat(request.getQuery());
}
}
注意那个 @AiService 注解,它会让 Spring 自动扫描并创建代理实现类。你不用写 @Bean,不用手动配置 AiServices.builder(),零配置接入。
3.4 流程图
下面是 LangChain4j 处理一次请求的完整流程:

从用户请求进来,到 AI Service 代理拦截、拼接 SystemMessage 和 ChatMemory 历史、调用模型、执行工具回调、返回结果,整条链路框架全包了。
四、对话记忆机制
裸调 API 最烦的就是对话历史------你得自己存 messages 数组,每次请求手动带上前几轮的对话,token 超了还得自己截断。LangChain4j 的 ChatMemory 把这些都封装了。
4.1 两种记忆策略
| 策略 | 类名 | 特点 | 适用场景 |
|---|---|---|---|
| 固定窗口 | MessageWindowChatMemory |
保留最近 N 条消息,超出自动丢弃最早的 | 简单对话、无需长期记忆 |
| Token 窗口 | TokenWindowChatMemory |
按 token 数截断,更精确控制成本 | 长对话、对 token 敏感 |
我一开始用的是 MessageWindowChatMemory,设了 10 条消息。结果在多轮对话里,用户问第 8 轮的时候 AI 把第 1 轮的内容忘了,回答开始"断片"。后来换成 TokenWindowChatMemory,设 2000 token,效果稳定多了。
4.2 配置记忆
在 Spring Boot 里通过 @AiService 配置记忆窗口:
java
package com.example.assistant;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AssistantConfig {
@Bean
public MessageWindowChatMemory chatMemory() {
// 保留最近 20 条消息,足够覆盖大部分多轮对话场景
return MessageWindowChatMemory.withMaxMessages(20);
}
}
如果你的系统需要多用户隔离对话(比如每个用户有自己的对话历史),用 @MemoryId 注解:
java
@AiService
public interface OpsAssistant {
@SystemMessage("你是运维平台助手")
@UserMessage("{{query}}")
String chat(@MemoryId String userId, String query);
}
调用时传入用户 ID,框架会自动按 ID 维护独立的对话历史。这个设计很聪明,不用你自己管 Map。
4.3 踩坑:记忆不会自动持久化
这是第一个坑。ChatMemory 默认是纯内存的,服务重启就没了。如果你的场景需要跨会话记忆,得自己实现 ChatMemoryStore 接口,接到 Redis 或数据库里:
java
package com.example.assistant;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
@Component
public class RedisChatMemoryStore implements ChatMemoryStore {
private final StringRedisTemplate redis;
private final ObjectMapper mapper = new ObjectMapper();
private static final String KEY_PREFIX = "chat:memory:";
public RedisChatMemoryStore(StringRedisTemplate redis) {
this.redis = redis;
}
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String json = redis.opsForValue().get(KEY_PREFIX + memoryId);
if (json == null) return List.of();
try {
return ChatMessageDeserializer.messagesFromJson(json);
} catch (Exception e) {
return List.of();
}
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
try {
String json = mapper.writeValueAsString(messages);
redis.opsForValue().set(KEY_PREFIX + memoryId, json);
} catch (Exception e) {
throw new RuntimeException("保存对话记忆失败", e);
}
}
@Override
public void deleteMessages(Object memoryId) {
redis.delete(KEY_PREFIX + memoryId);
}
}
那段 ChatMessageDeserializer 是 LangChain4j 自带的工具类,别自己手写 JSON 解析------我一开始自己拼的,message type 枚举没处理好,反序列化直接炸了。
五、工具调用
工具调用是让 AI 从"只会聊天"变成"能干活"的关键。LangChain4j 用 @Tool 注解把 Java 方法注册成 AI 可调用的函数,框架自动根据方法签名和注释生成 JSON schema。
5.1 定义工具
java
package com.example.assistant;
import dev.langchain4j.agent.tool.Tool;
import org.springframework.stereotype.Component;
@Component
public class OpsTools {
private final MetricService metricService;
private final AlertService alertService;
public OpsTools(MetricService metricService, AlertService alertService) {
this.metricService = metricService;
this.alertService = alertService;
}
@Tool("查询指定服务器的 CPU 使用率,返回百分比数值")
public double getCpuUsage(String serverIp) {
return metricService.getCpuUsage(serverIp);
}
@Tool("查询最近 N 分钟的活跃告警列表,返回告警摘要")
public List<String> getActiveAlerts(int minutes) {
return alertService.getActiveAlerts(minutes);
}
@Tool("对指定服务器执行重启操作,需要确认 IP 地址正确")
public String restartServer(String serverIp) {
return alertService.restartServer(serverIp);
}
}
那个 @Tool 里的描述是给 AI 看的,框架会把它翻译成 function calling 的 description。描述写得越清楚,AI 调用得越准 。我一开始 getCpuUsage 的描述只写了"查 CPU",结果 AI 把服务器名当 IP 传进来了,改了描述后就好了。
5.2 绑定工具到 AI Service
java
package com.example.assistant;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
@AiService
public interface OpsAssistant {
@SystemMessage("""
你是运维平台助手,可以查询服务器状态和告警信息。
调用工具时确保参数类型正确,服务器 IP 格式为 x.x.x.x
""")
@UserMessage("{{query}}")
String chat(String query);
}
Spring Boot 的 @AiService 会自动把 @Component 标注的 @Tool 方法注入进来,不用手动配置 AiServices.builder().tools(...)。
5.3 踩坑:工具返回值类型要简单
第二个坑在这里。我有个工具方法返回的是一个自定义的 ServerMetricDTO 对象,结果 AI 调用后直接报错。原因是 LangChain4j 把工具返回值序列化成字符串塞给 AI,复杂的自定义对象序列化出来 AI 根本看不懂。
解决方案:工具方法返回简单类型(String、double、int、List),或者手动把结果格式化成 AI 容易理解的文本:
java
@Tool("查询指定服务器的完整状态信息")
public String getServerStatus(String serverIp) {
ServerMetricDTO metric = metricService.getMetrics(serverIp);
// 手动格式化成 AI 友好的文本,别让框架自己序列化
return String.format("服务器 %s - CPU: %.1f%%, 内存: %.1f%%, 磁盘: %.1f%%",
serverIp, metric.getCpu(), metric.getMemory(), metric.getDisk());
}
改完之后 AI 对数据的理解准确率明显上来了。
5.4 工具调用里的异常处理
第三个坑是异常处理。我的 restartServer 工具一开始没有 try-catch,结果某个生产环境的服务器重启失败了,异常冒泡到 AI 调用层,AI 直接返回了一串看不懂的 stack trace 给用户。
正确做法是工具内部捕获异常,把错误信息也格式化成 AI 能理解的文本:
java
@Tool("对指定服务器执行重启操作,返回执行结果摘要")
public String restartServer(String serverIp) {
try {
alertService.restartServer(serverIp);
return String.format("服务器 %s 重启成功", serverIp);
} catch (Exception e) {
// 把异常信息转成 AI 友好的描述,别让 stack trace 冒上去
return String.format("重启失败:%s,请确认 IP 是否正确或服务是否可达", e.getMessage());
}
}
AI 拿到错误信息后会主动告知用户"重启失败请检查 IP",而不是傻乎乎地把 stack trace 念出来。这种细节在生产环境特别重要,AI 直接面对终端用户,错误的呈现方式决定了用户对系统的信任度。
六、选型建议
用了两周,说几个个人判断:
适合用 LangChain4j 的场景:需要在 Java 项目里集成多轮对话和工具调用,不想引入 Python 中间层。如果你的系统本身就是 Spring Boot 技术栈,LangChain4j 是目前最自然的选择,没有之一。我这次接 AI 助手的需求,从评估到上线用了不到一周,主要是框架省下来的胶水代码量太可观了。换以前自己撸,光是对话历史管理和工具调用 JSON schema 就要搞两天。
不适合的场景:如果你只是调一下 API 做个简单的文本生成,没必要上框架------RestTemplate 或 WebClient 直接调就够了。框架有学习成本,简单场景硬上框架反而增加复杂度。还有一种情况是你们系统已经在重度使用 Python,比如有现成的 LangChain 流水线,那就别再开 Java 侧的技术栈了,维护成本太高。
| 选型维度 | 裸调 API | LangChain4j | Spring AI |
|---|---|---|---|
| 学习成本 | 低 | 中 | 中 |
| 功能完整度 | 需自己实现 | 高(记忆/工具/RAG 全包) | 高 |
| 社区活跃度 | 不适用 | 快速增长中 | 有 Spring 背书 |
| 适合场景 | 简单单轮调用 | 复杂多轮对话+工具 | Spring 生态深度绑定 |
Spring AI 也在做类似的事,但我觉得 LangChain4j 在 AI Service 这个声明式接口的设计上更优雅,和 MyBatis Mapper 的思路一致,Java 开发者上手特别快。而且 LangChain4j 对模型 provider 的抽象做得不错,我现在想从 OpenAI 切到国产大模型,只需要改 starter 依赖和配置,业务代码一行不动。
还有一点个人体感:LangChain4j 的文档质量在 Java AI 框架里算第一梯队的,API 设计也基本符合 Java 开发者的习惯,没有把 Python 那套强搬过来。这点很重要,很多跨语言框架最容易死在"水土不服"上。
七、总结
把这两周踩的坑和关键配置要点汇总成一张清单,方便对照检查:
| 检查项 | 建议 |
|---|---|
| 依赖版本 | langchain4j 0.36+ 才有完整的 Spring Boot starter 支持 |
| 超时配置 | timeout 用 ISO 8601 格式(PT30S),别填数字 |
| 记忆窗口 | 长对话用 TokenWindowChatMemory,短对话用 MessageWindow |
| 记忆持久化 | 生产环境必须实现 ChatMemoryStore 接口接 Redis |
| 多用户隔离 | 用 @MemoryId 注解,别自己管 Map |
| 工具描述 | @Tool 描述要写清楚参数格式和返回含义 |
| 工具返回值 | 返回简单类型或格式化文本,别返回复杂对象 |
| 模型选择 | gpt-4o-mini 性价比最高,日常对话够用 |
| 错误处理 | 工具方法里 try-catch,别让异常冒泡到 AI 层 |
LangChain4j 目前还在快速迭代,版本之间 API 变动比较大。我用的 0.36.2 版本,到你现在看的时候可能已经有更新了,建议看官方文档确认 API 是否有变化。但核心思路------AI Service 接口 + ChatMemory + Tool------是不变的,这些概念已经是 Java AI 应用开发的标配了。
下周我打算在项目里加 RAG 能力,让 AI 助手能查我们的技术文档库。到时候再分享 RAG 集成的经验。