LangChain4j 简介

介绍

LangChain4j 是一个专为 Java 开发者设计的开源框架,旨在简化大语言模型(LLM)在 Java 应用中的集成。它的目标类似于 Python 生态中的 LangChain ,但针对Java生态进行了优化,提供统一的API抽象、上下文管理(Memory)、提示模板、文档检索(RAG)等功能。

统一的 API:LLM 提供商(如 OpenAI 或 Google Vertex AI)和嵌入向量存储(如 Pinecone 或 Milvus)使用专有的 API。LangChain4j 提供统一的 API,以避免学习和实现每个 API 的特定 API 的需要。要试验不同的 LLM 或嵌入存储,您可以轻松地在它们之间切换,而无需重写代码。

全面的工具箱:自 2023 年初以来,社区一直在构建许多由 LLM 驱动的应用程序,识别常见的抽象、模式和技术。LangChain4j 已将这些提炼成一个现成的包。我们的工具箱包括从低级别的提示模板、聊天记忆管理和函数调用到高级模式(如代理和 RAG)的工具。对于每个抽象,我们都提供一个接口以及基于常用技术的多个现成的实现。无论您是构建聊天机器人还是开发具有完整流程(从数据摄取到检索)的 RAG,LangChain4j 都能提供各种选项。

LangChain4j 旨在让 Java 开发者无需学习不同LLM提供商的专有API,快速构建智能应用(如聊天机器人、智能助手)。

目前主流的 Java AI 开发框架有 Spring AILangChain4j,它们都提供了很多 开箱即用的 API 来帮你调用大模型、实现 AI 开发常用的功能,比如,对话记忆,结构化输出,RAG 知识库,工具调用,MCP,SSE 流式输出 等等内容

这两个框架的很多概念和用法都是类似的,毕竟它们的服务对象和目标都是一致的,而且也都提供了很多插件扩展,都支持和 Spring Boot 项目集成。就我个人而言,我更喜欢使用 LangChain4j

两个抽象级别

LangChain4j 在两个抽象级别上运行,与众多框架一样,这是为了兼顾不同开发者的需求,提供了封装和相对底层的两个级别用于快速构建和定制开发

image-20260521094158917
  • 低级别抽象为开发者提供了与 LLM(大语言模型)交互的 原语 或基础构件。在这个层级,你可以获得最大的自由度和控制权。

    你可以完全控制 LLM 应用的各个组件,手动组合消息流程、向量存储、嵌入生成等细节。但相对的,你需要编写较多的粘合代码,例如这样手动构建消息列表并调用模型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 实例化模型
    ChatLanguageModel model = OpenAIChatModel.withApiKey("your-api-key");

    // 手动构建消息列表
    List<Message> messages = List.of(
    new SystemMessage("你是一个乐于助人的助手。"),
    new UserMessage("请给我讲一个笑话。")
    );

    // 调用模型并获取响应
    AiMessage response = model.generate(messages).content();
  • 高级别抽象通过封装复杂的底层逻辑,提供了更加简洁、声明式的开发体验。框架会自动处理细节。例如在高级别中,发送消息你只需要定义一个接口即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 定义一个 AI 服务接口
    @AiService
    interface Assistant {
    String chat(String userMessage);
    }

    // 创建并使用该服务
    Assistant assistant = AiServices.create(Assistant.class, model);
    String response = assistant.chat("请给我讲一个笑话。");

区分并简单介绍它们的依赖

LangChain4j的依赖有很多,而且各个之间作用区分很明显,配置不好甚至容易导致冲突,所以这里简单提及一下这些依赖它们的作用,负责的内容,之前的兼容性等等内容

首先,LangChain4j 遵守版本对齐原则,LangChain4j 的核心模块(langchain4j-corelangchain4j)和主流集成模块(如 langchain4j-anthropiclangchain4j-open-ai)通常需要并且也会保持版本同步(比如都是 1.15.0)。这些必须保持版本号完全一致

