LangChain4j 入门:Java 程序员的第一个 AI 对话程序
不需要学 Python,不需要啃论文,用你最熟悉的 Spring Boot,5 分钟跑通第一个 AI 对话。
本文记录我从零搭建 LangChain4j + Spring Boot 项目的完整过程,附真实可运行代码。
为什么选 LangChain4j?
先说背景:我是一个 Java 后端开发,在电力行业写了 5 年业务代码。当 AI 浪潮来的时候,我面临一个选择------去学 Python 生态(LangChain / LlamaIndex),还是留在 Java 生态?
答案是 LangChain4j。理由很简单:
- Java 原生:不需要切语言,不需要维护两套技术栈
- Spring Boot 友好:有官方 Starter(虽然本文手动装配,为了理解底层)
- API 设计贴近 Java 思维 :
ChatLanguageModel类比 Service 接口,ChatMemory类比 Session - 生产可用:已经有企业级案例,不是玩具框架
本文所有代码来自我的开源项目 langchain4j-demo,欢迎 Star。
1. 5 分钟搭好项目骨架
环境要求
| 工具 | 版本 | 说明 |
|---|---|---|
| JDK | 21+ | Lombok 在 JDK 21 下最稳定 |
| Maven | 3.8+ | 依赖管理 |
| DeepSeek API Key | --- | 本文用 DeepSeek,也可以换 Ollama 本地模型 |
pom.xml 核心依赖
只需要 3 个依赖:
xml
<properties>
<java.version>21</java.version>
<langchain4j.version>1.0.0-beta1</langchain4j.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- LangChain4j 核心 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- OpenAI 兼容适配器(兼容 DeepSeek / Ollama / DashScope 等) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
</dependencies>
关键理解 :
langchain4j-open-ai不只是对接 OpenAI,它是一个 OpenAI 兼容协议适配器 。任何提供/v1/chat/completions端点的服务(DeepSeek、Ollama、SiliconFlow、通义千问 DashScope)都能用。
2. 模型配置:手动装配 vs Starter
LangChain4j 有官方 Spring Boot Starter,但我选择 手动装配。原因:
- Phase 1 的目标是理解底层,不是追求开发速度
- 知道
@Bean怎么注册的,后面用 Starter 时才不会"黑盒恐惧" - 方便切换模型(改一个
@ConfigurationProperties就行)
scss
@Configuration
public class AiModelConfig {
@Bean
@ConfigurationProperties(prefix = "ai.model")
public AiModelProperties aiModelProperties() {
return new AiModelProperties();
}
/**
* 同步聊天模型(一问一答,等完整回复)
*/
@Bean
public ChatLanguageModel chatLanguageModel(AiModelProperties props) {
return OpenAiChatModel.builder()
.baseUrl(props.getBaseUrl()) // DeepSeek: https://api.deepseek.com/v1
.apiKey(props.getApiKey()) // 从环境变量/外部配置读取
.modelName(props.getModelName()) // deepseek-chat
.temperature(props.getTemperature())
.maxTokens(props.getMaxTokens())
.timeout(Duration.ofSeconds(120))
.build();
}
/**
* 流式聊天模型(逐 token 返回,打字机效果)
*/
@Bean
public StreamingChatLanguageModel streamingChatLanguageModel(AiModelProperties props) {
return OpenAiStreamingChatModel.builder()
.baseUrl(props.getBaseUrl())
.apiKey(props.getApiKey())
.modelName(props.getModelName())
.temperature(props.getTemperature())
.maxTokens(props.getMaxTokens())
.timeout(Duration.ofSeconds(120))
.build();
}
}
application.yml 配置(支持 Ollama / DeepSeek / 智谱 等切换):
yaml
ai:
model:
base-url: https://api.deepseek.com/v1
api-key: ${ai.model.deepseek-api-key} # 从外部 secrets 文件读取,不写死在代码里
model-name: deepseek-chat
temperature: 0.3
max-tokens: 1024
💡 为什么要区分 ChatLanguageModel 和 StreamingChatLanguageModel?
| 类型 | 类比 | 行为 | 适用场景 |
|---|---|---|---|
ChatLanguageModel |
同步 Service | 等完整回复,一次性返回 | API 调用、批量处理 |
StreamingChatLanguageModel |
带回调的 Service | 逐 token 回调,打字机效果 | 聊天 UI、SSE 推送 |
它们是两个独立的 Bean,因为底层连接方式和回调机制完全不同。不要试图用一个替换另一个。
3. 第一个对话:3 行代码跑通 AI
typescript
@Service
public class ChatService {
private final ChatLanguageModel chatModel;
// 最简单的对话:系统提示词 + 用户消息 → AI 回复
public String chat(String userMessage) {
List<ChatMessage> messages = List.of(
SystemMessage.from("你是一个电力行业AI助手"),
UserMessage.from(userMessage)
);
ChatResponse response = chatModel.chat(messages);
return response.aiMessage().text();
}
}
LangChain4j 消息类型速查(Java 开发者视角)
| 概念 | Java 类比 | 说明 |
|---|---|---|
SystemMessage |
配置文件/全局常量 | 设定 AI 的角色、行为边界、输出格式 |
UserMessage |
方法入参 | 用户输入的问题 |
AiMessage |
方法返回值 | AI 生成的回复 |
ChatMessage |
接口/基类 | 以上三者的父接口 |
关键认知 :LangChain4j 把"对话"抽象成
List<ChatMessage>。你传给模型的不是一段字符串,而是一个消息列表。这让多轮对话、上下文注入变得极其自然。
为什么 SystemMessage 重要?
没有 SystemMessage 时,模型不知道自己是谁。加上 SystemMessage 后:
ini
private static final String POWER_INDUSTRY_SYSTEM_PROMPT = """
你是一个电力行业AI助手,具备以下专业能力:
1. 电力系统运行分析(负荷预测、潮流计算、故障诊断)
2. 设备管理(变压器、开关柜、线路巡检)
3. 安全规程解读(安规、两票三制)
回答要求:
- 使用专业术语,但解释要通俗易懂
- 涉及安全问题时,必须强调安全规程
""";
这样每次对话都会带上这段角色设定,AI 回答会自动限定在电力领域。
4. 多轮对话:手写一个 ChatMemory
LangChain4j 有官方的 ChatMemory API,但为了理解底层原理,我们先用 ConcurrentHashMap 手写一个:
csharp
@Service
public class ChatService {
// 用 ConcurrentHashMap 存储每个用户的对话历史
private final Map<String, List<ChatMessage>> userConversations = new ConcurrentHashMap<>();
public String chatWithMemory(String userId, String userMessage) {
// 1. 获取或创建该用户的历史消息
// computeIfAbsent:key 不存在时自动创建
List<ChatMessage> messages = userConversations.computeIfAbsent(userId, k -> {
List<ChatMessage> list = new ArrayList<>();
list.add(SystemMessage.from(POWER_INDUSTRY_SYSTEM_PROMPT)); // 首次加系统提示词
return list;
});
// 2. 追加当前用户消息
messages.add(UserMessage.from(userMessage));
// 3. 把完整历史发给模型
ChatResponse response = chatModel.chat(messages);
String aiReply = response.aiMessage().text();
// 4. 把 AI 回复也加入历史(下一轮会带上)
messages.add(AiMessage.from(aiReply));
// 5. 防止历史太长,限制最近 20 条
if (messages.size() > 22) {
List<ChatMessage> trimmed = new ArrayList<>();
trimmed.add(messages.get(0)); // 保留系统提示词
trimmed.addAll(messages.subList(messages.size() - 20, messages.size()));
userConversations.put(userId, trimmed);
}
return aiReply;
}
}
这段代码体现了什么?
- 对话就是
List<ChatMessage>的累积------SystemMessage 在最前面,之后的 UserMessage 和 AiMessage 交替追加 computeIfAbsent是一个优雅的模式:首次对话自动初始化,后续对话直接追加- 消息截断是必须的------不截断的话,100 轮对话后 token 超限,模型直接报错
5. 流式输出:逐 token 推送的 SSE 接口
假流式 vs 真流式
很多教程写的"流式输出"是这样的:
arduino
// ❌ 假流式:等完整响应返回后一次性推送给前端
CompletableFuture<String> future = ...;
String full = future.get(); // 阻塞等待
emitter.send(full); // 一次性推送
这不是真正的流式。真正的流式是:模型每生成一个字,就立即推送给前端。
正确的实现
typescript
// ChatService.java --- 流式方法,接受三个回调
public void streamChat(
String userMessage,
Consumer<String> onToken, // 每收到一个 token 就调
Runnable onComplete, // 完成时调
Consumer<Throwable> onError // 出错时调
) {
List<ChatMessage> messages = List.of(
SystemMessage.from(POWER_INDUSTRY_SYSTEM_PROMPT),
UserMessage.from(userMessage)
);
streamingChatModel.chat(messages, new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String partialResponse) {
onToken.accept(partialResponse); // 立即回调,不等待
}
@Override
public void onCompleteResponse(ChatResponse response) {
onComplete.run();
}
@Override
public void onError(Throwable error) {
onError.accept(error);
}
});
}
less
// ChatController.java --- SSE 端点,每个 token 立即推送
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(@RequestBody Map<String, String> request) {
String message = request.get("message");
SseEmitter emitter = new SseEmitter(120_000L); // 2 分钟超时
chatService.streamChat(message,
// onToken:每收到一个 token,立即通过 SSE 推送
token -> {
try {
emitter.send(SseEmitter.event().data(token).build());
} catch (IOException e) {
log.error("推送失败", e);
}
},
// onComplete:推送 [DONE] 标记,关闭连接
() -> {
emitter.send(SseEmitter.event().data("[DONE]").build());
emitter.complete();
},
// onError:推送错误信息
error -> {
emitter.send(SseEmitter.event()
.data("{"error": "" + error.getMessage() + ""}")
.build());
emitter.completeWithError(error);
}
);
return emitter;
}
测试流式效果
bash
curl -N -X POST http://localhost:8080/api/chat/stream \
-H "Content-Type: application/json" \
-d "{"message": "用一句话解释什么是台区线损"}"
-N参数是关键 :禁用 curl 的输出缓冲。不加-N的话,curl 会把所有 SSE event 缓存起来一次显示,你就看不到打字机效果了。
6. 完整 REST 接口一览
| 接口 | 方法 | 说明 |
|---|---|---|
POST /api/chat |
同步对话 | 一问一答,等完整回复 |
POST /api/chat/stream |
流式对话 | SSE 逐 token 推送,打字机效果 |
POST /api/chat/multi |
多轮对话 | 传入 userId,自动维护上下文 |
POST /api/chat/clear?userId=xxx |
清空历史 | 重置某用户的对话 |
GET /api/chat/history?userId=xxx |
查看历史 | 调试用 |
GET /api/chat/health |
健康检查 | 确认服务 alive |
7. 完整项目结构
bash
langchain4j-demo/
├── pom.xml # Maven 依赖(3 个核心依赖)
├── src/main/java/com/power/ai/
│ ├── LangChain4jDemoApplication.java # Spring Boot 启动类
│ ├── config/
│ │ └── AiModelConfig.java # 模型手动装配(两个 Bean)
│ ├── controller/
│ │ └── ChatController.java # 6 个 REST 接口
│ └── service/
│ └── ChatService.java # 核心业务逻辑(230 行)
├── src/main/resources/
│ └── application.yml # 模型配置(支持 4 种后端切换)
└── src/test/java/com/power/ai/
└── ChatServiceTest.java # 集成测试
8. 我踩过的坑
坑 1:OpenAiChatModel 的 baseUrl 必须带 /v1
arduino
// ❌ 错误:404
.baseUrl("https://api.deepseek.com")
// ✅ 正确
.baseUrl("https://api.deepseek.com/v1")
LangChain4j 的 OpenAiChatModel 会在 baseUrl 后面拼接 /chat/completions,最终请求 https://api.deepseek.com/v1/chat/completions。如果你漏了 /v1,请求就发到了错误的路径。
坑 2:"假流式"陷阱
很多人写了 StreamingChatLanguageModel 以为就是流式了,实际上用 CompletableFuture.get() 阻塞等待完整结果再推送------这是假流式。判断方法:用 curl -N 测试,如果响应是一次性到的就是假流式。
坑 3:消息列表不要忘了加 SystemMessage
多轮对话初始化时,如果不加 SystemMessage,模型就从第一轮的角色变成通用助手,后续回答可能脱离电力领域。
下一步
这个 Demo 覆盖了 LangChain4j 的入门核心能力,但离真正的 AI 应用还差几步:
- Prompt 工程:CoT 思维链、Few-Shot 少样本、结构化输出(第 2 周)
- ChatMemory 官方 API:替换手写的 ConcurrentHashMap(第 3 周)
- RAG 文档问答:EmbeddingModel + 向量数据库(第 4--6 周)
如果你也在用 Java 学 AI,欢迎关注我的 GitHub 和掘金账号,每周更新学习笔记。
关于作者:8 年电力行业 Java 后端开发,正在用 10 个月转型 AI 大模型工程师。记录转型全过程:Java → LangChain4j → RAG → Agent。真实比完美重要,输出倒逼输入。