九、内容安全与合规:审核组件
前面已经掌握了构建智能知识库的核心技能。但是,当你的 AI 应用走向生产环境时,有一个重要问题必须考虑:内容安全。
AI 模型可能生成不合适的内容(例如暴力、色情、仇恨言论),也可能被用户恶意利用来诱导模型输出有害信息。因此,为你的 AI 应用加上"护栏"------内容审核机制,是至关重要的。
LangChain4j 提供了专门的审核组件,帮助你自动检测和过滤不安全的内容。本章将带你学习如何使用这些组件,确保你的 AI 应用安全合规。
9.1 ModerationModel 的作用
ModerationModel(审核模型)是一种特殊的模型,专门用于判断一段文本是否包含不安全内容。它通常返回一个分类结果,例如文本是否涉及仇恨言论、自残、暴力等。
LangChain4j 内置了对 OpenAI 审核模型的支持(也可以扩展其他服务商)。OpenAI 的审核模型可以识别以下类别:
hate:仇恨言论hate/threatening:仇恨言论且带有威胁self-harm:自残相关内容sexual:色情内容sexual/minors:涉及未成年人的色情内容violence:暴力内容violence/graphic:血腥暴力内容
你可以用审核模型在用户输入 和AI输出两个阶段进行检查:
- 预审核:在用户消息发送给 AI 之前,先检查是否违规。如果违规,可以拒绝回答或给出警告。
- 后审核:在 AI 生成回答之后,检查回答内容是否安全。如果不安全,可以屏蔽回答或返回默认消息。
9.2 使用 OpenAiModerationModel 进行内容审核
LangChain4j 提供了 OpenAiModerationModel 类,用于调用 OpenAI 的审核接口。它需要 API 密钥(可以使用与聊天模型相同的密钥)。
9.2.1 引入依赖
如果你还没有添加 OpenAI 的依赖,请确保项目中有:
xml
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
9.2.2 创建审核模型实例
java
import dev.langchain4j.model.moderation.ModerationModel;
import dev.langchain4j.model.openai.OpenAiModerationModel;
ModerationModel moderationModel = OpenAiModerationModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1") // 使用演示端点
.apiKey("demo")
.build();
注意:OpenAI 的审核接口通常不需要单独计费,但使用演示端点可能有限制。生产环境建议使用真实 OpenAI API 密钥。
9.2.3 审核一段文本
审核模型返回一个 Moderation 对象,包含是否违规以及详细的分类结果。
java
import dev.langchain4j.model.moderation.Moderation;
String userInput = "我想自杀,活着没意思。";
Moderation moderation = moderationModel.moderate(userInput).content();
if (moderation.flagged()) {
System.out.println("⚠️ 检测到违规内容!");
System.out.println("违规类别:" + moderation.categories());
System.out.println("分数:" + moderation.categoryScores());
} else {
System.out.println("✅ 内容安全,可继续处理。");
}
运行后,如果输入包含自残倾向,flagged() 会返回 true,我们可以拒绝该请求。
9.2.4 对用户输入进行预审核
在实际应用中,我们可以在调用 AI 之前先审核用户消息:
java
String userMessage = "我要骂人,你是笨蛋!";
Moderation moderation = moderationModel.moderate(userMessage).content();
if (moderation.flagged()) {
System.out.println("对不起,您的消息包含不当内容,请文明交流。");
} else {
// 安全,继续调用 AI
String answer = assistant.chat(userMessage);
System.out.println("AI: " + answer);
}
9.2.5 对 AI 输出进行后审核
同样,我们可以在 AI 返回回答后,再审核一次:
java
String answer = assistant.chat(userMessage);
Moderation outputModeration = moderationModel.moderate(answer).content();
if (outputModeration.flagged()) {
System.out.println("AI 生成的内容可能存在风险,已被拦截。");
} else {
System.out.println("AI: " + answer);
}
9.3 声明式审核:@Moderate 注解
手动调用审核模型虽然灵活,但略显繁琐。LangChain4j 提供了更优雅的方式:@Moderate 注解。你可以在 AI Service 的方法上添加此注解,框架会自动对用户输入和 AI 输出进行审核。
9.3.1 基本用法
java
import dev.langchain4j.service.Moderate;
interface SafeAssistant {
@Moderate
String chat(String userMessage);
}
然后构建 AI Service 时,需要传入审核模型:
java
SafeAssistant assistant = AiServices.builder(SafeAssistant.class)
.chatLanguageModel(chatModel)
.moderationModel(moderationModel) // 注入审核模型
.build();
String response = assistant.chat("你好"); // 自动审核输入和输出
当用户输入或 AI 输出触发审核时,框架会抛出 ModerationException 异常。你可以捕获该异常并处理。
9.3.2 处理审核异常
java
try {
String response = assistant.chat("我想自杀");
System.out.println(response);
} catch (dev.langchain4j.model.moderation.ModerationException e) {
System.out.println("内容不合规:" + e.getMessage());
}
9.3.3 自定义审核行为
默认情况下,@Moderate 会同时审核用户输入和 AI 输出。如果需要只审核输入或只审核输出,可以配置 ModerationModel 的行为,但目前注解本身不支持细分。如果你需要精细控制,可以结合手动审核。
9.4 实践:给聊天助手加上审核功能
现在我们把审核功能集成到之前的多用户聊天助手中,实现一个安全合规的客服系统。
9.4.1 定义带审核的接口
java
interface SafeCustomerService {
@SystemMessage("你是一个友好的客服助手。")
@Moderate
String chat(@MemoryId String userId, String userMessage);
}
9.4.2 构建 AI Service 并处理审核异常
java
import dev.langchain4j.model.moderation.ModerationException;
public class SafeChatDemo {
public static void main(String[] args) {
// 创建模型
OpenAiChatModel chatModel = OpenAiChatModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.apiKey("demo")
.build();
// 创建审核模型
OpenAiModerationModel moderationModel = OpenAiModerationModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.apiKey("demo")
.build();
// 创建记忆提供者
var memories = new ConcurrentHashMap<String, ChatMemory>();
ChatMemoryProvider memoryProvider = memoryId ->
memories.computeIfAbsent((String) memoryId, id ->
MessageWindowChatMemory.builder().id(id).maxMessages(10).build());
// 构建 AI Service
SafeCustomerService assistant = AiServices.builder(SafeCustomerService.class)
.chatLanguageModel(chatModel)
.moderationModel(moderationModel)
.chatMemoryProvider(memoryProvider)
.build();
// 模拟用户对话
Scanner scanner = new Scanner(System.in);
System.out.println("安全客服助手启动(输入 'exit' 退出)");
while (true) {
System.out.print("用户ID: ");
String userId = scanner.nextLine().trim();
if ("exit".equalsIgnoreCase(userId)) break;
System.out.print("消息: ");
String message = scanner.nextLine();
if ("exit".equalsIgnoreCase(message)) break;
try {
String response = assistant.chat(userId, message);
System.out.println("AI: " + response);
} catch (ModerationException e) {
System.out.println("⚠️ 内容不合规,已被拦截。原因:" + e.getMessage());
}
System.out.println();
}
scanner.close();
}
}
9.4.3 测试
尝试输入正常内容,如"你好",AI 会正常回复。 尝试输入敏感内容,如"我要自杀",程序会捕获异常并输出提示信息。
流程图:带审核的对话流程
9.5 本章小结
- 审核模型的作用:识别不当内容,保护应用安全。
- OpenAiModerationModel 的使用:手动审核文本,对输入输出分别检查。
- @Moderate 注解:声明式审核,自动拦截不安全内容。
- 实践:为聊天助手添加审核功能,捕获异常并友好提示。
十、多模态探索:处理图片与文件
到目前为止,我们所有的交互都是基于文本的。但现实世界的信息是多样的------图片、PDF、音频、视频......大语言模型正在向多模态进化,能够理解和生成非文本内容。LangChain4j 也紧跟潮流,提供了初步的多模态支持。
本章将带你探索如何让 AI "看懂"图片,以及如何处理 PDF 等文件。虽然目前多模态功能还在快速发展中,但掌握基础用法可以为你打开更多应用场景的大门。
10.1 LangChain4j 对多模态的支持现状
目前,LangChain4j 对多模态的支持主要体现在以下几个方面:
- 图像输入 :可以通过
ImageContent将图片作为消息的一部分发送给支持多模态的模型(如 OpenAI 的 GPT-4 Vision、Google 的 Gemini Pro Vision 等)。 - 文件输入 :某些模型支持接收 PDF、音频、视频等文件,LangChain4j 提供了对应的
PdfFileContent、AudioFileContent、VideoFileContent等类。 - 多模态输出:部分模型可以生成图像,但 LangChain4j 尚未直接封装,仍需要通过原始 API 调用。
需要注意的是,多模态功能需要模型本身支持 。本章示例将使用 OpenAI 的 GPT-4 Vision 模型(即 gpt-4-vision-preview 或更新版本)。由于演示端点可能不支持多模态,你需要使用真实的 OpenAI API 密钥。
10.2 发送包含图片的消息(ImageContent)
要让 AI 描述一张图片,我们需要将图片以 ImageContent 的形式嵌入到用户消息中。图片可以来自 URL 或本地文件。
10.2.1 准备依赖
确保你的项目中已经包含了 OpenAI 集成(用于支持 GPT-4 Vision)和必要的图片处理库(如果需要从本地文件加载)。如果你使用 Maven:
xml
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
10.2.2 创建支持多模态的模型
我们需要创建一个 OpenAiChatModel 实例,并指定支持视觉的模型名称(如 gpt-4-turbo 或 gpt-4-vision-preview)。同时,需要提供真实的 API 密钥(演示端点不支持多模态)。
java
OpenAiChatModel visionModel = OpenAiChatModel.builder()
.apiKey("你的真实OpenAI密钥") // 需要替换为真实密钥
.modelName("gpt-4-turbo") // 或 "gpt-4-vision-preview"
.build();
注意 :
gpt-4-vision-preview是早期的视觉模型版本,现在通常使用gpt-4-turbo(它内置了视觉能力)。请参考 OpenAI 官方文档确认当前可用的视觉模型名称。
10.2.3 构造包含图片的用户消息
我们使用 UserMessage.from(ImageContent) 或 UserMessage.from(TextContent, ImageContent) 来创建包含图片的消息。
java
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;
// 方式1:只有图片,没有文字
UserMessage messageWithImage = UserMessage.from(ImageContent.from("https://example.com/cat.jpg"));
// 方式2:文字 + 图片
UserMessage messageWithTextAndImage = UserMessage.from(
TextContent.from("请描述这张图片:"),
ImageContent.from("https://example.com/cat.jpg")
);
ImageContent.from() 可以接受:
- 图片的 URL(字符串)
- 本地图片文件的路径(会读取并 Base64 编码)
- 直接传入
Image对象(需要手动加载)
对于本地图片,可以这样:
java
import java.nio.file.Paths;
ImageContent localImage = ImageContent.from(Paths.get("/path/to/local/image.jpg"));
10.2.4 调用模型获取描述
将构造好的消息传入模型,获取回复。
java
UserMessage userMessage = UserMessage.from(
TextContent.from("这张图片里有什么?"),
ImageContent.from("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg")
);
List<ChatMessage> messages = List.of(userMessage);
String response = visionModel.chat(messages);
System.out.println("AI 描述:\n" + response);
10.2.5 完整示例:图片描述助手
创建一个类 ImageDescriptionDemo.java:
java
package com.example;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.openai.OpenAiChatModel;
import java.util.List;
public class ImageDescriptionDemo {
public static void main(String[] args) {
// 使用真实的 OpenAI 密钥(演示端点不支持视觉)
OpenAiChatModel visionModel = OpenAiChatModel.builder()
.apiKey("sk-...") // 替换为你的密钥
.modelName("gpt-4-turbo")
.build();
// 构造带图片的消息
UserMessage userMessage = UserMessage.from(
TextContent.from("请详细描述这张图片的内容,包括动物、环境、颜色等。"),
ImageContent.from("https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg")
);
// 调用模型
String response = visionModel.chat(List.of(userMessage));
System.out.println("AI 描述:\n" + response);
}
}
运行后,AI 会返回对猫图片的描述。如果图片 URL 不可用,可以替换为其他公开图片。
10.3 处理 PDF 等文件
除了图片,LangChain4j 还支持将 PDF 文件作为消息发送给某些模型(例如 GPT-4 的 file-search 功能)。但需要注意的是,模型本身通常不直接"看懂" PDF,而是将其内容(文本)提取后作为上下文。因此,更常见的做法是先用文档加载器提取 PDF 文本,然后作为普通文本发送。
不过,LangChain4j 提供了 PdfFileContent 类,用于表示 PDF 文件内容。如果你的模型支持直接处理 PDF(如 OpenAI 的 Assistants API),可以这样使用。
10.3.1 使用 PdfFileContent
java
import dev.langchain4j.data.message.PdfFileContent;
import dev.langchain4j.data.message.UserMessage;
import java.nio.file.Paths;
PdfFileContent pdfContent = PdfFileContent.from(Paths.get("/path/to/document.pdf"));
UserMessage userMessage = UserMessage.from(
TextContent.from("请总结这个 PDF 文件的内容:"),
pdfContent
);
然而,目前标准的 OpenAiChatModel 并不直接支持 PDF 文件输入(除非通过 Assistants API)。因此,这段代码可能无法直接工作,需要配合特定的模型实现。对于初学者,我们推荐使用传统方式:先用文档加载器提取 PDF 文本,再作为文本消息发送。
10.3.2 提取 PDF 文本后发送(推荐方式)
你需要添加 PDF 解析库,如 Apache PDFBox 或 LangChain4j 内置的 ApachePdfDocumentParser(需要额外依赖)。
首先,添加依赖:
xml
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
<version>${langchain4j.version}</version>
</dependency>
然后使用 ApachePdfDocumentParser 提取文本:
java
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser;
import java.nio.file.Paths;
ApachePdfBoxDocumentParser parser = new ApachePdfBoxDocumentParser();
Document pdfDocument = parser.parse(Paths.get("/path/to/document.pdf"));
String pdfText = pdfDocument.text();
// 然后将 pdfText 作为普通文本发送
UserMessage userMessage = UserMessage.from("请总结以下内容:" + pdfText);
String summary = visionModel.chat(List.of(userMessage));
System.out.println(summary);
这种方式虽然简单,但受限于模型上下文窗口,不适合非常长的 PDF。对于长文档,应该采用 RAG 方式(分割、检索、生成),这在前面章节已经介绍过。
10.4 实践:让 AI 描述一张图片的内容
为了巩固所学,我们做一个完整的多模态实践:编写一个程序,让用户输入图片路径或 URL,AI 返回对图片的描述。
10.4.1 代码实现
创建 InteractiveImageDescriber.java:
java
package com.example;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.openai.OpenAiChatModel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Scanner;
public class InteractiveImageDescriber {
public static void main(String[] args) {
// 配置模型(使用真实密钥)
OpenAiChatModel visionModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY")) // 从环境变量读取密钥
.modelName("gpt-4-turbo")
.build();
Scanner scanner = new Scanner(System.in);
System.out.println("图片描述助手启动。请输入图片路径或URL(输入 'exit' 退出):");
while (true) {
System.out.print("> ");
String input = scanner.nextLine().trim();
if ("exit".equalsIgnoreCase(input)) {
break;
}
try {
// 判断是 URL 还是本地文件
ImageContent imageContent;
if (input.startsWith("http://") || input.startsWith("https://")) {
imageContent = ImageContent.from(input);
} else {
// 假设是本地文件路径
Path path = Paths.get(input);
imageContent = ImageContent.from(path);
}
UserMessage userMessage = UserMessage.from(
TextContent.from("请用中文详细描述这张图片的内容,包括主体、颜色、动作、背景等。"),
imageContent
);
System.out.println("正在分析图片,请稍候...");
String description = visionModel.chat(List.of(userMessage));
System.out.println("AI描述:\n" + description);
} catch (Exception e) {
System.err.println("处理图片时出错:" + e.getMessage());
}
System.out.println();
}
scanner.close();
}
}
10.4.2 运行说明
- 设置环境变量
OPENAI_API_KEY为你的真实 OpenAI 密钥。 - 运行程序。
- 输入一张图片的 URL(例如
https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg)或本地图片的绝对路径。 - 等待 AI 返回描述。
示例输出:
ruby
> https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg
正在分析图片,请稍候...
AI描述:
这张图片中是一只可爱的虎斑猫。它有着黄绿色的眼睛和粉色的鼻子,毛色以棕色、黑色和白色相间,形成典型的虎斑纹。猫咪正坐在一个浅色的木质地板上,背景是模糊的室内环境,可以看到一些家具和植物。它的姿态放松,耳朵竖起,目光直视镜头,显得非常好奇和警觉。整体画面色调温暖,突出了猫咪的可爱和灵动。
10.4.3 注意事项
- 模型支持:确保你使用的 OpenAI 账户有权限访问 GPT-4 Vision 模型。某些旧账户可能需要单独开通。
- 图片大小:图片过大会消耗更多 Token,甚至超出限制。LangChain4j 会自动压缩图片吗?不会。你需要自行控制图片大小,或使用 URL 形式(模型通常会下载并处理)。
- 成本:视觉模型调用成本高于纯文本模型,请留意 Token 消耗。
10.5 本章小结
- 图像输入 :使用
ImageContent将图片发送给支持视觉的模型,让 AI 描述图片内容。 - 文件处理 :了解了
PdfFileContent的存在,并掌握了用传统方式提取 PDF 文本后发送。 - 实践:编写了交互式图片描述程序,体验了多模态交互的魅力。
多模态是 AI 发展的重要方向,LangChain4j 正在不断丰富这方面的支持。未来,你可能可以用它构建看图说话、图像问答、文档分析等更强大的应用。
十一、监控与扩展:事件监听
随着 AI 应用逐渐复杂,你可能会想知道:
- 每次请求花了多长时间?
- 消耗了多少 Token?(关系到成本)
- 如果出错了,错误信息是什么?
- 能不能记录所有请求和响应,用于调试或分析?
这些需求都可以通过 事件监听器(Listener) 来实现。LangChain4j 提供了 AiServiceListener 接口,允许你在 AI Service 调用的各个阶段插入自定义逻辑,比如日志记录、指标收集、耗时统计等。
本章将带你掌握监听器的使用,让你的应用具备可观测性。
11.1 为什么需要监听?------ 日志、监控、追踪
想象一下,你的 AI 助手已经上线,每天有成千上万用户使用。这时,你可能面临以下问题:
- 性能问题:某个用户的请求特别慢,是网络问题还是模型问题?
- 成本失控:Token 消耗突然飙升,是哪个用户、哪个问题导致的?
- 调试困难:用户反馈 AI 回答错误,但无法复现当时的上下文。
- 安全审计:需要记录所有用户输入和 AI 输出,以备合规检查。
监听器正是为了解决这些问题而生。它像"摄像头"一样,记录下每次调用的详细信息。
监听器工作示意图:
请求开始] L2[onResponse
成功响应] L3[onError
出错] end A -.-> L1 C -.-> L2 C -.-> L3 D -.-> L2
11.2 实现 AiServiceListener 接口
AiServiceListener 接口定义在 dev.langchain4j.service 包中,包含以下主要方法:
java
public interface AiServiceListener {
default void onRequest(AiServiceRequest request) { }
default void onResponse(AiServiceResponse response) { }
default void onError(AiServiceError error) { }
}
onRequest:在请求发送给模型之前调用。你可以在这里获取用户消息、系统消息、工具定义等信息。onResponse:在成功收到模型响应后调用。你可以获取模型回复、Token 用量、检索到的文档等。onError:在调用过程中发生异常时调用(包括网络错误、模型返回错误、审核拦截等)。
每个回调方法都提供了对应的上下文对象,包含丰富的信息。
11.2.1 自定义监听器示例
创建一个简单的监听器,打印日志:
java
import dev.langchain4j.service.AiServiceListener;
import dev.langchain4j.service.AiServiceRequest;
import dev.langchain4j.service.AiServiceResponse;
import dev.langchain4j.service.AiServiceError;
public class LoggingListener implements AiServiceListener {
@Override
public void onRequest(AiServiceRequest request) {
System.out.println(">>> 请求开始");
System.out.println("用户消息: " + request.userMessage());
System.out.println("系统消息: " + request.systemMessage());
System.out.println("记忆中的消息数: " + request.memory().messages().size());
}
@Override
public void onResponse(AiServiceResponse response) {
System.out.println("<<< 请求成功");
System.out.println("AI回复: " + response.aiMessage().text());
System.out.println("Token用量: " + response.tokenUsage());
System.out.println("检索到的文档: " + response.sources());
}
@Override
public void onError(AiServiceError error) {
System.err.println("!!! 请求出错");
System.err.println("错误信息: " + error.message());
System.err.println("异常: " + error.error());
}
}
11.3 注册监听器到 AI Service
监听器需要通过 AiServices 的 withListeners() 方法注册。可以注册一个或多个监听器。
java
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(model)
.withListeners(new LoggingListener()) // 注册监听器
.build();
如果要注册多个监听器,可以传入多个:
java
.withListeners(listener1, listener2, listener3)
监听器会按照传入顺序依次调用。
11.4 实践:记录每次请求的耗时和 Token 用量
让我们构建一个实用的监听器,它可以:
- 记录每次请求的耗时(从
onRequest到onResponse/onError) - 记录 Token 用量(输入、输出、总计)
- 将日志写入文件或控制台,便于后续分析。
11.4.1 定义带记忆的 AI Service 接口
为了演示更真实的场景,我们使用一个带记忆的助手接口。
java
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
interface MonitoredAssistant {
@SystemMessage("你是一个乐于助人的助手。")
String chat(@MemoryId int userId, String message);
}
11.4.2 实现监控监听器
我们将实现一个监听器,它使用 ThreadLocal 或实例变量来记录开始时间,但要注意监听器实例是单例的,多个请求会并发执行,因此需要确保线程安全。这里我们使用 ThreadLocal 存储每个请求的开始时间。
java
import dev.langchain4j.service.*;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
public class MonitoringListener implements AiServiceListener {
// 线程安全的 Map,存储每个请求的开始时间
private final ThreadLocal<Instant> startTime = new ThreadLocal<>();
@Override
public void onRequest(AiServiceRequest request) {
startTime.set(Instant.now());
System.out.println("[监控] 请求开始于: " + startTime.get());
System.out.println("[监控] 用户ID: " + request.memoryId());
System.out.println("[监控] 用户消息: " + request.userMessage().singleText());
}
@Override
public void onResponse(AiServiceResponse response) {
Instant end = Instant.now();
Instant start = startTime.get();
Duration duration = Duration.between(start, end);
startTime.remove(); // 清理
System.out.println("[监控] 请求成功,耗时: " + duration.toMillis() + " ms");
if (response.tokenUsage() != null) {
System.out.println("[监控] Token用量 - 输入: " + response.tokenUsage().inputTokenCount()
+ ", 输出: " + response.tokenUsage().outputTokenCount()
+ ", 总计: " + response.tokenUsage().totalTokenCount());
}
if (response.sources() != null && !response.sources().isEmpty()) {
System.out.println("[监控] 引用文档数: " + response.sources().size());
}
}
@Override
public void onError(AiServiceError error) {
Instant end = Instant.now();
Instant start = startTime.get();
Duration duration = Duration.between(start, end);
startTime.remove();
System.err.println("[监控] 请求失败,耗时: " + duration.toMillis() + " ms");
System.err.println("[监控] 错误: " + error.message());
}
}
11.4.3 构建并测试
创建主类 MonitoringDemo.java:
java
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;
public class MonitoringDemo {
public static void main(String[] args) {
// 模型
OpenAiChatModel model = OpenAiChatModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.apiKey("demo")
.build();
// 记忆提供者
var memories = new ConcurrentHashMap<Object, dev.langchain4j.memory.ChatMemory>();
var memoryProvider = (dev.langchain4j.memory.ChatMemoryProvider) memoryId ->
memories.computeIfAbsent(memoryId, id ->
MessageWindowChatMemory.builder().id(id).maxMessages(10).build());
// 创建监听器
MonitoringListener listener = new MonitoringListener();
// 构建 AI Service
MonitoredAssistant assistant = AiServices.builder(MonitoredAssistant.class)
.chatLanguageModel(model)
.chatMemoryProvider(memoryProvider)
.withListeners(listener)
.build();
// 模拟用户对话
Scanner scanner = new Scanner(System.in);
System.out.println("监控助手启动(输入用户ID和消息,如 '1001 你好')");
System.out.println("输入 'exit' 退出");
while (true) {
System.out.print("> ");
String input = scanner.nextLine();
if ("exit".equalsIgnoreCase(input)) break;
String[] parts = input.split(" ", 2);
if (parts.length < 2) {
System.out.println("格式错误,请使用 '用户ID 消息'");
continue;
}
int userId = Integer.parseInt(parts[0]);
String message = parts[1];
try {
String response = assistant.chat(userId, message);
System.out.println("AI: " + response);
} catch (Exception e) {
System.err.println("发生异常: " + e.getMessage());
}
System.out.println();
}
scanner.close();
}
}
11.4.4 运行观察
运行程序,输入类似 1 你好,你会看到类似以下的监控输出:
ini
[监控] 请求开始于: 2025-04-07T10:15:30.123Z
[监控] 用户ID: 1
[监控] 用户消息: 你好
[监控] 请求成功,耗时: 2345 ms
[监控] Token用量 - 输入: 50, 输出: 120, 总计: 170
AI: 你好!有什么可以帮你的吗?
11.5 监听器的高级应用
11.5.1 记录完整对话日志
你可以将每次请求的输入、输出、Token 等信息存储到数据库或日志文件,用于后续分析。例如,在 onResponse 中插入一条数据库记录。
11.5.2 动态调整行为
监听器可以修改请求吗?不可以 。监听器是只读的,主要用于观察和记录。如果你需要修改请求(例如动态添加系统消息),应该在构建 AI Service 时通过其他方式实现(如 SystemMessageProvider)。
11.5.3 多监听器的执行顺序
如果注册了多个监听器,它们会按注册顺序执行。例如,先执行日志监听器,再执行监控监听器。
11.5.4 与 Micrometer 集成
你可以将监听器与 Micrometer(Spring Boot 的监控库)集成,将 Token 用量、耗时等指标导出到 Prometheus 等监控系统。
示例(伪代码):
java
@Override
public void onResponse(AiServiceResponse response) {
if (response.tokenUsage() != null) {
Counter.builder("ai.token.usage")
.tag("type", "input")
.register(meterRegistry)
.increment(response.tokenUsage().inputTokenCount());
// ... 类似处理输出 token
}
}
11.6 本章小结
- 事件监听器的作用:监控、日志、度量。
- AiServiceListener 接口:三个主要回调方法。
- 实现自定义监听器:记录耗时和 Token 用量。
- 注册监听器 :通过
withListeners()方法。 - 实践:构建了一个带监控的助手,并观察输出。
十二、与 Spring Boot 生态集成
在前面的章节中,我们一直在 main 方法里运行示例,这在学习和测试阶段非常方便。但在实际企业开发中,我们通常使用 Spring Boot 这样的框架来构建可维护、可扩展的 Web 应用。将 LangChain4j 与 Spring Boot 集成,可以让你享受 Spring 生态的便利:依赖注入、自动配置、配置文件管理、监控等。
本章将带你一步步将 LangChain4j 融入 Spring Boot 项目,最终构建一个提供 REST API 的知识库问答服务。
12.1 为什么要在 Spring Boot 中使用 LangChain4j?
- 配置集中管理 :API 密钥、模型名称、超时等配置可以写在
application.yml中,而不是散落在代码里。 - 依赖注入 :AI Service、工具类、检索器等都可以定义为 Spring Bean,通过
@Autowired或构造函数注入,便于单元测试和替换。 - 自动配置 :LangChain4j 提供了 Spring Boot Starter,可以自动创建常用的 Bean(如
ChatLanguageModel),减少样板代码。 - 与 Web 层无缝整合:通过 Controller 暴露 REST API,让前端或其他服务调用你的 AI 能力。
- 利用 Spring 生态:可以结合 Spring Security 做权限控制,结合 Spring Cache 做结果缓存,结合 Spring Actuator 做健康检查。
12.2 引入 LangChain4j Spring Boot Starter
首先,创建一个 Spring Boot 项目(可以使用 Spring Initializr 快速生成),并添加以下依赖:
Maven (pom.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version> <!-- 使用较新版本,Java 17+ -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>langchain4j-springboot-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<langchain4j.version>1.0.0-beta3</langchain4j.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- LangChain4j Spring Boot Starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- 如果你需要使用 OpenAI 模型,引入对应集成(starter 会自动引入核心,但模型集成需单独加) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- 如果需要本地嵌入模型,添加(可选) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- 为了简化代码,引入 Lombok(可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Gradle (build.gradle)
groovy
plugins {
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
}
group = 'com.example'
version = '1.0-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'dev.langchain4j:langchain4j-spring-boot-starter:1.0.0-beta3'
implementation 'dev.langchain4j:langchain4j-open-ai-spring-boot-starter:1.0.0-beta3'
implementation 'dev.langchain4j:langchain4j-embeddings-all-minilm-l6-v2:1.0.0-beta3'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
test {
useJUnitPlatform()
}
注意 :langchain4j-spring-boot-starter 是核心自动配置模块,它本身不包含具体模型集成。你需要根据使用的模型引入对应的 starter,例如 langchain4j-open-ai-spring-boot-starter 或 langchain4j-ollama-spring-boot-starter 等。
12.3 在 application.yml 中配置模型和 API 密钥
在 src/main/resources/application.yml 中配置 OpenAI 模型参数。如果你使用演示端点,可以配置如下:
yaml
langchain4j:
open-ai:
chat-model:
base-url: http://langchain4j.dev/demo/openai/v1
api-key: demo
model-name: gpt-4o-mini
temperature: 0.7
log-requests: true # 可选,打印请求日志
log-responses: true # 可选,打印响应日志
如果你使用真实 OpenAI API,替换为:
yaml
langchain4j:
open-ai:
chat-model:
api-key: ${OPENAI_API_KEY} # 从环境变量读取
model-name: gpt-4o-mini
temperature: 0.7
提示 :更多配置项可以参考 LangChain4j Spring Boot Starter 文档。
12.4 将 AI Service 声明为 Bean
在 Spring Boot 中,我们可以将 AI Service 接口定义为 Bean,然后在需要的地方注入。有两种方式:
- 使用
@Bean方法手动创建(更灵活) - 使用
@AiService注解自动创建(更简洁,需要引入额外注解)
方式一:使用 @Bean 手动创建
创建一个配置类 AiConfig.java:
java
package com.example.config;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.service.AiServices;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AiConfig {
@Bean
public Assistant assistant(ChatLanguageModel chatLanguageModel) {
// ChatLanguageModel 已经由 Spring Boot Starter 自动创建并注入
return AiServices.create(Assistant.class, chatLanguageModel);
}
}
这里的 Assistant 接口是我们定义的 AI Service 接口,可以放在 com.example.service 包中:
java
package com.example.service;
import dev.langchain4j.service.SystemMessage;
public interface Assistant {
@SystemMessage("你是一个乐于助人的助手。")
String chat(String userMessage);
}
方式二:使用 @AiService 注解自动创建(推荐)
LangChain4j 提供了 @AiService 注解,你只需在接口上添加该注解,框架就会自动创建该接口的实现并注册为 Spring Bean。
首先,确保你的主类或配置类启用了 @EnableAiServices(如果 starter 版本支持,可能自动启用,但为了保险可以加上):
java
package com.example;
import dev.langchain4j.service.spring.EnableAiServices;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableAiServices // 启用 AI Service 自动扫描
public class LangChain4jApplication {
public static void main(String[] args) {
SpringApplication.run(LangChain4jApplication.class, args);
}
}
然后,在 AI Service 接口上添加 @AiService 注解:
java
package com.example.service;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;
@AiService
public interface Assistant {
@SystemMessage("你是一个乐于助人的助手。")
String chat(String userMessage);
}
无需手动创建 @Bean,Spring Boot 启动后会自动扫描带有 @AiService 的接口,并创建实现类 Bean。你可以在任何地方通过 @Autowired 注入该接口。
12.5 在 Controller 中注入 AI Service,构建 REST API
现在创建一个简单的 REST Controller,提供聊天接口。
java
package com.example.controller;
import com.example.service.Assistant;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/chat")
@RequiredArgsConstructor
public class ChatController {
private final Assistant assistant;
@PostMapping
public String chat(@RequestBody ChatRequest request) {
return assistant.chat(request.message());
}
// 简单的请求体封装
public record ChatRequest(String message) {}
}
也可以提供一个 GET 接口便于测试:
java
@GetMapping
public String chatGet(@RequestParam String message) {
return assistant.chat(message);
}
12.6 实践:快速搭建一个包含 RAG 功能的 Web 服务
接下来我们整合之前学过的 RAG 功能,构建一个知识库问答的 Web 服务。我们将:
- 在应用启动时加载知识文档并摄入到向量存储中(内存存储)
- 创建带有 RAG 检索器的 AI Service
- 通过 REST API 提供问答
12.6.1 准备知识文档
在 src/main/resources 目录下创建 knowledge.txt 文件,内容如下(你可以替换为自己的知识):
LangChain4j 是一个为 Java 开发者设计的 LLM 集成框架。
它提供了统一 API,支持多种模型提供商。
RAG 是 Retrieval-Augmented Generation 的缩写,意为检索增强生成。
LangChain4j 内置了完整的 RAG 工具链,包括文档加载器、分割器、嵌入模型和向量存储。
使用 LangChain4j,你可以轻松构建基于私有知识库的问答系统。
Spring Boot 集成让这一切更加简单。
12.6.2 定义 RAG 相关的 Bean
我们需要定义:
- 嵌入模型(
EmbeddingModel) - 向量存储(
EmbeddingStore) - 文档加载和摄入逻辑(在应用启动时执行)
- 检索器(
ContentRetriever) - 带有 RAG 的 AI Service
修改配置类 AiConfig.java:
java
package com.example.config;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.InMemoryEmbeddingStore;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.nio.file.Path;
@Slf4j
@Configuration
public class RagConfig {
// 定义嵌入模型 Bean
@Bean
public EmbeddingModel embeddingModel() {
return new AllMiniLmL6V2EmbeddingModel();
}
// 定义向量存储 Bean(使用内存存储,重启后丢失)
@Bean
public EmbeddingStore<TextSegment> embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
// 定义检索器 Bean,使用向量存储和嵌入模型
@Bean
public ContentRetriever contentRetriever(
EmbeddingStore<TextSegment> embeddingStore,
EmbeddingModel embeddingModel) {
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(3)
.minScore(0.5)
.build();
}
// 定义带有 RAG 的 AI Service Bean
@Bean
public RagAssistant ragAssistant(
dev.langchain4j.model.chat.ChatLanguageModel chatLanguageModel,
ContentRetriever contentRetriever) {
return AiServices.builder(RagAssistant.class)
.chatLanguageModel(chatLanguageModel)
.contentRetriever(contentRetriever)
.build();
}
// 应用启动时摄入知识文档
@Bean
public IngestionInitializer ingestionInitializer(
EmbeddingModel embeddingModel,
EmbeddingStore<TextSegment> embeddingStore) {
return new IngestionInitializer(embeddingModel, embeddingStore);
}
// 内部类,用于初始化摄入
@Slf4j
public static class IngestionInitializer {
private final EmbeddingModel embeddingModel;
private final EmbeddingStore<TextSegment> embeddingStore;
public IngestionInitializer(EmbeddingModel embeddingModel,
EmbeddingStore<TextSegment> embeddingStore) {
this.embeddingModel = embeddingModel;
this.embeddingStore = embeddingStore;
}
@PostConstruct
public void ingest() {
try {
// 从 classpath 加载文档
Path path = new ClassPathResource("knowledge.txt").getFile().toPath();
Document document = FileSystemDocumentLoader.loadDocument(path);
// 构建摄入器
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(dev.langchain4j.data.document.splitter.DocumentSplitters.recursive(300, 30))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
ingestor.ingest(document);
log.info("知识文档已成功摄入,向量存储条目数:{}",
((InMemoryEmbeddingStore<?>) embeddingStore).entries().size());
} catch (IOException e) {
log.error("摄入知识文档失败", e);
}
}
}
}
12.6.3 定义 RAG Assistant 接口
java
package com.example.service;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;
@AiService
public interface RagAssistant {
@SystemMessage("你是一个知识库助手,请基于提供的上下文回答问题。如果上下文不足以回答,请说明你不知道。")
String chat(String userMessage);
}
注意这里我们没有使用 @MemoryId,因为简单示例中不需要记忆。如果需要,可以按照前面章节的方法添加。
12.6.4 创建 REST Controller
java
package com.example.controller;
import com.example.service.RagAssistant;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/rag")
@RequiredArgsConstructor
public class RagController {
private final RagAssistant ragAssistant;
@PostMapping
public String ask(@RequestBody QueryRequest request) {
return ragAssistant.chat(request.question());
}
@GetMapping
public String askGet(@RequestParam String question) {
return ragAssistant.chat(question);
}
public record QueryRequest(String question) {}
}
12.6.5 启动应用并测试
运行 Spring Boot 主类。控制台会输出日志,显示文档摄入成功。
然后使用 curl 或浏览器测试:
bash
# GET 请求
curl "http://localhost:8080/api/rag?question=什么是RAG?"
# POST 请求
curl -X POST http://localhost:8080/api/rag \
-H "Content-Type: application/json" \
-d '{"question": "LangChain4j 支持哪些模型?"}'
你应该会看到基于 knowledge.txt 内容的回答。如果问题不在文档中,AI 会表示不知道。
12.7 进一步扩展
- 添加记忆 :如果需要多轮对话,可以注入
ChatMemory和@MemoryId。定义带有记忆的 AI Service,并在 Controller 中接收用户 ID。 - 添加工具 :将工具类也定义为 Spring Bean,并通过
AiServices.tools()传入。 - 配置多个模型 :可以在
application.yml中配置多个模型 Bean,然后通过@Qualifier选择使用哪个。 - 使用缓存:对频繁相同的问题,可以用 Spring Cache 缓存结果。
- 集成 Spring Security:为 API 添加认证和授权。
12.8 本章小结
通过本章的学习,你成功将 LangChain4j 与 Spring Boot 集成,并构建了一个提供 REST API 的知识库问答服务。你掌握了:
- 引入 LangChain4j Spring Boot Starter 及相关依赖。
- 在
application.yml中配置模型参数。 - 使用
@AiService注解或手动@Bean方式将 AI Service 声明为 Spring Bean。 - 在 Controller 中注入并使用 AI Service。
- 整合 RAG 组件,在应用启动时加载知识文档,并通过 REST API 提供问答。
至此,已经完成了从零到一构建完整 AI 应用的学习旅程。现在,你可以将所学知识应用到实际项目中,构建各种智能应用,如客服机器人、文档问答系统、智能助手等。