所以我们最好使用 BOM来管理依赖,你只需要在 Maven 的 <dependencyManagement> 中引入 BOM,之后引入任何 langchain4j 相关的依赖就不需要再写 <version> 标签了,它会自动帮你匹配最兼容的版本。

langchain4j模块化的特性非常明显,有时候一个完备的 AI Agent 应用可能会引入二三十个 LangChain4j 的依赖,图上没说的太多了,所有依赖的所有文档在这里

https://docs.langchain4j.dev/apidocs/index.html

langchain4j

这是框架的主模块。它依赖于 langchain4j-core,并提供了核心接口的具体实现和高级功能。比如大家常用的 AI Services(声明式 AI 接口)、文档加载与分割(DocumentLoaderTextSplitter)、内存管理实现以及 RAG(检索增强生成)的核心逻辑都在这里。

它位于中间层,承上启下。实际开发中,通常引入这个模块,引入它,你就拥有了编排 AI 业务流程的能力,但依然需要配合具体的 模型集成模块 才能真正调用大模型。

image-20260521095013183

这是其依赖,截至文章编写的最新版本为1.15.0

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.15.0</version>
<scope>compile</scope>
</dependency>

langchain4j-core

langchain4j-core是整个 LangChain4j 框架的地基。它只定义了最基础的接口和抽象类,比如 ChatModel(聊天模型接口)、EmbeddingStore(向量存储接口)、ChatMemory(对话记忆接口)以及各种核心数据类型(如 DocumentChatMessage)。

它处于依赖的最底层,几乎没有任何外部依赖。它不包含任何具体的模型实现,不能单独用来调用 AI。

正常情况下,我们几乎不直接引入它或者只引入它,但如果你只是想基于 LangChain4j 的标准开发自己的扩展插件,或者做一个非常轻量级的适配,可以只引入它。

在实际业务开发中,它通常作为其他模块的传递依赖被自动引入。

image-20260521095142646

这是其依赖,截至文章编写的最新版本为1.15.0

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
<version>1.15.0</version>
<scope>compile</scope>
</dependency>

langchain4j-mcp

这是一个扩展模块。MCP 全称是 Model Context Protocol(模型上下文协议),旨在标准化 AI 模型与外部数据/工具的连接方式。

引入它可以让你的 LangChain4j 应用支持 MCP 协议,从而更规范地获取上下文或连接外部系统。

这属于进阶玩法,基础的对话或 RAG 应用通常不需要引入。

image-20260521095603598

这是其依赖,截至文章编写的最新版本为1.15.0-beta25

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>1.15.0-beta25</version>
<scope>compile</scope>
</dependency>

langchain4j-embeddings

嵌入模型工具层,这个依赖是专门用于文本向量化(Embedding)的,它相当于一个本地 Embedding 模型运行引擎。

它的核心作用是提供在 Java 进程内通过 ONNX Runtime 加载和运行本地 AI 模型的基础设施,所以说,使用它,你需要自己去找向量化模型,并自定义加载逻辑,你就需要并且只需要依赖这个包就可以。

langchain4j-embeddings是一个基础工具包,提供了在 Java 进程中本地运行 Embedding 模型的基础设施和抽象。

如果你不想调用 OpenAI 等收费的在线 Embedding API,而是想在本地(内存中)直接把一段文字转化成向量,就可以引入这个具体的模型包。它通常用于本地开发测试,或者对数据隐私要求极高、不能出网的 RAG 场景。

image-20260521095848234

这是其依赖,截至文章编写的最新版本为1.15.0-beta25

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings</artifactId>
<version>1.15.0-beta25</version>
<scope>compile</scope>
</dependency>

langchain4j-embeddings-all-minilm-l6-v2-q是我们本地化 Embedding 用的最多的一个了,它不仅依赖了上面的“运行引擎”,还直接内置了 all-MiniLM-L6-v2 这个特定模型的权重文件(.onnx 文件)和对应的配置文件。

