LangChain4j 入门:Java 程序员的第一个 AI 对话程序

LangChain4j 入门:Java 程序员的第一个 AI 对话程序

不需要学 Python,不需要啃论文,用你最熟悉的 Spring Boot,5 分钟跑通第一个 AI 对话。

本文记录我从零搭建 LangChain4j + Spring Boot 项目的完整过程,附真实可运行代码。


为什么选 LangChain4j?

先说背景:我是一个 Java 后端开发,在电力行业写了 5 年业务代码。当 AI 浪潮来的时候,我面临一个选择------去学 Python 生态(LangChain / LlamaIndex),还是留在 Java 生态?

答案是 LangChain4j。理由很简单:

  1. Java 原生:不需要切语言,不需要维护两套技术栈
  2. Spring Boot 友好:有官方 Starter(虽然本文手动装配,为了理解底层)
  3. API 设计贴近 Java 思维ChatLanguageModel 类比 Service 接口,ChatMemory 类比 Session
  4. 生产可用:已经有企业级案例,不是玩具框架

本文所有代码来自我的开源项目 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;
    }
}

这段代码体现了什么?

  1. 对话就是 List<ChatMessage> 的累积------SystemMessage 在最前面,之后的 UserMessage 和 AiMessage 交替追加
  2. computeIfAbsent 是一个优雅的模式:首次对话自动初始化,后续对话直接追加
  3. 消息截断是必须的------不截断的话,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。真实比完美重要,输出倒逼输入。

相关推荐
海兰1 小时前
【实用程序】电商销售分析仪表盘 — 从零搭建一个AI参与的全栈数据洞察系统
人工智能·学习·算法
枫糖浆AI1 小时前
openclaw页面无法访问解决方法
人工智能
码农刚子1 小时前
从零开始:在 Windows 服务器上部署 Node.js 项目(小白实战教程)
后端·node.js
Cache技术分享2 小时前
435. Java 日期时间 API - Clock 灵活获取当前时间
前端·后端
浩子coding2 小时前
通过 Spring AI Alibaba 源码,看如何玩转 ReAct 智能体范式
人工智能·后端
卡梅德生物科技小能手2 小时前
卡梅德生物科普CD124(IL-4Rα):2型免疫炎症的核心调控靶点
人工智能·经验分享·深度学习
垂钓的小鱼12 小时前
TRIZ理论是什么?萃智引擎如何将它变为工程师的AI创新助手
人工智能·microsoft
咋吃都不胖lyh2 小时前
DBSCAN(基于密度的空间聚类应用与噪声)算法
人工智能·机器学习
诸葛务农2 小时前
涡喷式发烟机施放粉末状烟剂成烟面积的计算:烟剂材料特性的影响
人工智能