Java开发者的大模型入门:LangChain4j组件全攻略(二)

九、内容安全与合规:审核组件

前面已经掌握了构建智能知识库的核心技能。但是,当你的 AI 应用走向生产环境时,有一个重要问题必须考虑:内容安全

AI 模型可能生成不合适的内容(例如暴力、色情、仇恨言论),也可能被用户恶意利用来诱导模型输出有害信息。因此,为你的 AI 应用加上"护栏"------内容审核机制,是至关重要的。

LangChain4j 提供了专门的审核组件,帮助你自动检测和过滤不安全的内容。本章将带你学习如何使用这些组件,确保你的 AI 应用安全合规。

9.1 ModerationModel 的作用

ModerationModel(审核模型)是一种特殊的模型,专门用于判断一段文本是否包含不安全内容。它通常返回一个分类结果,例如文本是否涉及仇恨言论、自残、暴力等。

LangChain4j 内置了对 OpenAI 审核模型的支持(也可以扩展其他服务商)。OpenAI 的审核模型可以识别以下类别:

  • hate:仇恨言论
  • hate/threatening:仇恨言论且带有威胁
  • self-harm:自残相关内容
  • sexual:色情内容
  • sexual/minors:涉及未成年人的色情内容
  • violence:暴力内容
  • violence/graphic:血腥暴力内容

你可以用审核模型在用户输入AI输出两个阶段进行检查:

  1. 预审核:在用户消息发送给 AI 之前,先检查是否违规。如果违规,可以拒绝回答或给出警告。
  2. 后审核:在 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 会正常回复。 尝试输入敏感内容,如"我要自杀",程序会捕获异常并输出提示信息。

流程图:带审核的对话流程

sequenceDiagram participant 用户 participant 审核组件 participant AI服务 participant 大模型 用户->>审核组件: 发送消息 审核组件->>审核组件: 检查消息是否安全 alt 消息不安全 审核组件-->>用户: 返回拒绝信息(抛异常) else 消息安全 审核组件->>AI服务: 传递安全消息 AI服务->>大模型: 调用模型生成回答 大模型-->>AI服务: 返回回答 AI服务->>审核组件: 检查回答是否安全 alt 回答不安全 审核组件-->>用户: 拦截回答,返回警告 else 回答安全 审核组件-->>用户: 返回回答 end end

9.5 本章小结

  • 审核模型的作用:识别不当内容,保护应用安全。
  • OpenAiModerationModel 的使用:手动审核文本,对输入输出分别检查。
  • @Moderate 注解:声明式审核,自动拦截不安全内容。
  • 实践:为聊天助手添加审核功能,捕获异常并友好提示。

十、多模态探索:处理图片与文件

到目前为止,我们所有的交互都是基于文本的。但现实世界的信息是多样的------图片、PDF、音频、视频......大语言模型正在向多模态进化,能够理解和生成非文本内容。LangChain4j 也紧跟潮流,提供了初步的多模态支持。

本章将带你探索如何让 AI "看懂"图片,以及如何处理 PDF 等文件。虽然目前多模态功能还在快速发展中,但掌握基础用法可以为你打开更多应用场景的大门。

10.1 LangChain4j 对多模态的支持现状

目前,LangChain4j 对多模态的支持主要体现在以下几个方面:

  • 图像输入 :可以通过 ImageContent 将图片作为消息的一部分发送给支持多模态的模型(如 OpenAI 的 GPT-4 Vision、Google 的 Gemini Pro Vision 等)。
  • 文件输入 :某些模型支持接收 PDF、音频、视频等文件,LangChain4j 提供了对应的 PdfFileContentAudioFileContentVideoFileContent 等类。
  • 多模态输出:部分模型可以生成图像,但 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-turbogpt-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 运行说明

  1. 设置环境变量 OPENAI_API_KEY 为你的真实 OpenAI 密钥。
  2. 运行程序。
  3. 输入一张图片的 URL(例如 https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg)或本地图片的绝对路径。
  4. 等待 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 输出,以备合规检查。

监听器正是为了解决这些问题而生。它像"摄像头"一样,记录下每次调用的详细信息。

监听器工作示意图:

graph LR subgraph AI Service 调用生命周期 A[开始调用] --> B[请求构建] B --> C[发送给模型] C --> D[收到响应] D --> E[结束调用] end subgraph 监听器回调 L1[onRequest
请求开始] 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

监听器需要通过 AiServiceswithListeners() 方法注册。可以注册一个或多个监听器。

java 复制代码
Assistant assistant = AiServices.builder(Assistant.class)
        .chatLanguageModel(model)
        .withListeners(new LoggingListener())   // 注册监听器
        .build();

如果要注册多个监听器,可以传入多个:

java 复制代码
.withListeners(listener1, listener2, listener3)

监听器会按照传入顺序依次调用。

11.4 实践:记录每次请求的耗时和 Token 用量

让我们构建一个实用的监听器,它可以:

  • 记录每次请求的耗时(从 onRequestonResponse / 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-starterlangchain4j-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,然后在需要的地方注入。有两种方式:

  1. 使用 @Bean 方法手动创建(更灵活)
  2. 使用 @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 应用的学习旅程。现在,你可以将所学知识应用到实际项目中,构建各种智能应用,如客服机器人、文档问答系统、智能助手等。

相关推荐
重庆穿山甲3 小时前
Java开发者的大模型入门:LangChain4j组件全攻略(一)
后端
颜酱3 小时前
单调队列:滑动窗口极值问题的最优解(通用模板版)
javascript·后端·算法
Java水解4 小时前
Rust嵌入式开发实战——从ARM裸机编程到RTOS应用
后端·rust
AI探索者4 小时前
LangGraph 条件路由:构建支持工具调用的智能 Agent
后端
苍何4 小时前
终于,我把 Openclaw 加 Seed2.0 Skills 做 AI 漫剧搞定了
后端
苍何4 小时前
阿里出手,最强Coding Plan出炉,OpenClaw可以痛快玩了
后端
风象南5 小时前
Claude Code这个隐藏技能,让我告别PPT焦虑
人工智能·后端
神奇小汤圆5 小时前
为什么 Spring 强烈推荐你用 singleton
后端