引入它,你不需要自己去网上找模型文件,直接实例化对应的 Java 类(如 AllMiniLmL6V2QuantizedEmbeddingModel)就能立刻开始把文本转成向量。

image-20260521095356783

这是其依赖,截至文章编写的最新版本为1.15.0-beta25

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings-all-minilm-l6-v2-q</artifactId>
<version>1.15.0-beta25</version>
<scope>test</scope>
</dependency>

langchain4j-{ai厂商或者模型名}

以 langchain4j-anthropic 举例子,这一类属于集成层,负责对接具体的 LLM 提供商或向量数据库。比如 langchain4j-anthropic 就是用来对接 Anthropic 的模型。同理还有 langchain4j-open-ailangchain4j-ollama(本地模型)、langchain4j-milvus(向量数据库)等。

它们依赖 langchain4j-core(有时也依赖主模块),实现了 core 中定义的接口(如 ChatModel)。引入它,你的应用才能真实地 连接 到某个具体的 AI 模型或数据库。你可以根据业务需要,只引入你使用的那一两个厂商模块,保持项目轻量。

image-20260521095255539

这是其依赖,截至文章编写的最新版本为1.15.0

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
<version>1.15.0</version>
<scope>compile</scope>
</dependency>

langchain4j-spring-boot-starter

该依赖是 Spring 框架的通用集成,是核心的自动配置中心,它本身不包含具体的模型实现,它主要负责搭建 Spring 环境下的环境。它提供了 Spring Boot 的自动配置能力,定义了 Spring 环境下最基础的接口抽象。

image-20260522150643428
1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.15.0-beta25</version>
<scope>compile</scope>
</dependency>

langchain4j-{模型名}-spring-boot-starter

这里以 open-ai 为例子,这是 组合依赖,内部已经包含了

  • langchain4j
  • langchain4j-open-ai
  • langchain4j-spring-boot-starter

它负责把 OpenAI 的 API 包装成 LangChain4j 标准的 ChatModel 接口,并且提供对应的配置项和自动配置的相关内容,让 Spring 容器能识别,而且直接拥有对接对应模型的能力

image-20260522150739633

这是其依赖,截至文章编写的最新版本为1.15.0-beta25

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.15.0-beta25</version>
<scope>test</scope>
</dependency>

langchain4j-http-client

这一组是比较底层的依赖,通常被隐藏在上述 Starter 内部

langchain4j-http-client是一个标准协议(SPI)。LangChain4j 定义了一套“如何发送 HTTP 请求”的规范,但它自己不写具体的发送代码。这让 LangChain4j 变得很轻量,因为它不强制绑定任何特定的 HTTP 框架

image-20260522145704895

这是其依赖,截至文章编写的最新版本为1.15.0

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-http-client</artifactId>
<version>1.15.0</version>
<scope>compile</scope>
</dependency>

它相关的还有个这个内容,是上述规范的默认实现。它使用了 Java 11+ 自带的 java.net.http.HttpClient 来发送请求,叫langchain4j-http-client-jdk

这意味着,如果你用的是 JDK 17+(LangChain4j 的门槛),你不需要引入任何第三方 HTTP 包(如 OkHttp 或 Apache HttpClient),LangChain4j 就能直接工作,依赖自己少引一个是一个我说

image-20260522145622661

这是其依赖,截至文章编写的最新版本为1.15.0

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-http-client-jdk</artifactId>
<version>1.15.0</version>
<scope>runtime</scope>
</dependency>

langchain4j-document-parser-apache-tika

这是一个很好用的文件解析器,用于处理各种文档。

在 Java 中处理文件非常麻烦。读 PDF 需要 PDFBox,读 Word 需要 POI,读 Excel 又要 EasyExcel 什么的。如果还要处理编码问题,会非常头大。

它是对 Apache Tika 的封装。也是老牌 Java 内容分析工具了

