文章目录
- 一、前言
- 二、简单示例
- [三、Model 类型](#三、Model 类型)
- 四、ChatMessage
-
- [1. ChatRequest](#1. ChatRequest)
- [2. ChatMessage 的类型](#2. ChatMessage 的类型)
-
- [2.1 UserMessage](#2.1 UserMessage)
- [2.2 AiMessage](#2.2 AiMessage)
- [2.3 ToolExecutionResultMessage](#2.3 ToolExecutionResultMessage)
- [2.4 SystemMessage](#2.4 SystemMessage)
- [2.5 CustomMessage](#2.5 CustomMessage)
- [五、UserMessage 多模态](#五、UserMessage 多模态)
- [六、Chat Memory](#六、Chat Memory)
-
- [1. ChatMemory 的特性](#1. ChatMemory 的特性)
- [2. Eviction policy](#2. Eviction policy)
- [3. Persistence](#3. Persistence)
-
- [3.1 ChatMemoryStore](#3.1 ChatMemoryStore)
- [3.2 基于 Redis 的持久化案例](#3.2 基于 Redis 的持久化案例)
- [4. 对 SystemMessage的特殊处理](#4. 对 SystemMessage的特殊处理)
- [5. Tool messages 的特殊处理](#5. Tool messages 的特殊处理)
- [七、Model Parameters](#七、Model Parameters)
- 八、流式响应
-
- [1. StreamingChatResponseHandler](#1. StreamingChatResponseHandler)
- [2. 示例](#2. 示例)
- 九、参考内容
一、前言
本系列仅做个人笔记使用,绝大部分内容基于 LangChain4j 官网 ,内容个人做了一定修改,可能存在错漏,一切以官网为准。
本系列使用 LangChain4j 版本:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.8.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
本系列完整代码地址 :langchain4j-hwl
二、简单示例
首先搭建一个基础 langchain4j 项目,基于 LangChain4j 搭建项目非常简单,参照 https://docs.langchain4j.dev/get-started 即可。
-
引入基础依赖
xml<dependencyManagement> <dependencies> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-bom</artifactId> <version>1.8.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> -
引入 open-ai 依赖
xml
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
- 配置响应模型 : 官方默认提供了 apiKey,如果使用 "demo" 则会默认转发到官方提供的 apiKey,方便用户调试。
java
@Bean
public OpenAiChatModel openAiChatModel() {
return OpenAiChatModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.apiKey("demo")
.modelName("gpt-4o-mini")
.build();
}
-
至此便可以完成了一个简单 AI 服务的搭建,可以直接调用。
java@Service public class LlmServiceImpl implements LlmService { @Resource private OpenAiChatModel chatModel; @Override public String chat(String userMessage) { return chatModel.chat(userMessage); } }
三、Model 类型
LangChain4j 支持的模型如下:
| 模型 | 作用 |
|---|---|
| LanguageModel | 该API非常简单:接受String作为输入,并返回String作为输出。这种API正逐渐被淘汰,转而使用 ChatModel |
| ChatModel | 它们接受多个ChatMessage作为输入,并返回单个 AiMessage 作为输出。ChatMessage 通常包含文本,但一些大语言模型也支持其他模态(例如图像、音频等)。 |
| EmbeddingModel | 该模型可以将文本转换为一个Embedding |
| ImageModel | 该模型可以生成和编辑Image |
| ModerationModel | 该模型可以检查文本是否包含有害内容 |
| ScoringModel | 该模型可以根据查询对多个文本片段进行评分(或排序),本质上是确定每个文本片段与查询的相关程度 |
LangChain4j 提供了两种API :low-level API(以ChatModel为核心)和 high-level API(以AI Services为代表)。
- ChatModel是 LangChain4j 与大语言模型(LLMs)交互的底层基础接口,其设计目标是暴露模型交互的 "原始能力",让开发者能直接操控每一个细节,适合需要深度定制的场景。
- AI Services是在ChatModel等底层能力基础上封装的高层接口,其设计目标是 "屏蔽底层细节",让开发者通过更简洁的方式调用 LLM 能力,适合快速开发或无需深度定制的场景。
本篇内容以 ChatModel 为主,关于 AI Services 的内容我们在 【LangChain4j 02】【AI Services】 中进行进行了详细介绍
四、ChatMessage
1. ChatRequest
我们可以通过 ChatModel#chat 来与 LLM 进行沟通,而 ChatModel#chat 有多个重载方法,如下:
java
default ChatResponse chat(ChatRequest chatRequest) {
...
}
default String chat(String userMessage) {
ChatRequest chatRequest = ChatRequest.builder().messages(new ChatMessage[]{UserMessage.from(userMessage)}).build();
ChatResponse chatResponse = this.chat(chatRequest);
return chatResponse.aiMessage().text();
}
default ChatResponse chat(ChatMessage... messages) {
ChatRequest chatRequest = ChatRequest.builder().messages(messages).build();
return this.chat(chatRequest);
}
default ChatResponse chat(List<ChatMessage> messages) {
ChatRequest chatRequest = ChatRequest.builder().messages(messages).build();
return this.chat(chatRequest);
}
可以看到 ChatModel#chat 最终会封装成 ChatRequest 类型发起请求,如果需要自定义更多请求参数,便可以使用 ChatRequest 类型,如下:
java
ChatRequest chatRequest = ChatRequest.builder()
.messages(...)
.modelName(...)
.temperature(...)
.topP(...)
.topK(...)
.frequencyPenalty(...)
.presencePenalty(...)
.maxOutputTokens(...)
.stopSequences(...)
.toolSpecifications(...)
.toolChoice(...)
.responseFormat(...)
.parameters(...) // you can also set common or provider-specific parameters all at once
.build();
ChatResponse chatResponse = chatModel.chat(chatRequest);
2. ChatMessage 的类型
2.1 UserMessage
UserMessage是 LangChain4j 中向LLM传递"用户侧需求"的标准化载体------无论是人类的提问、应用的自动化请求,都通过它规整格式后提交给LLM,同时通过contents()适配多模态交互、attributes()留存本地上下文,为后续多轮对话(依赖ChatMemory)提供支持。
人话 :调用 LLM 时的发送的消息类型
UserMessage 有三个属性,如下:
| 要素 | 功能说明 | 关键注意点 |
|---|---|---|
contents() |
承载消息核心内容,是与LLM交互的核心数据 | 内容格式取决于LLM支持的"模态":既可以是简单文本(String),也能包含图片、音频等非文本模态 |
name() |
标识用户名称(如人类用户的昵称、应用程序的服务名) | 兼容性有限,并非所有LLM提供商(如OpenAI、Google)都支持该字段,使用前需确认适配性 |
attributes() |
存储附加信息(如用户ID、请求时间等) | 仅用于本地存储(存入ChatMemory,即聊天记忆),不会随请求发送给LLM,属于"本地辅助数据" |
2.2 AiMessage
AiMessage是AI模型对输入消息(如用户消息、系统消息等)的"官方回复载体",是LLM(大语言模型)输出内容的结构化封装------所有由AI生成的响应,最终都会以AiMessage的形式在LangChain4j框架中流转,方便后续处理(如存入聊天记忆、触发工具调用等)。
人话 :调用 LLM 时返回的内容
AiMessage 有三个属性,如下:
| 要素 | 功能说明 |
|---|---|
text() |
文本内容,最基础的组成部分,即AI对外展示的"自然语言回复",比如用户问"今天天气如何"时,AI返回的"今日气温25℃,晴"就存储在text()中 |
thinking() |
思考/推理内容。用于记录AI生成回复过程中的"内部逻辑",相当于AI的"草稿纸"。例如在复杂问题推理(如"如何规划去北京的旅行")中,AI可能会先通过thinking()记录"第一步确定出行时间,第二步查交通方式......",最终仅将整理后的结论放入text(),方便开发者追溯AI的决策过程。 |
attributes() |
附加属性。 存储模型提供商(如OpenAI、Google)特有的额外信息,属于"非通用补充数据"。例如OpenAI可能会在attributes()中存入"回复的token消耗明细",Google可能存入"模型响应的置信度分数",这些属性仅对特定提供商的功能生效。 |
2.3 ToolExecutionResultMessage
ToolExecutionResultMessage是专门承载 Tools 执行结果的消息类型,核心作用是将ToolExecutionRequest(工具调用请求)的执行反馈传递给大语言模型(LLM),为后续对话或决策提供依据。
人话 :有些场景下我们 LLM 无法完成一些特别定制化的功能(比如请求查询业务数据库、调用API等工具),这时候我们可以提供给 LLM 一些 Tool 供其调用。ToolExecutionResultMessage 就是调用 Tool 后的返回结果。
- 关于 Tools 的使用详参 【LangChain4j 04】【Tools (Function Calling)】(尚未写完) 一文
2.4 SystemMessage
SystemMessage是来自 "系统" 的指令类消息,并非用户输入或 AI 回复,是对话的 "规则设定层",必须由开发者手动配置,而非终端用户(如使用 Chatbot 的普通用户),确保规则的可控性。
用于明确 LLM 在当前对话中的核心参数,比如 "你是医疗咨询助手,需用通俗语言解释医学知识"(角色)、"回答需基于最新临床指南,不夸大功效"(行为约束)、"回复控制在 300 字以内,分点说明"(风格 / 格式)。
LLM 在训练时已被赋予 "优先遵循 SystemMessage" 的逻辑,其指令权重高于UserMessage(用户消息)、AiMessage(AI 回复)等,是确保 LLM 不偏离预设方向的关键。
2.5 CustomMessage
CustomMessage 是一条可包含任意属性的自定义消息。此消息类型仅可被支持它的ChatModel实现使用(目前仅Ollama支持)。
五、UserMessage 多模态
UserMessage 不仅可以包含文本,还可以包含其他类型如图片,音频等内容。
多模态调用本身需要 LLM 自身支持,在使用的时候需要选择合适的模型。
UserMessage 包含一个 List<Content> contents,而Content 是一个接口,具有以下实现:
- TextContent : 文本内容
- ImageContent : 图片内容
- AudioContent :音频内容
- VideoContent :视频内容
- PdfFileContent : PDF 文件内容
下面是向 LLM 同时发送文本和图像的示例(其余几种类型与图片使用方式类似):
根据大语言模型提供商的不同,ImageContent 既可以通过远程图像的URL创建,也可以通过Base64编码的二进制数据创建
java
@Override
public AiMessage chatWithMultimodality(String message) throws IOException {
// 图片 转 base64
String base64Data = Base64.getEncoder().encodeToString(resource.getContentAsByteArray());
// 构建多模态消息
UserMessage userMessage = UserMessage.from(
// 文本消息
TextContent.from(message),
// 图片消息
ImageContent.from(base64Data, MimeTypeUtils.IMAGE_PNG_VALUE)
);
// 切换模型是因为 LangChain4j 官方给的apiKey token 长度不够,无法处理图片
return aliQwenModel.chat(userMessage).aiMessage();
}
六、Chat Memory
LangChain4j目前只提供"记忆",而非"历史"。如果需要保存完整的历史记录,需要单独处理。
- 历史记录会完整保留用户与 LLM 之间的所有消息。历史记录是用户在用户界面中看到的内容,它代表了实际所说的话
- 记忆会保存一些信息,这些信息会呈现给大语言模型,使其表现得仿佛"记住"了对话内容。记忆与历史记录有很大不同。根据所使用的记忆算法,它可以通过多种方式修改历史记录:移除一些消息、总结多条消息、分别总结不同消息、从消息中去除不重要的细节、在消息中注入额外信息(例如,用于检索增强生成)或指令(例如,用于结构化输出)等等。
大语言模型(LLM)本质上不会保留对话历史 ------ 每次调用时,它只 "看到" 当前输入的内容,无法记住上一轮对话的信息。因此,若要实现 "用户问'我叫什么名字',AI 能回答'克劳斯'" 这类多轮交互,必须主动将历史对话内容(包括之前的用户消息和 AI 回复) 一起传给模型,否则模型会 "失忆"。
在 langchain4j-hwl 中我们通过自己实现了历史数据的传输的功能,如下:
java
/**
* 历史记录的问答
*/
private final List<ChatMessage> messageHistory = new ArrayList<>();
@Override
public AiMessage chatWithHistory(String message) {
UserMessage userMessage = UserMessage.from(message);
// TODO : 可以将历史记录持久化到 DB 中
messageHistory.add(userMessage);
return chatModel.chat(messageHistory).aiMessage();
}
但手动维护历史消息存在明显问题:对话轮次越多,需传递的消息越多,代码繁琐且易出错。因此 LangChain4j 设计了 ChatMemory 组件,其核心作用是自动存储、管理对话历史,无需开发者每次手动拼接历史消息。
1. ChatMemory 的特性
在LangChain4j中,ChatMemory的核心作用是管理对话过程中的ChatMessage(由List数据结构存储),同时通过四大附加功能解决对话记忆管理的关键问题,如下:
-
驱逐策略(Eviction policy) :解决大语言模型(LLM)上下文窗口有限、调用成本与 latency 过高的问题:当对话消息量/令牌数超出LLM处理上限时,会按规则"移除"部分消息。LangChain4j提供两种现成实现------
MessageWindowChatMemory(保留最新N条消息,适合快速原型)和TokenWindowChatMemory(保留最新N个令牌,需借助TokenCountEstimator统计令牌,更精准适配上下文窗口)。 -
持久性(Persistence) :解决默认内存存储易丢失的问题:默认
ChatMemory将消息存在内存中,若需长期保存,可通过自定义ChatMemoryStore接口,将消息存储到数据库、文件等持久化介质中。核心通过getMessages(读取)、updateMessages(新增/更新)、deleteMessages(删除)方法实现,支持按memoryId区分不同用户/对话的消息。 -
对SystemMessage的特殊处理 :
SystemMessage是定义LLM行为的指令类消息,因此有专属规则:一旦添加便永久保留,且同一时间仅能存在1条;若添加相同内容的新SystemMessage会被忽略,添加不同内容则直接替换旧消息,确保LLM始终遵循最新且唯一的指令。 -
对 Tool 消息的特殊处理 :避免LLM请求报错:当包含
ToolExecutionRequest(工具调用请求)的AiMessage被驱逐时,后续关联的ToolExecutionResultMessage(工具执行结果)会被自动"连带驱逐"------因部分LLM提供商(如OpenAI)禁止发送无关联的"孤儿式"工具结果消息,该处理可规避请求异常。
在下文我们会详细说明介绍这几个特性。
2. Eviction policy
Eviction policy(驱逐策略)是LangChain4j中ChatMemory组件的关键功能,核心是解决对话过程中"消息过量"带来的问题,驱逐策略的3个核心必要性:
- 适配LLM上下文窗口限制:大语言模型(LLM)单次能处理的"令牌(Token)"数量有固定上限(即上下文窗口),随着对话推进,消息累积会超出该上限,此时必须移除部分消息才能让LLM继续处理,避免因令牌超量导致请求失败。
- 控制调用成本:LLM调用费用按令牌数量计费,消息越多、令牌数越多,单次调用成本越高。通过移除不必要的消息,可减少每次请求的令牌量,直接降低使用成本。
- 降低处理延迟:LLM处理速度与输入令牌数正相关------令牌越多,模型解析、生成响应的时间越长。驱逐冗余消息能减少输入令牌量,缩短响应延迟,提升用户体验。
LangChain4j提供两种开箱即用的实现方案,适配不同场景需求,如下:
| 特性 | MessageWindowChatMemory(消息窗口) | TokenWindowChatMemory(令牌窗口) |
|---|---|---|
| 核心逻辑 | 保留"最近N条消息",超出则移除 oldest 消息 | 保留"最近N个令牌",超出则移除 oldest 消息 |
| 消息处理方式 | 按"消息条数"计算,忽略单条消息的令牌量差异 | 按"令牌总数"计算,需精确统计每条消息的令牌数 |
| 依赖组件 | 无额外依赖 | 需搭配TokenCountEstimator(令牌计数器) |
| 适用场景 | 快速原型开发(无需精确控制令牌,追求简单) | 生产环境(需严格适配上下文窗口、控制成本) |
| 局限性 | 可能因单条长消息(多令牌)导致实际令牌超量 | 消息不可分割------若某条消息令牌量超剩余额度,需整段移除 |
3. Persistence
Persistence (持久性)是指将 ChatMessage(对话消息)持久化的特性。LangChain4j的ChatMemory实现(如MessageWindowChatMemory)默认将ChatMessage(对话消息)存在内存中 ,若需要长期保存对话(如用户下次访问仍能读取历史聊天),需手动实现dev.langchain4j.store.memory.chat.ChatMemoryStore接口,将消息存入持久化介质(如数据库、Redis、文件等)。
3.1 ChatMemoryStore
dev.langchain4j.store.memory.chat.ChatMemoryStore 定义如下:
java
public interface ChatMemoryStore {
List<ChatMessage> getMessages(Object var1);
void updateMessages(Object var1, List<ChatMessage> var2);
void deleteMessages(Object var1);
}
这里的三个方法具体说明如下:
| 方法名 | 触发时机 | 核心作用 |
|---|---|---|
getMessages(memoryId) |
每次与LLM交互时(需加载历史消息) | 根据memoryId(对话唯一标识),从持久化存储中读取该对话的所有消息 |
updateMessages(memoryId, messages) |
新增UserMessage/AiMessage时(各1次) |
全量更新该memoryId对应的消息列表(含新增/删除,如驱逐策略触发后同步删除) |
deleteMessages(memoryId) |
调用ChatMemory.clear()时 |
清空该memoryId对应的所有消息(无需此功能可留空) |
这里我们介绍几个地方:
memoryId的作用 :创建ChatMemory时指定,用于区分不同用户/不同对话,避免消息混淆(比如用户A和用户B的对话不会存在同一份存储中);- 存储格式灵活 :
ChatMessage可"单条存储"(每条消息对应1条记录)或"批量存储"(整个对话的消息打包成1条记录),LangChain4j提供ChatMessageSerializer/Deserializer工具类,支持JSON格式的序列化/反序列化,降低开发成本; - 驱逐策略同步 :若
ChatMemory因"上下文窗口限制"等触发消息驱逐,被移除的消息会通过updateMessages()同步从ChatMemoryStore中删除,确保内存与持久化存储的数据一致。
3.2 基于 Redis 的持久化案例
如下是自定义 RedisChatMemoryStore 存储方案的简单实现:
完整代码地址 :langchain4j-hwl
-
定义 RedisChatMemoryStore 持久化类
java@Component public class RedisChatMemoryStore implements ChatMemoryStore { public static final String CHAT_MEMORY_PREFIX = "CHAT_MEMORY:"; @Resource private RedisTemplate<String, String> redisTemplate; @Override public List<ChatMessage> getMessages(Object memoryId) { String retValue = redisTemplate.opsForValue().get(CHAT_MEMORY_PREFIX + memoryId); return ChatMessageDeserializer.messagesFromJson(retValue); } @Override public void updateMessages(Object memoryId, List<ChatMessage> messages) { redisTemplate.opsForValue() .set(CHAT_MEMORY_PREFIX + memoryId, ChatMessageSerializer.messagesToJson(messages)); } @Override public void deleteMessages(Object memoryId) { redisTemplate.delete(CHAT_MEMORY_PREFIX + memoryId); } } -
RedisChatMemoryStore 的使用有两种方式:
-
基于 low-level 的 ChatModel 的使用
javapublic final Map<String, ChatMemory> chatMemoryCache = new HashMap<>(); @Override public AiMessage chatWithLowLevelPersistentHistory(String memoryId, String message) { UserMessage userMessage = UserMessage.from(message); ChatMemory chatMemory = chatMemoryCache.computeIfAbsent(memoryId, mId -> MessageWindowChatMemory.builder() // 用户id 或者会话id .id(mId) // 最大消息数 .maxMessages(10) // 存储方式 .chatMemoryStore(redisChatMemoryStore) .build()); chatMemory.add(userMessage); return chatModel.chat(chatMemory.messages()).aiMessage(); } -
基于 high-level 的 AiServices 使用 RedisChatMemoryStore
-
声明代理接口
javapublic interface ChatPersistenceAssistant { String chat(@MemoryId String memoryId, @UserMessage String message); } -
容器中注入代理对象
java@Bean public ChatPersistenceAssistant chatMemoryAssistant(ChatModel chatModel) { ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(1000) .chatMemoryStore(redisChatMemoryStore) .build(); return AiServices.builder(ChatPersistenceAssistant.class) .chatModel(chatModel) .chatMemoryProvider(chatMemoryProvider) .build(); } -
直接发起调用
java@Override public String chatWithHighLevelPersistentHistory(String memoryId, String message) { return chatPersistenceAssistant.chat(memoryId, message); } -
-
4. 对 SystemMessage的特殊处理
在LangChain4j的ChatMemory机制中,SystemMessage 具有特殊处理逻辑,具体如下:
-
区别于 UserMessage、AiMessage 可能因"驱逐策略"(如超上下文窗口、控成本)被删除,
SystemMessage一旦添加就不会被清理。这是因为它通常承载LLM的核心行为指令(如"以专业程序员语气回答""输出格式为JSON"),需全程生效。 -
同一个 ChatMemory 实例在任意时间点都只会存在1 条有效的 SystemMessage,不会出现多条 SystemMessage 并存的情况。若允许同时存在多条SystemMessage,可能出现指令矛盾(如一条要求 "详细回答",另一条要求 "简洁回答"),导致 LLM 行为混乱。"单条持有" 的规则从根源上杜绝了这种冲突,让 LLM 始终只有一个明确的核心指令作为行动依据。
-
若重复添加完全相同的SystemMessage(如误操作重复调用接口),LLM 无需重复接收相同指令,忽略重复内容可避免不必要的消息存储占用,同时不影响对话逻辑的连贯性。
-
当业务需要调整 LLM 的行为(如从 "回答产品问题" 切换为 "处理售后咨询"),添加内容不同的新SystemMessage时,直接替换旧指令,既能快速更新 LLM 的行为准则,又无需手动清理历史指令,简化了开发操作。
5. Tool messages 的特殊处理
LangChain4j中 Tool messages 相关消息的联动清理规则 如下:
-
明确关联关系 :
AiMessage若包含ToolExecutionRequest(调用 Tool 的请求内容,比如让AI调用计算器、查天气的指令),后续会对应生成ToolExecutionResultMessage(Tool 执行后的结果反馈,比如计算器算出的答案)------二者是「请求-结果」的绑定关系。 -
清理逻辑 :当带有
ToolExecutionRequest的AiMessage因触发驱逐策略(如超出token限制)被移除时,原本关联它的ToolExecutionResultMessage会变成「孤立消息」(失去了对应的请求来源),此时系统会自动将这些孤立结果消息也移除。 -
解决的核心问题:部分LLM提供商(如OpenAI)的API有明确限制:不允许在请求中发送「无对应 Tool 请求」的孤立结果消息(可能导致模型理解混乱或请求报错)。这种联动清理正是为了遵守该规则,避免调用LLM时出现接口错误。
七、Model Parameters
根据实际选择的模型和提供商,在创建时可以调整许多参数,这些参数将定义:
- 模型的输出:生成内容(文本、图像)中的创造性或确定性水平,生成内容的数量等。
- 连接性:基础URL、授权密钥、超时设置、重试机制、日志记录等。
比较常见的参数如:
modelName:要使用的模型名称(例如,gpt-4o、gpt-4o-mini等)temperature:使用什么采样温度,范围在0到2之间。像0.8这样较高的值会使输出更具随机性,而像0.2这样较低的值会使输出更集中、更具确定性。maxTokens:聊天补全中可生成的最大令牌数。frequencyPenalty:介于-2.0到2.0之间的数字。正值会根据文本中到目前为止已有的标记出现频率对新标记进行惩罚,从而降低模型逐字重复相同内容的可能性。
在创建模型时,我们可以通过两种方式创建:
-
构造器方式创建,如下:
javaOpenAiChatModel model = OpenAiChatModel.builder() .apiKey(System.getenv("OPENAI_API_KEY")) .modelName("gpt-4o-mini") .temperature(0.3) .timeout(ofSeconds(60)) .logRequests(true) .logResponses(true) .build(); -
Spring Boot 环境, 如下:
- 引入langchain4j启动器
xml<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> </dependency>-
在application.properties文件中按如下方式配置模型参数:
propertieslangchain4j.open-ai.chat-model.api-key=${OPENAI_API_KEY} langchain4j.open-ai.chat-model.model-name=gpt-4-1106-preview ...
八、流式响应
LLM 生成文本时并非一次性输出完整内容,而是按「token」(可理解为文字片段,如单个单词、字或标点组合)逐段生成。 基于上述机制, LLM 提供「token 级流式响应」功能------生成一个/多个 token 就立即传输,而非等待所有 token 生成后再一次性返回。用户无需猜测完整响应的生成时长,可在首个 token 传输后立即开始阅读,减少等待焦虑,尤其适合长文本生成场景(如论文、长对话)。
1. StreamingChatResponseHandler
为适配流式响应,LangChain4j 在基础接口 ChatModel(对话模型)、LanguageModel(通用语言模型)之外,提供了专属的流式接口 StreamingChatModel、StreamingLanguageModel。 流式接口的核心调用逻辑与基础接口一致,降低开发者迁移成本,仅新增「流式传输」能力。
LangChain4j 流式响应的关键在于 StreamingChatResponseHandler 类,其 StreamingChatResponseHandler 定义如下:
java
public interface StreamingChatResponseHandler {
default void onPartialResponse(String partialResponse) {}
default void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) {}
default void onPartialThinking(PartialThinking partialThinking) {}
default void onPartialThinking(PartialThinking partialThinking, PartialThinkingContext context) {}
default void onPartialToolCall(PartialToolCall partialToolCall) {}
default void onPartialToolCall(PartialToolCall partialToolCall, PartialToolCallContext context) {}
default void onCompleteToolCall(CompleteToolCall completeToolCall) {}
void onCompleteResponse(ChatResponse completeResponse);
void onError(Throwable error);
}
StreamingChatResponseHandler 中每个事件对应 LLM 流式生成的一个关键节点,且每个事件都提供了"基础版"和"带上下文版"两种方法(开发者二选一实现即可,带上下文版能获取更多调用细节),具体如下:
| 事件场景 | 触发时机 | 核心作用 | 关键细节 |
|---|---|---|---|
| onPartialResponse | LLM 输出一段不完整的文本(如一句话中的几个词) | 实时展示内容(如 UI 逐字/逐句刷新) | 文本片段可能是 1 个或多个 Token,取决于 LLM 提供商(如 OpenAI、Anthropic 等) |
| onPartialThinking | LLM 输出"思考过程"(如 Agent 推理下一步操作时的中间逻辑) | 追踪 LLM 内部决策过程(如调试 Agent 行为) | 同样支持 Token 级流式输出,帮助开发者理解 LLM 的"思考逻辑" |
| onPartialToolCall | LLM 输出不完整的工具调用指令(如调用搜索工具时,先返回工具名、再返回参数) | 实时处理工具调用的中间步骤 | 适用于 LLM 需分步骤生成工具调用信息的场景(如复杂参数拼接) |
| onCompleteToolCall | LLM 完整输出一次工具调用的所有信息(不再有后续片段) | 确认工具调用指令已完整,触发工具执行 | 标志一次工具调用的"片段流"结束,可后续执行工具逻辑(如调用 API) |
| onCompleteResponse | LLM 输出所有内容(文本+思考+工具调用),流式过程结束 | 获取完整结果,做收尾处理(如存储完整响应、提示用户"已完成") | 返回的 ChatResponse 包含完整 AI 消息(AiMessage)和元数据(如生成耗时、Token 数) |
| onError | 流式过程中出现异常(如 API 调用失败、网络中断) | 捕获错误并处理(如提示用户"请求失败"、记录错误日志) | 通过 Throwable 参数获取错误详情,避免程序崩溃 |
如果需要取消取消流传输,可以通过 onPartialResponse、onPartialThinking、 onPartialToolCall 方法完成,如下:
java
model.chat(userMessage, new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) {
process(partialResponse);
if (shouldCancel()) {
// 取消流式传输
context.streamingHandle().cancel();
}
}
...
});
2. 示例
基于流式响应的方案有 low-level (通过 ChatModel)实现和 high-level (通过 Ai Service)两种 API 的实现方案,我们这里只看 ChatModel 实现方案, Ai Service 的实现在==【LangChain4j 02】【AI Services】==中有具体介绍。
-
注入流式响应模型 aliQwenStreamingChatModel
java@Bean("aliQwenStreamingChatModel") public OpenAiStreamingChatModel aliQwenStreamingChatModel() { return OpenAiStreamingChatModel .builder() .apiKey(System.getenv("aliQwen-api")) .modelName("qwen2.5-omni-7b") .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1") .build(); }博主自测如果使用 LangChain4j 自带的 OpenAiChatModel 会中文乱码,所以这里使用 千问模型
-
执行流式调用并返回
java@Override public Flux<String> streamChat(String message) { return Flux.create(emitter -> aliQwenStreamingChatModel.chat(message, new StreamingChatResponseHandler() { @Override public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) { emitter.next(partialResponse.text()); // 判断是否需要取消请求 // if (shouldCancel()) { // context.streamingHandle().cancel(); // } } @Override public void onCompleteResponse(ChatResponse chatResponse) { emitter.complete(); } @Override public void onError(Throwable throwable) { emitter.error(throwable); } })); }