主题
聊天与语言模型
NOTE
本页描述一个低层 LLM API。关于高层 LLM API,参见 AI 服务。
NOTE
所有受支持的 LLM 可在这里找到。
LLM 目前有两种 API 类型:
LanguageModel。它们的 API 非常简单——接受一个String作为输入,返回一个String作为输出。 该 API 正逐渐被聊天 API(第二种 API 类型)取代而过时。ChatModel。它们接受多个ChatMessage作为输入,返回单个AiMessage作为输出。ChatMessage通常包含文本,但一些 LLM 也支持其他模态(例如图像、音频等)。支持此类模态的聊天模型例子包括 OpenAI 的gpt-4o-mini与 Google 的gemini-1.5-pro。
LangChain4j 将不再扩展对 LanguageModel 的支持,因此在所有新功能中,我们都会使用 ChatModel API。
ChatModel 是 LangChain4j 中与 LLM 交互的低层 API,提供最强大的能力与灵活性。还有一个高层 API(AI 服务),我们会在讲完基础之后再介绍。
除 ChatModel 与 LanguageModel 外,LangChain4j 还支持以下类型的模型:
EmbeddingModel—— 该模型能把文本转换为一个Embedding。ImageModel—— 该模型能生成与编辑Image。ModerationModel—— 该模型能检查文本是否含有害内容。ScoringModel—— 该模型能对多段文本针对某个查询打分(或排序), 本质上决定每段文本与查询的相关度。这对 RAG 很有用。这些我们会在后面介绍。
现在,让我们更仔细地看看 ChatModel API。
java
public interface ChatModel {
String chat(String userMessage);
...
}如你所见,有一个简单的 chat 方法,它接受一个 String 作为输入、返回一个 String 作为输出,类似于 LanguageModel。这只是一个便捷方法,让你能快速轻松地试验,而无需把 String 包装成 UserMessage。
以下是其他的聊天 API 方法:
java
...
ChatResponse chat(ChatMessage... messages);
ChatResponse chat(List<ChatMessage> messages);
...这些版本的 chat 方法接受一个或多个 ChatMessage 作为输入。ChatMessage 是一个基础接口,代表一条聊天消息。下一节会更详细地介绍聊天消息。
如果你想定制请求(例如指定模型名、温度、工具、JSON schema 等),可以使用 chat(ChatRequest) 方法:
java
...
ChatResponse 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);ChatMessage 的类型
目前有五种聊天消息类型,分别对应消息的一种"来源":
UserMessage:这是来自用户的消息。用户可以是你的应用的终端用户(人类),也可以是你的应用本身。它可以包含:contents():消息的内容。取决于 LLM 支持的模态,它可以只包含一段文本(String),或其他模态。name():用户的名称。并非所有模型提供者都支持它。attributes():额外的属性:这些属性不会发送给模型,但会存储在ChatMemory中。
AiMessage:这是 AI 生成的、作为对所发消息响应的一条消息。它可以包含:text():文本内容thinking():思考/推理内容toolExecutionRequests():执行工具的请求。我们会在另一节探讨工具。attributes():额外属性,通常为提供者专有
ToolExecutionResultMessage:这是ToolExecutionRequest的结果。SystemMessage:这是来自系统的消息。 通常,你作为开发者应当定义这条消息的内容。一般而言,你会在这里写明 LLM 在这场对话中的角色是什么、它应如何表现、以什么风格作答等。LLM 被训练成对SystemMessage比对其他类型消息投入更多注意力,所以要谨慎,最好不要给终端用户自由定义或向SystemMessage注入某些输入的权限。它通常位于对话的开头。它可以包含:text():消息文本。attributes():额外的属性:这些属性不会发送给模型,但会存储在ChatMemory中。
CustomMessage:这是一条可包含任意属性的自定义消息。此消息类型只能由支持它的ChatModel实现使用(目前仅 Ollama)。
既然我们已了解所有 ChatMessage 类型,接下来看看如何在对话中组合它们。
在最简单的场景中,我们可以向 chat 方法提供一个 UserMessage 实例。这类似于 chat 方法接受 String 作为输入的第一个版本。主要区别在于,它现在返回的不是 String,而是 ChatResponse。除 AiMessage 外,ChatResponse 还包含 ChatResponseMetadata。ChatResponseMetadata 包含 TokenUsage,其中有统计信息:输入(你传给 generate 方法的所有 ChatMessage)包含多少词元、作为输出(在 AiMessage 中)生成了多少词元,以及总数(输入 + 输出)。你会需要这些信息来计算一次对 LLM 的调用花费多少。此外,ChatResponseMetadata 还包含 FinishReason,它是一个枚举,列出了生成停止的各种原因。通常,如果 LLM 自行决定停止生成,它会是 FinishReason.STOP。
有多种方式可以创建 UserMessage,取决于内容。最简单的是 new UserMessage("Hi") 或 UserMessage.from("Hi")。
多个 ChatMessage
那么,为什么需要提供一个以上的 ChatMessage 作为输入,而不只是单个?这是因为 LLM 天生是无状态的,意味着它们不维护对话的状态。因此,如果你想支持多轮对话,就需要自行管理对话的状态。
假设你想构建一个聊天机器人。设想一个用户与聊天机器人(AI)之间的简单多轮对话:
- User: Hello, my name is Klaus
- AI: Hi Klaus, how can I help you?
- User: What is my name?
- AI: Klaus
与 ChatModel 的交互将如下所示:
java
UserMessage firstUserMessage = UserMessage.from("Hello, my name is Klaus");
AiMessage firstAiMessage = model.chat(firstUserMessage).aiMessage(); // Hi Klaus, how can I help you?
UserMessage secondUserMessage = UserMessage.from("What is my name?");
AiMessage secondAiMessage = model.chat(firstUserMessage, firstAiMessage, secondUserMessage).aiMessage(); // Klaus如你所见,在第二次调用 chat 方法时,我们不仅提供一个 secondUserMessage,还提供了对话中之前的消息。
手动维护与管理这些消息很繁琐。因此,存在 ChatMemory 这个概念,我们会在下一节中探讨。
多模态
UserMessage 不仅能包含文本,还能包含其他类型的内容。UserMessage 包含一个 List<Content> contents。Content 是一个接口,有以下实现:
TextContentImageContentAudioContentVideoContentPdfFileContent
你可以在这里的比较表中查看哪些 LLM 提供者支持哪些模态。
下面是一个同时向 LLM 发送文本与图像的示例:
java
UserMessage userMessage = UserMessage.from(
TextContent.from("Describe the following image"),
ImageContent.from("https://example.com/cat.jpg")
);
ChatResponse response = model.chat(userMessage);文本内容
TextContent 是 Content 最简单的形式,表示纯文本并包装一个 String。UserMessage.from(TextContent.from("Hello!")) 等价于 UserMessage.from("Hello!")。
你可以在 UserMessage 中提供一个或多个 TextContent:
java
UserMessage userMessage = UserMessage.from(
TextContent.from("Hello!"),
TextContent.from("How are you?")
);图像内容
取决于 LLM 提供者,ImageContent 既可以由远程图像的 URL 创建(见上文示例),也可以由 Base64 编码的二进制数据创建:
java
byte[] imageBytes = readBytes("/home/me/cat.jpg");
String base64Data = Base64.getEncoder().encodeToString(imageBytes);
ImageContent imageContent = ImageContent.from(base64Data, "image/jpg");
UserMessage userMessage = UserMessage.from(imageContent);你还可以指定 DetailLevel 枚举(含 LOW/HIGH/AUTO 选项)来控制模型如何处理图像。更多细节见这里。
音频内容
AudioContent 与 ImageContent 类似,但表示音频内容。
视频内容
VideoContent 与 ImageContent 类似,但表示视频内容。
PDF 文件内容
PdfFileContent 与 ImageContent 类似,但表示一个 PDF 文件的二进制内容。
非阻塞调用
NOTE
实验性。该 API 标注了 @Experimental,未来版本可能变化。
ChatModel 也能在不阻塞调用线程的情况下作答:
java
CompletableFuture<ChatResponse> future = model.chatAsync(chatRequest);与 chat(...) 一样,也有相同的便捷重载,例如 CompletableFuture<String> chatAsync(String userMessage)。
取消该 future 会释放调用方,并尽力尝试中止正在进行的 HTTP 调用。该 future 在模型的传输线程上完成,因此未显式指定执行器而附加的续接(continuation)会在该线程上运行——请保持它非阻塞,或使用 thenApplyAsync(fn, executor)。
尚未实现非阻塞路径的提供者会以 AsyncNotSupportedException 响亮地失败(由返回的 future 携带),而不是悄悄阻塞一个线程。
关于全貌(包括流式与 AI 服务),参见非阻塞与响应式。
Kotlin 扩展
WARNING
针对 Kotlin 用户的破坏性变更。 ChatModel 现已声明一个 Java 成员 chatAsync(ChatRequest): CompletableFuture<ChatResponse>。在 Kotlin 中,签名相同的成员会优先于扩展函数,因此原先解析到 suspend 扩展(返回 ChatResponse)的单个参数的裸调用,现在会解析到该成员(返回 CompletableFuture<ChatResponse>):
kotlin
// Before - resolved to the suspend extension, returned ChatResponse:
val response: ChatResponse = model.chatAsync(request)
// After - the bare single-arg call resolves to the Java member (a CompletableFuture):
val future: CompletableFuture<ChatResponse> = model.chatAsync(request)
// Migration - await the future...
val response: ChatResponse = model.chatAsync(request).await()
// ...or supply a coroutineContext, which still selects the suspend extension:
val response: ChatResponse = model.chatAsync(request, Dispatchers.IO)ChatModel 的 Kotlin 扩展提供了利用 Kotlin 协程能力、处理与语言模型聊天交互的异步方法。chatAsync 方法允许对 ChatRequest 或 ChatRequest.Builder 配置进行非阻塞处理,返回带有模型答复的 ChatResponse。类似地,generateAsync 处理从聊天消息异步生成响应。这些扩展简化了在 Kotlin 应用中构建聊天请求并高效处理对话。请注意,这些方法被标记为实验性,可能会随时间演变。
ChatModel.chatAsync(request: ChatRequest):为 Kotlin 协程而设计,这个异步扩展函数用 Dispatchers.IO 在一个协程作用域内包装了同步的 chat 方法。这实现了非阻塞操作,对保持应用响应性至关重要。它被命名为 chatAsync 正是为了避免与已存在的同步 chat 冲突。其函数签名为:suspend fun ChatModel.chatAsync(request: ChatRequest): ChatResponse。关键字 suspend 将它标识为一个协程函数。
ChatModel.chat(block: ChatRequestBuilder.() -> Unit):这个 chat 变体通过使用 Kotlin 的类型安全构建器 DSL 提供了一种更简洁的方式。它简化了 ChatRequest 对象的构建,同时在内部使用 chatAsync 进行异步执行。这个版本借助协程同时提供了简洁性与非阻塞行为。