无论你给它 PDF、Word、Excel、HTML 还是纯文本,它都能把它们扒下来,变成 LangChain4j 能理解的纯文本,一般是Document 对象。而且亲测它不仅能提取文字,还能顺便提取文件的元数据(如作者、创建时间、文件类型),这对于后续做元数据过滤非常有用。

image-20260522150038977

这是其依赖,截至文章编写的最新版本为1.15.0-beta25

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-tika</artifactId>
<version>1.15.0-beta25</version>
<scope>compile</scope>
</dependency>

langchain4j-agentic

这是 LangChain4j 的智能体(Agent)核心框架。

如果说基础的语言模型是只会回答问题,那么加上这个模块,它就能帮你干活了。它提供了让 AI 能够自主规划、调用工具、循环思考的 Agent 能力。支持 Human-in-the-loop(人机协作,AI 做事前询问人类意见)、多智能体协作、以及复杂的任务拆解逻辑,这些都是 AI Agent 中比较常见而且实用的内容

当你需要 AI 不仅仅是聊天,而是要执行查询天气 -> 预定机票 -> 发送邮件这种复杂流程时,有必要进行使用。

image-20260522150328612

这是其依赖,截至文章编写的最新版本为1.15.0-beta25

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-agentic</artifactId>
<version>1.15.0-beta25</version>
<scope>compile</scope>
</dependency>

langchain4j-easy-rag

这是一个 RAG 快速工具包,标准的 RAG 流程非常复杂,而且涉及到各种组件,如果你只是需要一个轻量的 RAG 功能,或者说就是自己本地研究一个 RAG, 使用这个会很简单,而且应该是开箱即用的

它把上述复杂的流程封装成了一个简单的工具类,你只需要告诉它文件夹路径和 Embedding 模型,它会自动帮你完成文件的加载、分块、向量化和存储,而且默认配置能完全本地的进行,不需要配置复杂的向量数据库

我说,预制 RAG 嗯造

image-20260522150554774

这是其依赖,截至文章编写的最新版本为1.15.0-beta25

1
2
3
4
5
6
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>1.15.0-beta25</version>
<scope>compile</scope>
</dependency>

搭建一个LangChain4j聊天项目

本项目会使用 SSE 流式的方式来进行请求和接受 AI 的回复,这样更符合现代大模型相关业务的需求,SSE 比阻塞好太多了))

我们就简单的建立一个能够进行基础流式对话,并且能够有记忆的就可以了,因为我用的 deepseek 模型它怎么弄也不支持多模态,按道理来说应该是支持的,我也不知道什么情况)))

项目依赖与配置

对于依赖,有了上面我的区分讲解,想必大家肯定都知道引入什么依赖了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<properties>
<java.version>21</java.version>
<!-- LangChain4j 最新版本 -->
<langchain4j.version>1.15.0</langchain4j.version>
</properties>

<dependencyManagement>
<dependencies>
<!-- LangChain4j BOM:统一管理所有 langchain4j 模块版本 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<!-- LangChain4j 核心框架 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>

<!-- 本文使用deepseek模型,所以就用open ai兼容的格式来了 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

然后对于配置

1
2
3
4
5
6
7
8
9
10
11
12
spring.application.name=LangChain4jDemo
server.port=8594

app.ai.deepseek.api-key=sk-1xxxx
app.ai.deepseek.base-url=https://api.deepseek.com/v1

app.ai.deepseek.model-name=deepseek-v4-pro
app.ai.deepseek.temperature=0.7
app.ai.deepseek.max-tokens=4096

# 聊天记忆每个会话最大保留消息数
app.ai.memory.max-messages=20

实体

对于我们在 LangChain4j 中与 LLM 进行沟通,可以这样构建请求体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 通用聊天请求 DTO。
*
* <p>sessionId 用于标识一个独立会话,相同 sessionId 的请求共享同一份聊天记忆,
* 不同 sessionId 之间的记忆完全隔离,这是实现多用户/多会话记忆管理的关键。
*/
@Data
public class ChatRequest {

/**
* 会话唯一标识。
* 客户端应在同一轮对话中始终传入相同的 sessionId,
* 若不传则视为无状态单次对话(由 Controller 层处理默认值)。
*/
private String sessionId;

/**
* 用户输入的消息内容(纯文本)。
*/
private String message;
}

那么 AI 的响应也就可以是这样,但是因为 SSE 不涉及到这个,所以这个就是给同步的请求演示的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 通用聊天响应 DTO(用于同步接口)。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatResponse {

/**
* AI 回复的文本内容。
*/
private String content;

/**
* 本次对话的会话 ID,方便客户端在后续请求中复用。
*/
private String sessionId;

public static ChatResponse of(String content, String sessionId) {
return new ChatResponse(content, sessionId);
}
}

很明显我们能发现,sessionId是实现多用户/多会话记忆的核心

  • ChatLanguageModel 本身是无状态的:每次调用 generate() 方法都是独立的,不知道之前的对话内容
  • 所以说,我们需要 LangChain4j 的 ChatMemory 组件通过 sessionId 来区分不同会话

这些内容在服务层部分会有进一步的体现

服务层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import dev.langchain4j.data.message.*;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import hbnu.project.langchain4jdemo.config.AiConfig;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
* AI 聊天核心服务类。
*
* <p>本类演示了以下 LangChain4j 低级 API 的核心用法:
* <ul>
* <li>手动管理 {@link ChatMemory},实现多会话隔离的对话记忆</li>
* <li>使用 {@link ChatModel} 进行同步阻塞式对话</li>
* <li>构建包含 {@link TextContent} 和 {@link ImageContent} 的多模态 {@link UserMessage}</li>
* <li>通过 {@link SystemMessage} 设置 AI 角色人格</li>
* </ul>
*
* <p>记忆管理策略:
* 使用 {@link ConcurrentHashMap} 以 sessionId 为 key 缓存每个会话的 {@link ChatMemory},
* 保证线程安全且每个会话完全独立。
*/
@Service
public class ChatService {

// 系统基础提示词
private static final String SYSTEM_PROMPT =
"你是一个友好、专业的 AI 助手。" +
"你的目标是为用户提供准确、有帮助的回答。" +
"在回答问题时,请保持简洁清晰,并在必要时提供示例。";

private final ChatModel chatModel;

private final StreamingChatModel streamingChatModel;

private final AiConfig aiConfig;

/**
* 以 sessionId 为 key 缓存每个会话独立的 ChatMemory。
* 生产环境中建议替换为带 TTL 的缓存(如 Caffeine 或 Redis)。
*/
private final ConcurrentHashMap<String, ChatMemory> memoryMap = new ConcurrentHashMap<>();

public ChatService(ChatModel chatModel, StreamingChatModel streamingChatModel, AiConfig aiConfig) {
this.chatModel = chatModel;
this.streamingChatModel = streamingChatModel;
this.aiConfig = aiConfig;
}

/**
* 获取或创建指定 sessionId 对应的 ChatMemory。
* 首次访问时会自动注入系统提示,设置 AI 角色。
*/
private ChatMemory getOrCreateMemory(String sessionId) {
return memoryMap.computeIfAbsent(sessionId, id -> {
ChatMemory memory = aiConfig.buildChatMemory(id);
// 初始化系统提示,告知 AI 它的角色定位
memory.add(SystemMessage.from(SYSTEM_PROMPT));
return memory;
});
}

/**
* 清除指定会话的聊天记忆。
*/
public void clearMemory(String sessionId) {
ChatMemory memory = memoryMap.remove(sessionId);
if (memory != null) {
memory.clear();
}
}

/**
* 同步阻塞式文本对话(带记忆)。
*
* <p>核心步骤:
* <ol>
* <li>将用户消息加入记忆</li>
* <li>将记忆中的所有消息(含历史)传给 ChatModel</li>
* <li>将 AI 回复加入记忆</li>
* <li>返回 AI 回复文本</li>
* </ol>
*
* @param sessionId 会话标识
* @param userInput 用户输入文本
* @return AI 回复文本
*/
public String chat(String sessionId, String userInput) {
ChatMemory memory = getOrCreateMemory(sessionId);

// 将用户消息加入记忆
UserMessage userMessage = UserMessage.from(userInput);
memory.add(userMessage);

// 传入全部历史消息,让模型感知上下文
AiMessage aiMessage = chatModel.chat(memory.messages()).aiMessage();

// 将 AI 回复也加入记忆,供下轮对话使用
memory.add(aiMessage);

return aiMessage.text();
}

/**
* SSE 流式文本对话(带记忆)。
*
* <p>通过 {@link StreamingChatModel} 逐 token 推送内容到 {@link SseEmitter},
* 实现类似 ChatGPT 的打字机效果。
*
* <p>核心步骤:
* <ol>
* <li>将用户消息加入记忆</li>
* <li>调用 streamingChatModel.chat(),注册 StreamingResponseHandler 回调</li>
* <li>在 onNext 回调中,通过 SseEmitter 逐 token 推送给客户端</li>
* <li>在 onComplete 回调中,将完整 AI 回复加入记忆,并关闭 SseEmitter</li>
* <li>在 onError 回调中,推送错误并关闭 SseEmitter</li>
* </ol>
*
* @param sessionId 会话标识
* @param userInput 用户输入文本
* @param emitter SSE 发射器,用于向客户端推送数据
*/
public void streamChat(String sessionId, String userInput, SseEmitter emitter) {
ChatMemory memory = getOrCreateMemory(sessionId);

UserMessage userMessage = UserMessage.from(userInput);
memory.add(userMessage);

// 用 StringBuilder 拼接完整回复,等流结束后存入记忆
StringBuilder fullResponse = new StringBuilder();

streamingChatModel.chat(memory.messages(), new StreamingChatResponseHandler() {

@Override
public void onPartialResponse(String token) {
// 每收到一个 token 就立刻推送给客户端
try {
fullResponse.append(token);
emitter.send(SseEmitter.event().data(token));
} catch (IOException e) {
emitter.completeWithError(e);
}
}

@Override
public void onCompleteResponse(ChatResponse response) {
// 流结束后,将完整 AI 回复写入记忆
memory.add(response.aiMessage());
// 发送结束标记,客户端可据此关闭 EventSource
try {
emitter.send(SseEmitter.event().name("done").data("[DONE]"));
} catch (IOException e) {
// 忽略关闭时的 IO 异常
}
emitter.complete();
}

@Override
public void onError(Throwable error) {
try {
emitter.send(SseEmitter.event().name("error").data(error.getMessage()));
} catch (IOException e) {
// 忽略
}
emitter.completeWithError(error);
}
});
}
}

很明显,我们使用了两种的 AI 对话形式,包括同步和流式,这两种 ChatModel 的本质区别我们来看看

  • 对于同步阻塞,使用的就是chatModel

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public String chat(String sessionId, String userInput) {
    // 1. 构建完整上下文
    ChatMemory memory = getOrCreateMemory(sessionId);
    memory.add(UserMessage.from(userInput));

    // 2. 阻塞等待完整响应(可能 3-10 秒)
    AiMessage aiMessage = chatModel.chat(memory.messages()).aiMessage();

    // 3. 一次性返回
    memory.add(aiMessage);
    return aiMessage.text();
    }

    不需要实时反馈的场景下,阻塞实现起来还是很简单的,但是消息过长容易导致一些不必要的问题

  • 对于流式,使用的就是streamingChatModel,我们发现,创建streamingChatModel对象后重写它的这三个方法,就能够正确使用流式对话了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    streamingChatModel.chat(memory.messages(), new StreamingChatResponseHandler() {
    @Override
    public void onPartialResponse(String token) {
    // 每次收到 token(通常几个字符)就立即发送
    emitter.send(token); // 用户几乎实时看到 AI "打字"
    }

    @Override
    public void onCompleteResponse(ChatResponse response) {
    // 流结束后,将完整回复存入记忆
    memory.add(response.aiMessage());
    }

    @Override
    public void onError(Throwable error) {
    // 错误处理
    }
    });

    流式不需要在服务端缓存完整响应,所以说会有那种一块一块的流式感,而且可以提前中断

至于这两个接口的详细内容,下面会提到

对于记忆,就是涉及到了 ChatMemory 的内容,而 ChatMemory 内部维护着一个消息列表,在与 AI 对话的时候,会发送上下文记忆,所以说,要限制好上下文的大小,防止成本太高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ChatMemory 内部维护的消息列表
memory.add(SystemMessage.from("你是一个专业助手"));
memory.add(UserMessage.from("我叫张三"));
memory.add(AiMessage.from("你好张三!"));
memory.add(UserMessage.from("我叫什么?"));

// chatModel.chat(memory.messages()) 实际发送的消息结构
// 这几种消息类型下面会提到
[
SystemMessage("你是一个专业助手"),
UserMessage("我叫张三"),
AiMessage("你好张三!"),
UserMessage("我叫什么?")
]

控制器

接口这边其实就没啥好说的了,正常调用就可以了,只不过对于 SSE 部分,要正确处理好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@RestController
@RequestMapping("/api/chat")
@CrossOrigin(origins = "*")
public class ChatController {

private final ChatService chatService;

public ChatController(ChatService chatService) {
this.chatService = chatService;
}

/**
* SSE 流式文本对话接口(带记忆)
*/
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamChat(@RequestBody ChatRequest request) {
// 若客户端未传 sessionId,则自动生成一个,保证每次都是新会话
if (request.getSessionId() == null || request.getSessionId().isBlank()) {
request.setSessionId(UUID.randomUUID().toString());
}

// SseEmitter 超时时间设为 3 分钟,防止长对话超时断连
SseEmitter emitter = new SseEmitter(3 * 60 * 1000L);

// 在新线程中执行流式请求,避免阻塞 HTTP 请求线程
Thread.ofVirtual().start(() ->
chatService.streamChat(request.getSessionId(), request.getMessage(), emitter)
);

return emitter;
}

/**
* 同步阻塞式文本对话接口(带记忆)。
* 等待模型生成完整回复后一次性返回,适合简单测试。
*/
@PostMapping("/sync")
public ResponseEntity<ChatResponse> syncChat(@RequestBody ChatRequest request) {
if (request.getSessionId() == null || request.getSessionId().isBlank()) {
request.setSessionId(UUID.randomUUID().toString());
}

String reply = chatService.chat(request.getSessionId(), request.getMessage());
return ResponseEntity.ok(ChatResponse.of(reply, request.getSessionId()));
}

/**
* 记忆管理
* 清除指定会话的聊天记忆。
* 适用于用户主动开启新话题,或退出对话时清理资源。
*/
@DeleteMapping("/memory/{sessionId}")
public ResponseEntity<String> clearMemory(@PathVariable String sessionId) {
chatService.clearMemory(sessionId);
return ResponseEntity.ok("会话 [" + sessionId + "] 的记忆已清除");
}
}

测试

首先我们测试一下同步对话,可以发现我们能够正常的与 LLM 进行对话了

image-20260529112136263

之后我们来测试一下同步对话请求的接口,可以发现阻塞式的体验是远远不如 SSE 的,等待时间也更长

image-20260527092040926

然后,我们一直使用的都是my-session-001,我们验证一下记忆的相关内容

首先问一下模型是否还保持记忆

image-20260529112506798

通过简单的诱导,虽然 deepseek 说自己不支持跨实例记忆这个那个的,但是还是能够回答最近我都问了什么,说明我们的 LangChain4j 是参与了相关的记忆的处理

我们使用我们项目中的接口来处理一下一轮对话的记忆,我们使用相同的sessionId,发现LLM怎么诱导也想不起来自己最近说过什么内容了,说明这个是真清理了

image-20260529112817548