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

一、开篇:为什么Java开发者需要LangChain4j

1.1 大模型浪潮下,Java 开发者的机遇与挑战

过去两年,大语言模型(LLM)席卷全球,从 ChatGPT 到各种垂直领域模型,AI 能力正以前所未有的速度渗透到各行各业。作为企业级后端的中流砥柱,Java 开发者自然需要思考:如何将大模型的能力无缝集成到现有的 Java 系统中?

直接调用大模型 API(例如 OpenAI 的接口)听起来很简单------发一个 HTTP 请求,拿回一段文本。但在实际生产环境中,我们会面临一系列棘手的问题:

  • 复杂的调用细节:需要手动构建 JSON 请求体、处理 HTTP 连接、解析流式响应、处理鉴权和错误重试。
  • 提示词管理困难:业务场景往往需要动态构造提示词,拼接用户输入、历史对话、系统指令,代码很快就变得难以维护。
  • 对话状态维护:实现一个多轮对话机器人,必须自己维护会话历史,并在每次请求时把历史消息都带上。
  • 输出不可控:大模型返回的是自然语言文本,如果想让 AI 返回结构化的数据(例如 JSON、对象),还需要自己编写解析器和异常处理。
  • 知识库集成复杂:要让模型基于企业内部知识回答问题(RAG),需要自己实现文档加载、文本分割、向量化、向量检索等一系列组件。

如果每个项目都从零开始重复造这些轮子,不仅开发效率低,而且容易出错,更难以应对模型切换、版本升级等变化。

1.2 原始调用方式:一个直观的对比

让我们先看一段最原始的 Java 代码,它使用 HttpClient 调用 OpenAI 的聊天接口:

java 复制代码
// 原始方式:直接调用 OpenAI API
HttpClient client = HttpClient.newHttpClient();
String requestBody = """
    {
        "model": "gpt-3.5-turbo",
        "messages": [
            {"role": "user", "content": "你好,请介绍一下自己"}
        ]
    }
    """;

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.openai.com/v1/chat/completions"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer YOUR_API_KEY")
    .POST(HttpRequest.BodyPublishers.ofString(requestBody))
    .build();

try {
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    // 手动解析 JSON 响应
    JSONObject json = new JSONObject(response.body());
    String answer = json.getJSONArray("choices")
                        .getJSONObject(0)
                        .getJSONObject("message")
                        .getString("content");
    System.out.println("AI 回答:" + answer);
} catch (Exception e) {
    e.printStackTrace();
}

这段代码虽然能工作,但存在明显的问题:

  • 硬编码 API 地址和密钥,难以管理。
  • JSON 构造和解析冗长且脆弱(字段名变化、空值处理)。
  • 没有错误重试、超时控制等生产级必备机制。
  • 如果要支持多轮对话,需要自己拼接历史消息数组,代码复杂度会急剧上升。

1.3 LangChain4j 登场:为 Java 量身打造的 LLM 集成框架

LangChain4j 正是为了解决上述痛点而诞生的。它是著名的 LangChain 社区的 Java 版本,但并非简单移植,而是深度契合 Java 语言习惯和生态的一套全新框架。它的核心理念是:让 Java 开发者用最熟悉的方式,像调用普通方法一样使用大模型。

让我们用 LangChain4j 重写上面的功能:

java 复制代码
// LangChain4j 方式:简洁、直观
OpenAiChatModel model = OpenAiChatModel.builder()
    .apiKey("demo")  // 使用官方 demo 密钥,无需注册
    .modelName("gpt-3.5-turbo")
    .build();

String answer = model.chat("你好,请介绍一下自己");
System.out.println("AI 回答:" + answer);

是不是简洁得令人惊讶?短短几行代码,没有 JSON 处理,没有 HTTP 细节,甚至不需要自己解析响应。这就是 LangChain4j 的魅力------它将所有底层复杂性封装起来,让你专注于业务逻辑。

1.4 LangChain4j 的核心优势

优势 说明
统一 API 支持 OpenAI、Azure、Google Vertex AI、Hugging Face、Ollama(本地模型)等主流模型提供商,切换模型只需修改一行配置,业务代码无需改动。
组件化设计 模型、记忆、工具、检索器、输出解析器、审核器等都是独立组件,可以自由组合,像搭积木一样构建复杂应用。
声明式编程 通过 Java 接口和注解(如 @SystemMessage@Tool)定义 AI 行为,框架自动实现接口,代码极度简化。
生产级特性 内置重试、回退、缓存、请求/响应拦截、Token 用量统计、结构化输出、函数调用等,开箱即用。
与 Java 生态无缝集成 提供 Spring Boot Starter 和 Quarkus 扩展,可以像使用普通 Bean 一样使用 AI 服务,完美融入现有项目。
面向 RAG 的完整工具链 提供了文档加载器、分割器、嵌入模型、向量存储、检索器等一系列组件,让构建知识库问答系统变得轻而易举。

下图展示了 LangChain4j 的核心组件及其关系:

graph TD A[AI Services
声明式接口] --> B[ChatLanguageModel
对话模型] A --> C[ChatMemory
记忆管理] A --> D[Tool
工具调用] A --> E[OutputParser
输出解析] A --> F[ContentRetriever
知识检索] F --> G[EmbeddingStore
向量存储] F --> H[EmbeddingModel
嵌入模型] B --> I[OpenAI] B --> J[Azure] B --> K[本地模型
Ollama等] style A fill:#f9f,stroke:#333,stroke-width:2px style F fill:#bbf,stroke:#333

1.5 本文目标:零基础全组件实战

如果你是一名 Java 开发者,但从未接触过大模型开发,不用担心。本文将带你从零开始,一步一步亲手实践 LangChain4j 的所有核心组件。你将学到:

  • 基础篇:模型调用、提示词模板、多轮对话
  • 声明式篇:用 AI Services 和注解定义智能接口
  • 记忆篇:管理多用户对话状态
  • 工具篇:让 AI 调用你的 Java 方法(函数调用)
  • RAG 篇:构建基于私有知识库的问答系统(检索增强生成)
  • 安全篇:内容审核与合规
  • 多模态篇:处理图片、PDF 等文件
  • 监控篇:监听器与日志
  • 生态集成篇:与 Spring Boot 整合

二、环境准备:5分钟搭建第一个LangChain4j应用

在开始动手之前,我们先确保你的开发环境就绪。本章将带领你完成从零到第一个可运行程序的全过程,全程只需5分钟。

2.1 开发环境要求

  • JDK 8 或更高版本(推荐 JDK 11 或 17,LangChain4j 完全兼容)
  • Maven (3.6+)或 Gradle(6.8+)------ 任选一种你熟悉的构建工具
  • IDE:IntelliJ IDEA、Eclipse 或 VS Code(Java插件)
  • 网络连接:能够访问公网(因为需要调用大模型API,本地模型除外)

如果你还没有安装JDK或Maven/Gradle,请先自行安装。这里不再赘述。

2.2 在项目中引入 LangChain4j 核心依赖

我们将创建一个最简单的 Java 项目,并添加 LangChain4j 的依赖。

使用 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>

    <groupId>com.example</groupId>
    <artifactId>langchain4j-quickstart</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <langchain4j.version>1.0.0-beta3</langchain4j.version>
    </properties>

    <dependencies>
        <!-- LangChain4j 核心依赖 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>
        <!-- OpenAI 集成(包含对 OpenAI API 的支持) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>
    </dependencies>
</project>

使用 Gradle

创建 build.gradle 文件,内容如下:

groovy 复制代码
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'dev.langchain4j:langchain4j:1.0.0-beta3'
    implementation 'dev.langchain4j:langchain4j-open-ai:1.0.0-beta3'
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

注意 :版本号 1.0.0-beta3 是编写本文时的最新版本,建议你访问 LangChain4j 官方发布页 查看并使用最新的稳定版本。

依赖引入后,Maven/Gradle 会自动下载所需的 jar 包。这一步可能需要几分钟,取决于你的网络。

2.3 获取并配置大模型 API 密钥

LangChain4j 支持多种模型提供商,包括 OpenAI、Azure、Google Vertex AI、Hugging Face、Ollama(本地模型)等。对于初学者,我们推荐使用 OpenAI 的 demo 密钥进行快速测试,无需注册付费。

OpenAI 官方提供了一个测试用的 API 端点:http://langchain4j.dev/demo/openai/v1,配合密钥 demo 即可使用,无需真实 OpenAI 账户。这非常适合学习和实验。

当然,如果你有自己的 OpenAI API 密钥,也可以使用官方端点 https://api.openai.com/v1

安全提示 :API 密钥属于敏感信息,请勿硬编码在代码中。本文示例使用 demo 密钥,因此无需担心。如果你使用真实密钥,建议通过环境变量或配置文件加载。

2.4 编写第一个程序:与 AI 对话

现在让我们创建第一个 Java 类 FirstLangChain4jDemo,体验 LangChain4j 的简洁与强大。

代码实现

src/main/java/com/example 目录下创建 FirstLangChain4jDemo.java

java 复制代码
package com.example;

import dev.langchain4j.model.openai.OpenAiChatModel;

public class FirstLangChain4jDemo {
    public static void main(String[] args) {
        // 1. 创建模型实例(使用 demo 密钥快速测试)
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1") // 使用官方演示端点
                .apiKey("demo")                                    // 固定密钥
                .modelName("gpt-4o-mini")                          // 可选,指定模型
                .build();

        // 2. 发送一条消息,获取回复
        String userMessage = "你好,请用一句话介绍你自己。";
        String response = model.chat(userMessage);

        // 3. 打印结果
        System.out.println("用户: " + userMessage);
        System.out.println("AI: " + response);
    }
}

代码逐行解释

  • OpenAiChatModel.builder() :使用建造者模式创建 OpenAI 聊天模型实例。
    • .baseUrl(...):指定 API 端点。这里用了 LangChain4j 官方提供的测试端点,无需真实 OpenAI 账户。
    • .apiKey("demo"):测试端点对应的固定密钥。
    • .modelName(...):指定使用的模型名称,例如 gpt-4o-minigpt-3.5-turbo。如果不指定,会使用默认模型。
  • model.chat(userMessage):这是最核心的方法。传入用户消息(字符串),返回 AI 的回复(字符串)。背后自动处理了 HTTP 请求、JSON 解析、错误处理等。
  • 打印输出:简单展示对话内容。

运行效果

在 IDE 中直接运行 main 方法,控制台将输出类似以下内容:

makefile 复制代码
用户: 你好,请用一句话介绍你自己。
AI: 你好!我是一个人工智能助手,由 OpenAI 开发,旨在帮助用户解答问题、提供信息和支持各种任务。

恭喜! 你已经成功用 LangChain4j 完成了第一次与 AI 的对话。

流程图:第一个程序的执行过程

sequenceDiagram participant 你的Java代码 participant LangChain4j participant 演示API端点 你的Java代码->>LangChain4j: 调用 model.chat("你好...") LangChain4j->>演示API端点: 发送HTTP请求(含消息) 演示API端点-->>LangChain4j: 返回AI生成的回复 LangChain4j-->>你的Java代码: 返回解析后的字符串 你的Java代码->>控制台: 打印对话

2.5 可能遇到的问题及解决

  • 依赖下载失败:检查网络,配置 Maven/Gradle 镜像(如阿里云镜像)。
  • 运行时报错 :确认 baseUrlapiKey 正确无误;如果使用真实 OpenAI 密钥,请将 baseUrl 改为 https://api.openai.com/v1,并将 apiKey 替换为你的密钥。
  • 响应慢:网络问题,可尝试切换网络或使用代理。

2.6 小结

本章我们完成了:

  • 环境搭建:JDK、Maven/Gradle 准备就绪
  • 依赖引入:添加 LangChain4j 核心和 OpenAI 集成
  • 密钥获取:了解 demo 密钥的用法
  • 第一个程序:使用 OpenAiChatModel 发送消息并获取回复

你已经迈出了使用 LangChain4j 的第一步!接下来,我们将深入探讨更丰富的组件,从基础交互到高级功能,逐步构建一个完整的 AI 应用。

三、基础交互:消息、提示词与多轮对话

我们实现了最简单的"一问一答"。但在实际应用中,我们往往需要更复杂的交互:给 AI 设定角色(系统消息)、动态构造提问内容、让 AI 记住对话上下文。本章将带你掌握这些基础但至关重要的技能。

3.1 理解 LangChain4j 的消息类型

在 LangChain4j 中,与大模型的对话由一条条消息组成,每条消息都有一个"角色"。最常见的三种角色是:

  • SystemMessage(系统消息):用于设定 AI 助手的角色、行为准则或全局指令。它通常放在对话的最开始,对整个对话生效。
  • UserMessage(用户消息):代表用户输入的问题或指令。
  • AiMessage(AI 消息):代表模型生成的回复。

LangChain4j 提供了对应的 Java 类来表示这些消息。下面的类图展示了它们的关系:

classDiagram class ChatMessage { <> +Role role() +String text() } class SystemMessage { +String text } class UserMessage { +String text } class AiMessage { +String text } ChatMessage <|-- SystemMessage ChatMessage <|-- UserMessage ChatMessage <|-- AiMessage

在调用模型时,我们需要传入一个包含多条消息的列表。例如,包含系统消息和用户消息的请求:

java 复制代码
List<ChatMessage> messages = Arrays.asList(
    new SystemMessage("你是一个乐于助人的Java编程助手,回答要简洁。"),
    new UserMessage("Java 8 和 Java 11 的主要区别是什么?")
);

3.2 使用系统消息设定 AI 角色

我们先通过一个例子来感受系统消息的作用。创建一个新类 SystemMessageDemo.java

java 复制代码
package com.example;

import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;

import java.util.List;

import static dev.langchain4j.data.message.UserMessage.userMessage;
import static dev.langchain4j.data.message.SystemMessage.systemMessage;
import static dev.langchain4j.data.message.AiMessage.aiMessage;

public class SystemMessageDemo {
    public static void main(String[] args) {
        // 1. 创建模型
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        // 2. 构造系统消息和用户消息
        SystemMessage sysMsg = systemMessage("你是一个精通中文的翻译助手,将用户输入翻译成英文,只输出翻译结果,不要多余的解释。");
        UserMessage userMsg = userMessage("今天天气真好!");

        // 3. 调用模型(传入消息列表)
        String response = model.chat(List.of(sysMsg, userMsg));
        System.out.println("AI 翻译结果:" + response);
    }
}

运行这段代码,你会看到 AI 只输出翻译结果,没有任何额外内容,因为我们通过系统消息约束了它的行为。

注意systemMessage()userMessage() 是 LangChain4j 提供的静态工厂方法,可以更简洁地创建消息对象。上面的 import static 语句让代码更简洁。

3.3 提示词模板(PromptTemplate)------ 动态构造消息

在实际业务中,用户消息往往需要动态插入变量,例如"我的订单号是 {orderId},请查询状态"。如果每次都用字符串拼接,不仅繁琐,而且容易出错(如注入风险)。LangChain4j 提供了 PromptTemplate 来解决这个问题。

PromptTemplate 允许你定义一个带占位符的模板,然后用变量值填充它,生成最终的 UserMessage

代码示例:使用 PromptTemplate

创建 PromptTemplateDemo.java

java 复制代码
package com.example;

import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;

import java.util.Map;

public class PromptTemplateDemo {
    public static void main(String[] args) {
        // 1. 定义模板,占位符用 {{变量名}} 表示
        String template = "我的订单号是 {{orderId}},请帮我查询当前状态。";

        // 2. 创建 PromptTemplate 对象
        PromptTemplate promptTemplate = PromptTemplate.from(template);

        // 3. 准备变量值
        Map<String, Object> variables = Map.of("orderId", "NO123456789");

        // 4. 应用变量,生成 Prompt 对象(Prompt 可以转换为 UserMessage)
        Prompt prompt = promptTemplate.apply(variables);
        
        // 5. 从 Prompt 获取用户消息
        UserMessage userMessage = prompt.toUserMessage();

        // 6. 创建模型并发送
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        String response = model.chat(userMessage);
        System.out.println("AI: " + response);
    }
}

执行流程示意图:

graph LR A[定义模板-我的订单号是orderId] --> B[创建PromptTemplate] C[变量Map orderId:NO123] --> D[apply] B --> D D --> E[生成Prompt对象] E --> F[toUserMessage] F --> G[发送给模型]

PromptTemplate 不仅可以用在用户消息上,也可以用于系统消息(通过 PromptTemplate 生成字符串后手动创建 SystemMessage),但通常系统消息是静态的,不需要频繁变量替换。

3.4 多轮对话的实现

真正的对话往往是多轮的,模型需要知道之前说过什么才能保持上下文连贯。例如:

  • 用户:"我叫小明。"
  • AI:"你好,小明!"
  • 用户:"我喜欢 Java。"
  • AI:"Java 是一门很棒的编程语言,小明。"

要实现这一点,我们需要在每次请求时,把历史消息都传给模型。最简单的方式是自己维护一个消息列表,每次追加新消息。

3.4.1 手动维护消息列表

创建 ManualConversationDemo.java

java 复制代码
package com.example;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;

import java.util.ArrayList;
import java.util.List;

public class ManualConversationDemo {
    public static void main(String[] args) {
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        // 用于存储整个对话历史的消息列表
        List<dev.langchain4j.data.message.ChatMessage> conversation = new ArrayList<>();

        // 第一轮:用户自我介绍
        UserMessage firstUserMsg = UserMessage.from("我叫小明,你呢?");
        conversation.add(firstUserMsg);
        
        String firstResponse = model.chat(conversation);
        System.out.println("AI第一轮回复:" + firstResponse);
        
        // 将AI的回复加入历史
        conversation.add(AiMessage.from(firstResponse));

        // 第二轮:用户继续提问
        UserMessage secondUserMsg = UserMessage.from("我喜欢 Java,你能推荐一些学习资源吗?");
        conversation.add(secondUserMsg);
        
        String secondResponse = model.chat(conversation);
        System.out.println("AI第二轮回复:" + secondResponse);
        
        // 再将AI回复加入历史
        conversation.add(AiMessage.from(secondResponse));

        // 可以继续更多轮...
    }
}

运行后你会发现,第二轮 AI 的回答会记得你叫小明,因为它看到了历史消息。这种手动管理的方式虽然可行,但代码冗长,且在多用户并发场景下容易出错。

3.4.2 引入 ChatMemory:让记忆管理更简单

LangChain4j 提供了 ChatMemory 接口来专门管理对话历史。它的作用就像一个"记忆盒子",自动存储用户和 AI 的所有消息,并且可以随时获取整个历史记录。

最常用的实现是 MessageWindowChatMemory,它只保留最近 N 条消息,防止记忆无限增长(同时也节省 Token 消耗)。

流程图:ChatMemory 的作用

sequenceDiagram participant 你的代码 participant ChatMemory participant 模型 你的代码->>ChatMemory: 添加用户消息 你的代码->>ChatMemory: 获取所有消息 ChatMemory-->>你的代码: 返回消息列表 你的代码->>模型: 传入消息列表 模型-->>你的代码: 返回AI回复 你的代码->>ChatMemory: 添加AI回复到记忆

3.4.3 实践:用 ChatMemory 构建聊天机器人

创建 ChatMemoryDemo.java

java 复制代码
package com.example;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;

import java.util.List;
import java.util.Scanner;

public class ChatMemoryDemo {
    public static void main(String[] args) {
        // 1. 创建模型
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        // 2. 创建 ChatMemory,设置最大消息数为10(即保留最近10条对话)
        ChatMemory chatMemory = MessageWindowChatMemory.builder()
                .maxMessages(10)
                .build();

        // 3. 用 Scanner 模拟控制台聊天
        Scanner scanner = new Scanner(System.in);
        System.out.println("开始和 AI 聊天(输入 'exit' 结束)");

        while (true) {
            System.out.print("你: ");
            String userInput = scanner.nextLine();
            if ("exit".equalsIgnoreCase(userInput)) {
                break;
            }

            // 4. 将用户消息加入记忆
            chatMemory.add(UserMessage.from(userInput));

            // 5. 从记忆获取整个对话历史(包含刚加入的用户消息)
            List<ChatMessage> allMessages = chatMemory.messages();

            // 6. 调用模型
            String answer = model.chat(allMessages);

            // 7. 将 AI 回复加入记忆
            chatMemory.add(AiMessage.from(answer));

            System.out.println("AI: " + answer);
        }
        scanner.close();
    }
}

运行说明:这是一个简易的交互式聊天程序。你可以连续输入多条消息,AI 会记住之前的对话内容。

关键点解释

  • MessageWindowChatMemory 就像一个滑动窗口,只保留最近的 maxMessages 条消息。这里设为10,适合大多数场景。
  • 每次用户输入后,我们手动 add 用户消息,调用模型后 add AI 回复。如果不 add AI 回复,AI 就看不到自己的历史回答,可能造成上下文断裂。
  • 调用 model.chat(allMessages) 时,传入的是当前完整的历史记录,模型据此生成有上下文的回复。

3.4.4 使用 ChatMemory 的注意事项

  • 记忆的持久化MessageWindowChatMemory 是内存存储,应用重启后对话消失。如果需要长期保存(如数据库),可以实现自己的 ChatMemory 或结合 ChatMemoryStore
  • 多用户隔离 :在多用户 Web 应用中,每个用户应该拥有独立的 ChatMemory 实例。我们将在第五章详细讲解如何通过 @MemoryId 实现。
  • Token 消耗 :保留的消息越多,每次请求消耗的 Token 也越多。合理设置 maxMessages 平衡上下文长度和成本。

3.5 本章小结

  • 消息类型SystemMessageUserMessageAiMessage 的作用和用法。
  • 提示词模板 :用 PromptTemplate 动态构造用户消息,避免字符串拼接。
  • 多轮对话 :从手动维护消息列表到使用 ChatMemory 管理对话历史,实现带上下文的聊天机器人。

现在,你可以用这些知识构建一个简单的客服机器人或知识问答助手了。

四、声明式AI:AI Services 组件

通过前几章的学习,已经能够用几行代码调用大模型,并实现多轮对话。但是,你是否觉得每次都要手动构造消息列表、处理历史记忆还是有点繁琐?如果能让 AI 像调用本地方法一样简单,那该多好!

这正是 AI Services 要解决的问题。它是 LangChain4j 最强大的高级抽象,让你通过定义 Java 接口来声明式地使用 AI,框架会在背后自动实现所有细节。

4.1 什么是 AI Services?为何它能极大简化代码?

AI Services 是 LangChain4j 的核心概念之一。它的工作方式是:

  1. 你定义一个 Java 接口,并在接口方法上添加注解来描述 AI 的行为(例如使用什么系统提示、如何处理输出)。
  2. LangChain4j 在运行时动态生成该接口的实现类。
  3. 当你调用接口方法时,框架自动处理:
    • 根据注解和方法参数构造提示词
    • 调用大模型
    • 解析模型输出为你期望的 Java 类型(String、POJO、List 等)
    • 如果配置了记忆,自动管理对话历史
    • 如果配置了工具,自动触发函数调用

这意味着你几乎不用写任何实现代码,只需要关注业务逻辑。这非常符合 Java 开发者熟悉的"面向接口编程"风格。

工作原理示意图:

graph TD A[定义AI Service接口
如 Assistant] --> B[在接口方法上添加注解
SystemMessage, UserMessage] B --> C[使用 AiServices.create
生成实现类] C --> D[调用接口方法] D --> E[LangChain4j 自动处理:
1. 构造提示词
2. 调用模型
3. 解析输出
4. 管理记忆] E --> F[返回结果给调用方]

4.2 定义第一个 AI Service 接口:一个简单的聊天助手

我们从最简单的例子开始:定义一个聊天助手接口,只有一个方法,接收用户消息并返回 AI 回复。

代码实现

创建一个新类 AiServiceDemo.java

java 复制代码
package com.example;

import dev.langchain4j.service.AiServices;
import dev.langchain4j.model.openai.OpenAiChatModel;

// 1. 定义 AI Service 接口
interface Assistant {
    String chat(String userMessage);
}

public class AiServiceDemo {
    public static void main(String[] args) {
        // 2. 创建模型实例(和之前一样)
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        // 3. 使用 AiServices 为接口创建实现类
        Assistant assistant = AiServices.create(Assistant.class, model);

        // 4. 像调用普通方法一样使用 AI
        String answer = assistant.chat("你好,请介绍一下 Java 的特点");
        System.out.println("AI: " + answer);
    }
}

代码解释

  • interface Assistant :我们定义了一个极其简单的接口,包含一个方法 String chat(String userMessage)。没有注解,意味着方法参数 userMessage 会直接作为用户消息发送给模型,返回类型 String 表示我们期望纯文本回复。
  • AiServices.create(Assistant.class, model) :这是核心魔法。AiServices.create() 方法接受接口的 Class 对象和模型实例,返回一个动态代理对象,实现了该接口。
  • 调用 assistant.chat(...) :调用方法时,LangChain4j 自动将参数包装成 UserMessage,调用模型,并将模型返回的 AiMessage 的文本内容作为方法返回值。

运行这段代码,你会得到类似之前的回复,但代码量更少,而且无需手动处理消息列表。

4.3 深入注解:@SystemMessage 和 @UserMessage

上面的例子中,我们没有给 AI 设定任何角色指令。在实际应用中,我们经常需要设置系统消息来定义 AI 的行为。这时可以用 @SystemMessage 注解。

4.3.1 使用 @SystemMessage 设定角色

修改上面的 Assistant 接口,添加 @SystemMessage

java 复制代码
import dev.langchain4j.service.SystemMessage;

interface Assistant {
    @SystemMessage("你是一个 Java 编程导师,回答要简洁并鼓励用户学习。")
    String chat(String userMessage);
}

然后重新运行 AiServiceDemo,观察输出是否更符合导师角色。

4.3.2 使用变量占位符:{{it}} 和 {{变量名}}

有时候系统消息或用户消息需要动态插入变量。例如,导师可以根据不同的用户水平调整语气。这时可以使用占位符。

占位符规则:

  • {{it}} 代表方法的第一个参数(如果只有一个参数)。
  • {{变量名}} 代表方法参数中带有 @V("变量名") 注解的参数。
示例 1:使用 {{it}}
java 复制代码
interface Assistant {
    @SystemMessage("你是一个乐于助人的助手。")
    @UserMessage("请用中文回答:{{it}}")
    String chat(String userMessage);
}

这里 {{it}} 会被方法参数 userMessage 的值替换。这样即使方法参数名随意,也能正确填充。

示例 2:使用多个变量

如果方法有多个参数,需要给每个参数命名,然后用 @V 注解标记:

java 复制代码
import dev.langchain4j.service.V;
import dev.langchain4j.service.UserMessage;

interface Translator {
    @UserMessage("将以下文本翻译成 {{targetLanguage}}:{{text}}")
    String translate(@V("text") String text, @V("targetLanguage") String targetLanguage);
}

然后在 main 方法中:

java 复制代码
Translator translator = AiServices.create(Translator.class, model);
String result = translator.translate("Hello, world", "中文");
System.out.println(result); // 输出:你好,世界

框架会自动将 texttargetLanguage 填充到模板中,生成完整的用户消息。

4.3.3 实践:带角色和变量的 AI Service

创建一个完整的示例 AnnotatedAiServiceDemo.java

java 复制代码
package com.example;

import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.model.openai.OpenAiChatModel;

interface JavaMentor {
    @SystemMessage("你是一个 Java 编程导师,你的学生是 {{level}} 水平。")
    @UserMessage("请解释一下 {{concept}}")
    String explain(@V("level") String level, @V("concept") String concept);
}

public class AnnotatedAiServiceDemo {
    public static void main(String[] args) {
        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        JavaMentor mentor = AiServices.create(JavaMentor.class, model);

        String explanation = mentor.explain("初学者", "什么是类?");
        System.out.println("导师回答:\n" + explanation);
    }
}

运行后,AI 会根据"初学者"水平,用通俗易懂的方式解释"类"的概念。

4.4 返回结构化输出(Output Parsing)

大模型返回的是自然语言文本,但很多时候我们希望能直接得到结构化的 Java 对象,例如从一段文本中提取出人员信息。LangChain4j 的 AI Services 支持将模型输出自动解析为 POJO、枚举、集合等类型。

4.4.1 场景:让 AI 返回 Java 对象

假设我们要从一段用户描述中提取姓名、年龄和城市。我们定义一个 Person 类:

java 复制代码
public class Person {
    private String name;
    private int age;
    private String city;

    // 必须提供无参构造和 getter/setter,或者全参构造+字段(框架会通过反射设置)
    public Person() {}

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + ", city='" + city + "'}";
    }
}

然后定义 AI Service 接口,让方法直接返回 Person 对象:

java 复制代码
import dev.langchain4j.service.UserMessage;

interface PersonExtractor {
    @UserMessage("从以下文本中提取人物信息:{{it}}")
    Person extractPerson(String text);
}

main 方法中:

java 复制代码
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);

String text = "我叫张三,今年28岁,住在上海。";
Person person = extractor.extractPerson(text);
System.out.println(person);

运行后,你会看到类似输出:

ini 复制代码
Person{name='张三', age=28, city='上海'}

背后原理:LangChain4j 会在发送给模型的提示词中隐式地要求模型以 JSON 格式输出,然后将 JSON 反序列化为目标类。这一切对开发者透明。

4.4.2 支持的类型

除了 POJO,AI Services 还支持:

  • 基础类型intdoubleboolean
  • 集合类型List<String>Set<Integer>
  • 枚举 :例如将情感分类为 POSITIVENEUTRALNEGATIVE

示例:返回枚举列表

java 复制代码
enum Sentiment {
    POSITIVE, NEUTRAL, NEGATIVE
}

interface SentimentAnalyzer {
    @UserMessage("分析以下评论的情感倾向:{{it}}")
    List<Sentiment> analyzeSentiments(String review);
}

假设输入是多个评论,AI 可以返回对应的情感列表。

4.5 获取元数据:Result<T> 包装类

有时除了模型返回的内容,我们还想知道这次调用的额外信息,例如消耗了多少 Token(TokenUsage)、模型回答的结束原因(FinishReason)、RAG 检索到的来源(Sources)等。这时可以用 Result<T> 包装返回类型。

Result<T> 是一个泛型类,包含:

  • content():实际内容(类型为 T)
  • tokenUsage():Token 消耗信息
  • finishReason():结束原因
  • sources():RAG 检索到的来源(后续章节会用到)

4.5.1 示例:获取 Token 消耗

修改前面的 Assistant 接口,返回 Result<String>

java 复制代码
import dev.langchain4j.service.Result;

interface Assistant {
    @SystemMessage("你是一个 Java 编程导师。")
    Result<String> chat(String userMessage);
}

main 中调用并获取元数据:

java 复制代码
Assistant assistant = AiServices.create(Assistant.class, model);
Result<String> result = assistant.chat("什么是多态?");

String answer = result.content();
TokenUsage tokenUsage = result.tokenUsage();
FinishReason finishReason = result.finishReason();

System.out.println("回答:" + answer);
System.out.println("消耗 Token:" + tokenUsage.inputTokenCount() + " 输入,"
        + tokenUsage.outputTokenCount() + " 输出,总计 " + tokenUsage.totalTokenCount());
System.out.println("结束原因:" + finishReason);

输出类似:

vbnet 复制代码
回答:多态是面向对象编程的重要特性,指同一个方法在不同对象上有不同实现...
消耗 Token:150 输入,320 输出,总计 470
结束原因:STOP

这对于监控成本、调试非常有用。

4.5.2 其他元数据

  • sources():当使用 RAG 时,可以获取模型参考的文档片段列表。
  • 可以通过 result.metadata() 获取完整的元数据 Map。

4.6 本章小结

通过本章,已经掌握了 AI Services 这一强大武器:

  • 声明式接口:定义接口,添加注解,即可获得 AI 能力。
  • @SystemMessage 和 @UserMessage:设定角色和动态提示词。
  • 结构化输出:让 AI 直接返回 Java 对象,省去手动解析 JSON 的麻烦。
  • Result 包装:获取 Token 消耗等元数据,便于监控和调试。

五、有状态的对话:记忆管理

我们已经能够用 AI Services 以声明式的方式调用大模型。但你有没有发现一个问题:如果多个用户同时使用同一个 AI Service 实例,他们的对话会互相干扰吗?让我们用一个简单的例子来演示这个问题。

5.1 问题:多用户场景下如何隔离对话记忆?

假设我们构建了一个客服助手,需要为每个用户保持独立的对话上下文。如果所有用户共享同一个 ChatMemory,那么用户 A 的消息会被用户 B 看到,这显然是不合理的。

模拟共享记忆的问题

创建一个简单的 AI Service,并使用我们之前学的 ChatMemory

java 复制代码
package com.example;

import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;

interface ChatAssistant {
    String chat(String userMessage);
}

public class SharedMemoryProblemDemo {
    public static void main(String[] args) {
        // 创建共享的记忆(容量为10条)
        var chatMemory = MessageWindowChatMemory.builder().maxMessages(10).build();

        var model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        // 将同一个记忆注入到 AI Service 中
        ChatAssistant assistant = AiServices.builder(ChatAssistant.class)
                .chatLanguageModel(model)
                .chatMemory(chatMemory)
                .build();

        // 模拟两个用户交替发送消息
        System.out.println("用户A: 你好,我叫小明");
        String responseA = assistant.chat("你好,我叫小明");
        System.out.println("AI: " + responseA);

        System.out.println("\n用户B: 我叫小红,你呢?");
        String responseB = assistant.chat("我叫小红,你呢?");
        System.out.println("AI: " + responseB);

        System.out.println("\n用户A: 你还记得我叫什么吗?");
        String responseA2 = assistant.chat("你还记得我叫什么吗?");
        System.out.println("AI: " + responseA2);
    }
}

运行这段代码,你会发现一个严重的问题:AI 把两个用户的对话混在一起了。当用户 A 第二次提问时,AI 可能会提到"小红",因为它在记忆中看到了用户 B 的消息。这在实际应用中是不可接受的。

多用户记忆混淆示意图:

graph TD subgraph 共享记忆 M1[用户A: 我叫小明] M2[AI: 你好小明] M3[用户B: 我叫小红] M4[AI: 你好小红] end UserA -->|发送消息| M1 UserB -->|发送消息| M3 M2 -->|返回给A| UserA M4 -->|返回给B| UserB UserA -.->|后续问题| 共享记忆 共享记忆 -.->|包含了B的对话| UserA

解决办法是为每个用户分配独立的 ChatMemory。但如何优雅地实现呢?LangChain4j 提供了 @MemoryId 注解和 ChatMemoryProvider

5.2 @MemoryId 注解的作用

@MemoryId 是一个方法参数注解,用于标识哪个参数是"记忆 ID"。框架会根据这个 ID 自动为每个 ID 分配独立的 ChatMemory

5.2.1 基本用法

在 AI Service 接口的方法参数中,添加 @MemoryId 注解:

java 复制代码
import dev.langchain4j.service.MemoryId;

interface ChatAssistant {
    String chat(@MemoryId int memoryId, String userMessage);
}

当调用 chat(1, "你好")chat(2, "你好") 时,框架会为 ID 为 1 和 2 的用户分别维护独立的对话记忆。

5.2.2 支持的类型

@MemoryId 参数可以是任何类型,只要它的 hashCode()equals() 方法正确实现。常见类型:

  • 整数类型:intlong
  • 字符串:String
  • UUID:java.util.UUID

5.3 使用 ChatMemoryProvider 为每个用户提供独立记忆

@MemoryId 需要配合 ChatMemoryProvider 使用。ChatMemoryProvider 是一个函数式接口,负责根据 ID 提供对应的 ChatMemory 实例。

LangChain4j 提供了一个简单的实现:MessageWindowChatMemory.withMaxMessages(10) 本身不是 provider,我们需要创建 provider 来为每个 ID 生成新的 ChatMemory

5.3.1 创建 ChatMemoryProvider

最常用的方式是使用 Lambda 表达式或方法引用:

java 复制代码
ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder()
        .id(memoryId)           // 给记忆设置一个ID(可选,便于调试)
        .maxMessages(10)        // 每个记忆最多保留10条消息
        .build();

然后将这个 provider 传递给 AiServices

5.3.2 完整示例:多用户隔离的助手

创建 MemoryIsolationDemo.java

java 复制代码
package com.example;

import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;

interface MultiUserAssistant {
    String chat(@MemoryId int userId, String userMessage);
}

public class MemoryIsolationDemo {
    public static void main(String[] args) {
        // 1. 创建模型
        var model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        // 2. 创建 ChatMemoryProvider:为每个 userId 新建一个记忆窗口
        var chatMemoryProvider = (dev.langchain4j.memory.ChatMemoryProvider) memoryId -> {
            // memoryId 是 @MemoryId 参数的值,这里是 userId
            return MessageWindowChatMemory.builder()
                    .id(memoryId)           // 记忆的唯一标识
                    .maxMessages(10)
                    .build();
        };

        // 3. 使用 AiServices.builder() 构建,传入 provider
        MultiUserAssistant assistant = AiServices.builder(MultiUserAssistant.class)
                .chatLanguageModel(model)
                .chatMemoryProvider(chatMemoryProvider)
                .build();

        // 4. 模拟两个用户(ID分别为1和2)的对话
        System.out.println("用户1: 你好,我是小明");
        String response1 = assistant.chat(1, "你好,我是小明");
        System.out.println("AI对用户1: " + response1);

        System.out.println("\n用户2: 你好,我是小红");
        String response2 = assistant.chat(2, "你好,我是小红");
        System.out.println("AI对用户2: " + response2);

        System.out.println("\n用户1: 你知道我叫什么吗?");
        String response1_2 = assistant.chat(1, "你知道我叫什么吗?");
        System.out.println("AI对用户1: " + response1_2);

        System.out.println("\n用户2: 你还记得我的名字吗?");
        String response2_2 = assistant.chat(2, "你还记得我的名字吗?");
        System.out.println("AI对用户2: " + response2_2);
    }
}

运行输出示例:

makefile 复制代码
用户1: 你好,我是小明
AI对用户1: 你好小明!我是你的AI助手,有什么可以帮助你的吗?

用户2: 你好,我是小红
AI对用户2: 你好小红!很高兴认识你,有什么问题需要我帮忙吗?

用户1: 你知道我叫什么吗?
AI对用户1: 你刚才说你叫小明,对吗?需要我记住你的名字吗?

用户2: 你还记得我的名字吗?
AI对用户2: 当然记得,你叫小红。需要我帮你做什么吗?

可以看到,AI 正确地为每个用户保持了独立的上下文,没有互相干扰。

多用户隔离示意图:

graph TD subgraph 记忆池 direction TB M1[记忆空间 for userId=1] M2[记忆空间 for userId=2] end UserA -- userId=1 --> M1 UserB -- userId=2 --> M2 M1 --> AI[AI Service] M2 --> AI AI -->|回复给用户1| UserA AI -->|回复给用户2| UserB

5.4 实践:构建一个支持多用户记忆的客服助手

现在让我们把这个概念应用到实际场景中。假设我们要为电商平台开发一个客服助手,每个用户有独立的对话历史。

5.4.1 定义客服助手接口

java 复制代码
interface CustomerServiceAssistant {
    @SystemMessage("你是一个电商客服助手,友好、专业,帮助用户解答购物相关问题。")
    String chat(@MemoryId String sessionId, @UserMessage String message);
}

这里使用 String 类型的 sessionId,可以是用户 ID 或会话令牌。

5.4.2 实现支持多用户的客服服务

创建一个可运行的类 CustomerServiceDemo.java

java 复制代码
package com.example;

import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;

import java.util.Scanner;

interface CustomerService {
    @SystemMessage("你是一个电商客服助手,友好、专业。记住用户的名字和之前提到的问题。")
    String chat(@MemoryId String sessionId, @UserMessage String message);
}

public class CustomerServiceDemo {
    public static void main(String[] args) {
        var model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        var chatMemoryProvider = (dev.langchain4j.memory.ChatMemoryProvider) memoryId ->
                MessageWindowChatMemory.builder()
                        .id(memoryId)
                        .maxMessages(20)   // 保留最近20条
                        .build();

        CustomerService service = AiServices.builder(CustomerService.class)
                .chatLanguageModel(model)
                .chatMemoryProvider(chatMemoryProvider)
                .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;
            }
            // 解析用户ID和消息,格式如 "1001:你好"
            String[] parts = input.split(":", 2);
            if (parts.length != 2) {
                System.out.println("格式错误,请使用 用户ID:消息");
                continue;
            }
            String userId = parts[0].trim();
            String message = parts[1].trim();

            String response = service.chat(userId, message);
            System.out.println("AI对用户" + userId + ": " + response);
        }
        scanner.close();
    }
}

运行演示:

makefile 复制代码
> 1001:你好,我叫张三
AI对用户1001: 你好张三!我是你的客服助手,今天想咨询什么问题呢?
> 1002:你好,我叫李四
AI对用户1002: 你好李四!很高兴为你服务,有什么可以帮助你的?
> 1001:我刚才问你什么了?
AI对用户1001: 你刚才告诉我你叫张三,并打了个招呼。需要我帮你查商品还是其他问题?
> 1002:你还记得我的名字吗?
AI对用户1002: 当然记得,你叫李四。有什么购物问题需要帮助吗?

完美!每个用户的记忆独立,体验就像与专属客服对话。

5.4.3 并发场景下的注意事项

在多线程环境中,ChatMemory 的实现需要是线程安全的。MessageWindowChatMemory 是线程安全的,可以放心在 Web 应用中使用。同时,ChatMemoryProvider 也应该保证为相同 ID 返回同一个 ChatMemory 实例,而不是每次新建(否则会丢失记忆)。上面的 provider 使用 memoryId 作为键,可以结合缓存实现单例,但最简单的是使用 ConcurrentHashMap 来存储每个 ID 对应的记忆。

改进的 provider 示例(生产级):

java 复制代码
import java.util.concurrent.ConcurrentHashMap;

var memories = new ConcurrentHashMap<Object, ChatMemory>();
var chatMemoryProvider = (ChatMemoryProvider) memoryId -> 
    memories.computeIfAbsent(memoryId, id -> 
        MessageWindowChatMemory.builder()
            .id(id)
            .maxMessages(20)
            .build()
    );

这样,相同 ID 会复用同一个记忆实例,确保对话连续性。

5.5 记忆的持久化扩展(简介)

目前我们使用的 MessageWindowChatMemory 是基于内存的,应用重启后记忆会丢失。在生产环境中,通常需要将对话历史持久化到数据库,以便长期保存或跨会话恢复。

LangChain4j 提供了 ChatMemoryStore 接口,你可以实现它来将消息存储到数据库、Redis 等持久化存储中。

5.5.1 ChatMemoryStore 接口

java 复制代码
public interface ChatMemoryStore {
    List<ChatMessage> getMessages(Object memoryId);
    void updateMessages(Object memoryId, List<ChatMessage> messages);
    void deleteMessages(Object memoryId);
}
  • getMessages:从存储中加载指定 memoryId 的历史消息。
  • updateMessages:当记忆内容变化时(添加新消息),调用此方法更新存储。框架会定期调用,或者在每次修改后调用(取决于实现)。
  • deleteMessages:删除记忆(可选)。

5.5.2 实现思路

你可以将 ChatMessage 序列化为 JSON,存入数据库。框架内置了 PersistentChatMemory,它需要你提供 ChatMemoryStore 实现。

示例伪代码(持久化到数据库):

java 复制代码
public class DatabaseChatMemoryStore implements ChatMemoryStore {
    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        // 从数据库查询 memoryId 对应的消息列表
        // 反序列化为 List<ChatMessage>
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        // 将 messages 序列化后存入数据库
    }

    // deleteMessages 可选实现
}

然后在创建 ChatMemory 时使用:

java 复制代码
ChatMemory chatMemory = PersistentChatMemory.builder()
        .id(memoryId)
        .chatMemoryStore(new DatabaseChatMemoryStore())
        .maxMessages(100)  // 持久化也可以限制窗口大小
        .build();

这样,即使应用重启,用户记忆也能从数据库恢复,实现长期记忆。

5.6 本章小结

  • 多用户记忆隔离的必要性:避免用户间对话干扰。
  • @MemoryId 注解:标识记忆 ID,框架自动根据 ID 分配独立记忆。
  • ChatMemoryProvider:为每个 ID 提供 ChatMemory 实例,可结合缓存实现复用。
  • 实践:构建了一个支持多用户、独立记忆的客服助手。
  • 持久化扩展 :了解如何通过 ChatMemoryStore 将记忆持久化到数据库。

六、赋予 AI 行动能力:工具调用(Function Calling)

通过前几章的学习,你已经能够用 AI Services 构建智能对话系统,并能记住多轮对话。但到目前为止,AI 只能基于它训练时学到的知识回答问题。如果用户问"现在几点了?"、"帮我查一下天气"、"查询订单号 123 的状态",纯文本模型是无法直接获取这些实时信息的------它没有时钟,也无法访问你的数据库。

工具调用(Function Calling,也称为函数调用)正是为了解决这个问题而生。它允许大模型在需要时"调用"你编写的 Java 方法,获取实时数据或执行操作,然后将结果整合到回答中。

6.1 什么是工具调用?AI 如何调用外部方法?

工具调用的核心思想是:你提供一组工具(Java 方法),并告诉 AI 这些工具的存在、用途以及参数。当 AI 认为需要某个工具来回答问题时,它会返回一个特殊的请求,要求执行该工具并提供参数。你的应用负责执行对应方法,并将结果返回给 AI,AI 再根据结果生成最终回答。

工作流程示意图:

sequenceDiagram participant 用户 participant AI服务 participant 工具方法 用户->>AI服务: 提问(例如"现在几点了?") AI服务->>AI服务: 分析问题,决定需要调用工具 AI服务-->>AI服务: 返回工具调用请求(方法名+参数) AI服务->>工具方法: 执行对应方法(例如 getCurrentTime()) 工具方法-->>AI服务: 返回结果(例如"14:30") AI服务->>AI服务: 将结果整合到回答中 AI服务-->>用户: 返回最终回答("现在是下午2点30分。")

在整个流程中,除了定义工具方法外,你几乎不需要额外代码。LangChain4j 会自动处理工具调用的握手过程。

6.2 用 @Tool 注解标记 Java 方法

在 LangChain4j 中,将一个普通 Java 方法变为"可被 AI 调用的工具"非常简单:只需要在方法上添加 @Tool 注解。

6.2.1 最简单的工具:获取当前时间

让我们从一个极简的例子开始。创建一个工具类,其中包含一个获取当前时间的方法:

java 复制代码
import dev.langchain4j.agent.tool.Tool;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class TimeTool {
    @Tool("获取当前时间")  // 描述工具的作用,会传给 AI
    public String getCurrentTime() {
        return LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
    }
}

@Tool 注解中的描述是可选的,但强烈建议提供,因为它帮助 AI 理解这个工具是做什么的,从而在合适的场景下调用。

6.2.2 将工具注册到 AI Service

我们需要告诉 AI Service 有哪些工具可用。通过 AiServices.withTools() 来添加工具实例:

java 复制代码
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;

interface Assistant {
    String chat(String userMessage);
}

public class ToolCallingDemo {
    public static void main(String[] args) {
        var model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        // 创建工具实例
        TimeTool timeTool = new TimeTool();

        // 构建 AI Service,传入工具
        Assistant assistant = AiServices.builder(Assistant.class)
                .chatLanguageModel(model)
                .tools(timeTool)  // 可以传入多个工具
                .build();

        String response = assistant.chat("现在几点了?");
        System.out.println("AI: " + response);
    }
}

运行这段代码,AI 应该能正确回答当前时间。背后的过程:

  1. AI 识别出需要知道当前时间才能回答。
  2. AI 返回一个工具调用请求(调用 getCurrentTime)。
  3. LangChain4j 自动执行 timeTool.getCurrentTime()
  4. 将结果(例如 "14:30:45")返回给 AI。
  5. AI 生成最终回答,如"现在是下午2点30分45秒。"

6.3 带参数的工具

很多工具需要参数。例如,查询天气需要城市名,查询订单需要订单号。工具方法可以定义参数,AI 在调用时会自动提取参数值。

6.3.1 示例:查询天气

java 复制代码
import dev.langchain4j.agent.tool.Tool;

public class WeatherTool {
    @Tool("查询指定城市的天气")
    public String getWeather(String city) {
        // 这里应该是真实的天气 API 调用,为了演示,我们返回模拟数据
        return switch (city) {
            case "北京" -> "晴,25℃";
            case "上海" -> "多云,28℃";
            case "广州" -> "雷阵雨,30℃";
            default -> city + "的天气数据暂未收录";
        };
    }
}

AI Service 的构建方式与之前相同:

java 复制代码
Assistant assistant = AiServices.builder(Assistant.class)
        .chatLanguageModel(model)
        .tools(new WeatherTool())
        .build();

String response = assistant.chat("上海天气怎么样?");
System.out.println(response);

AI 会调用 getWeather("上海"),获取结果后生成回答:"上海今天多云,28℃。"

6.3.2 参数描述

为了让 AI 更准确地填充参数,可以在 @Tool 注解中通过 valuearray 提供更详细的参数描述。LangChain4j 也支持使用 Javadoc 或 @Parameter 注解来描述参数,但目前最简单的是在工具描述中说明。

如果需要更精细的控制(如参数是否必填、参数说明),可以使用 @Toolvaluename 属性,结合工具方法签名。

示例:带有参数描述的 Javadoc(会被 LangChain4j 自动提取)

java 复制代码
public class OrderTool {
    /**
     * 查询订单状态
     * @param orderId 订单号,例如 "NO123456"
     * @return 订单状态描述
     */
    @Tool
    public String getOrderStatus(String orderId) {
        // 查询数据库...
        return "订单 " + orderId + " 已发货,预计明天送达。";
    }
}

LangChain4j 会解析 Javadoc 中的描述,传递给 AI,帮助 AI 理解参数含义。

6.4 实践:让 AI 自动调用工具

我们做一个综合练习:创建一个电商助手,它可以查询订单状态,还能计算两个数字的和(虽然简单,但演示了多种工具)。用户会混合提问,AI 需要判断何时调用哪个工具。

6.4.1 定义工具类

java 复制代码
import dev.langchain4j.agent.tool.Tool;

public class ECommerceTools {
    
    @Tool("查询订单状态,需要订单号")
    public String getOrderStatus(String orderId) {
        // 模拟数据库查询
        return "订单 " + orderId + " 当前状态:已发货,预计3天内送达。";
    }
    
    @Tool("计算两个数字的和")
    public int add(int a, int b) {
        return a + b;
    }
}

6.4.2 构建 AI Service 并测试

java 复制代码
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;

interface ShoppingAssistant {
    String chat(String userMessage);
}

public class ECommerceDemo {
    public static void main(String[] args) {
        var model = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        var tools = new ECommerceTools();

        ShoppingAssistant assistant = AiServices.builder(ShoppingAssistant.class)
                .chatLanguageModel(model)
                .tools(tools)
                .build();

        // 测试几个问题
        System.out.println(assistant.chat("我的订单号是 ABC123,帮我查一下状态"));
        System.out.println(assistant.chat("计算 123 + 456 等于多少?"));
        System.out.println(assistant.chat("今天天气怎么样?")); // 没有天气工具,AI 会如实告知无法回答
    }
}

预期输出:

  • 对于第一个问题,AI 调用 getOrderStatus("ABC123"),然后回答"订单 ABC123 当前状态:已发货,预计3天内送达。"
  • 对于第二个问题,AI 调用 add(123, 456),得到结果 579,然后回答"123 + 456 = 579"。
  • 对于第三个问题,因为没有天气工具,AI 可能会说"抱歉,我没有查询天气的能力。"或者类似的话。

6.5 高级:ToolProvider 动态提供工具集

在某些场景下,你可能希望根据上下文动态决定提供哪些工具。例如,普通用户只能使用订单查询,管理员可以使用更多工具。这时可以用 ToolProvider 接口。

6.5.1 ToolProvider 接口

ToolProvider 是一个函数式接口,定义如下:

java 复制代码
public interface ToolProvider {
    List<ToolSpecification> provideTools(ToolProviderRequest request);
    default ToolExecutor toolExecutor(String toolName) { ... }
}

实现这个接口可以更精细地控制工具的提供和执行。

6.5.2 示例:基于用户角色提供不同工具

假设我们有用户对象,包含角色信息。我们需要在调用时传入用户角色,让 AI Service 根据角色决定是否提供某些敏感工具。

首先,定义两个工具类:普通工具和管理员工具。

java 复制代码
public class CommonTools {
    @Tool("查询商品信息")
    public String queryProduct(String productName) {
        return productName + " 的价格是 99 元。";
    }
}

public class AdminTools {
    @Tool("删除商品(管理员专用)")
    public String deleteProduct(String productId) {
        return "商品 " + productId + " 已删除。";
    }
}

接下来,我们需要一个自定义的 ToolProvider,它会根据当前用户角色返回不同的工具集。为此,我们需要在 AI Service 方法中传入角色信息,但 ToolProvider 本身无法直接访问方法参数。一个常见的做法是使用线程局部变量或通过上下文传递,但更简单的是:我们可以在每次调用前构建一个临时的 AiServices,不过那样效率低。

LangChain4j 提供了 ToolProvider 结合 AiServiceContext 的方式,但对于小白教程,我们介绍一种更直观的方法:在 @Tool 方法内部进行权限检查,而不是动态移除工具。这样实现简单,且易于理解。

权限检查示例:

java 复制代码
public class AdminTools {
    private final User currentUser;

    public AdminTools(User user) {
        this.currentUser = user;
    }

    @Tool("删除商品(需要管理员权限)")
    public String deleteProduct(String productId) {
        if (!currentUser.isAdmin()) {
            throw new RuntimeException("无权限执行此操作");
        }
        return "商品 " + productId + " 已删除。";
    }
}

然后在调用时,为每个用户创建包含其信息的工具实例。由于每个用户有自己的会话,我们可以结合 @MemoryId 来获取用户信息,但这需要更复杂的集成。对于小白来说,先理解工具调用的基本概念即可。

6.6 工具调用的注意事项

  1. 工具方法应该是线程安全的:如果多个用户同时调用同一个工具实例,方法需要能够正确处理并发。
  2. 工具方法的执行时间:如果工具执行耗时较长(如调用外部API),可能会影响用户体验。建议将耗时操作异步化,或设置合理的超时。
  3. 错误处理:工具方法可能抛出异常,你应该捕获并返回友好的错误信息,或者让异常传播(框架会捕获异常并告诉 AI 调用失败)。
  4. 工具描述的重要性:清晰的描述能大大提高 AI 调用工具的准确率。描述应包括工具的作用、何时使用、参数的格式等。
  5. Token 消耗:工具的定义(包括方法名、参数描述)会作为系统提示的一部分发送给 AI,消耗一定的 Token。所以不要定义过多无关的工具。

6.7 本章小结

  • 工具调用的概念:让 AI 调用外部方法获取实时信息或执行操作。
  • @Tool 注解:将普通 Java 方法标记为工具。
  • 带参数的工具:方法参数由 AI 自动提取。
  • 注册工具 :通过 AiServices.tools() 将工具实例注入 AI Service。
  • 实践:构建了一个能查询订单、计算加法的电商助手。
  • 高级扩展 :了解 ToolProvider 和权限检查的思路。

七、构建企业级知识库:RAG 组件(上)

前面的章节我们已经掌握了 AI Services、记忆管理和工具调用,现在可以构建真正实用的企业级应用了------基于私有知识库的智能问答系统

想象一下,公司内部有大量的技术文档、产品手册、规章制度,员工想快速获取信息,传统方式是翻文档或问同事。如果有一个 AI 助手,能够基于这些文档回答问题,将极大提升效率。这就是 RAG(Retrieval-Augmented Generation,检索增强生成) 的典型场景。

7.1 RAG 核心概念:为什么需要 RAG?

大语言模型(LLM)虽然知识渊博,但它有几个天然局限:

  • 知识截止日期:模型训练的数据是某个时间点之前的,无法了解之后发生的事情。
  • 无法获取私有知识:公司内部的文档、数据库,模型从未见过。
  • 容易"幻觉":对不知道的问题,模型可能会编造答案。

RAG 通过引入一个外部知识检索步骤来解决这些问题。它的基本流程是:

  1. 用户提问
  2. 系统从知识库中检索与问题相关的片段(比如文档段落)
  3. 将检索到的片段作为上下文,连同问题一起发给大模型
  4. 模型基于提供的上下文生成答案

这样,模型的回答就有了事实依据,大大降低幻觉,而且可以随时更新知识库而无需重新训练模型。

RAG 标准流程示意图:

graph TD subgraph 离线阶段(知识摄入) A[原始文档
PDF/Word/TXT] --> B[文档加载器] B --> C[文档分割器
将文档切成小块] C --> D[嵌入模型
将每个小块转为向量] D --> E[向量数据库
存储向量+原始文本] end subgraph 在线阶段 (问答) F[用户问题] --> G[嵌入模型
将问题转为向量] G --> H[向量检索
在数据库中找相似片段] H --> I[将检索到的片段作为上下文] I --> J[大语言模型
基于上下文生成答案] J --> K[最终回答] end

从图中可以看出,RAG 分为两大阶段:知识摄入(Ingestion)问答检索(Retrieval & Generation)。本章我们先完成知识摄入部分,下一章实现问答。

7.2 知识摄入(Ingestion)详解

知识摄入是将原始文档处理成可供检索的形式的过程。LangChain4j 提供了完整的工具链,让我们一步步操作。

7.2.1 文档加载器(DocumentLoader)

文档加载器负责从各种来源读取文档。LangChain4j 内置了文件系统加载器,支持从目录加载多个文件。

java 复制代码
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import java.nio.file.Paths;
import java.util.List;

// 加载指定目录下的所有文档(支持 .txt, .pdf, .docx 等,但需要额外解析器)
List<Document> documents = FileSystemDocumentLoader.loadDocuments(Paths.get("/path/to/docs"));

对于 PDF、Word 等格式,需要引入对应的解析器依赖。为了简化,我们先用文本文件(.txt)演示。

注意:生产环境中,你可能需要处理 PDF、Word、Markdown 等格式。LangChain4j 通过 Apache Tika 等库支持多种格式,只需添加相应依赖即可。

7.2.2 文档分割器(DocumentSplitter)

大模型对输入长度有限制(上下文窗口),而且检索时也需要较小的文本块才能精确匹配。因此需要将长文档切分成若干段落(chunks)。

LangChain4j 提供了 DocumentSplitters 工具类,包含多种分割策略。最常用的是递归分割器,它会根据段落、句子、单词等层级递归切割,尽量保持语义完整。

java 复制代码
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;

// 创建一个分割器:每段最多300字符,重叠30字符
DocumentSplitter splitter = DocumentSplitters.recursive(300, 30);

// 将文档列表分割成文本块列表
List<TextSegment> segments = splitter.splitAll(documents);
  • maxSegmentSize:每个文本块的最大字符数(或 token 数,取决于具体实现)。
  • overlapSize:相邻块之间的重叠字符数,避免切在关键位置丢失上下文。

7.2.3 嵌入模型(EmbeddingModel)

嵌入模型将文本转换为向量(一组浮点数),向量的维度通常在几百到几千之间。语义相近的文本,它们的向量在空间中也更接近。

LangChain4j 支持多种嵌入模型:

  • 本地模型:如 AllMiniLmL6V2EmbeddingModel(基于 sentence-transformers,小巧快速)
  • 云端模型:OpenAI 的 text-embedding-ada-002、通义千问的嵌入模型等

对于入门,我们使用本地模型,无需 API 密钥。

首先添加依赖(如果之前没加的话):

xml 复制代码
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
    <version>${langchain4j.version}</version>
</dependency>

创建嵌入模型实例:

java 复制代码
import dev.langchain4j.model.embedding.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.model.embedding.EmbeddingModel;

EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();

7.2.4 向量存储(EmbeddingStore)

向量存储负责保存向量以及对应的原始文本块,并提供相似度检索功能。LangChain4j 提供了多种实现:

  • InMemoryEmbeddingStore:内存存储,适合测试和小型应用。
  • ElasticsearchEmbeddingStorePineconeEmbeddingStoreChromaEmbeddingStore:对接专业向量数据库。
  • PGvector:通过 PostgreSQL 的 pgvector 插件存储。

入门阶段,我们使用 InMemoryEmbeddingStore

java 复制代码
import dev.langchain4j.store.embedding.InMemoryEmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStore;

EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();

7.2.5 使用 EmbeddingStoreIngestor 一键完成摄入

手动一步步操作(分割、嵌入、存储)虽然清晰,但代码稍显繁琐。LangChain4j 提供了 EmbeddingStoreIngestor,它将分割器、嵌入模型、向量存储组合起来,只需一行代码完成摄入。

java 复制代码
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.InMemoryEmbeddingStore;

// 创建组件
EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();
EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();

// 构建摄入器
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
        .documentSplitter(DocumentSplitters.recursive(300, 30))  // 分割器
        .embeddingModel(embeddingModel)                           // 嵌入模型
        .embeddingStore(embeddingStore)                           // 存储
        .build();

// 执行摄入
List<Document> documents = ...; // 从之前加载得到
ingestor.ingest(documents);      // 一行代码完成分割、嵌入、存储

7.3 动手实践:将本地一个文本文件摄入为向量

现在让我们写一个完整的程序,将本地 knowledge.txt 文件摄入到内存向量存储中,并验证数据是否成功存储。

7.3.1 准备测试文档

在项目根目录下创建 knowledge.txt,内容如下:

复制代码
LangChain4j 是一个为 Java 开发者设计的 LLM 集成框架。
它提供了统一 API,支持多种模型提供商。
RAG 是 Retrieval-Augmented Generation 的缩写,意为检索增强生成。
LangChain4j 内置了完整的 RAG 工具链,包括文档加载器、分割器、嵌入模型和向量存储。
使用 LangChain4j,你可以轻松构建基于私有知识库的问答系统。

7.3.2 编写摄入代码

创建 IngestionDemo.java

java 复制代码
package com.example;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.InMemoryEmbeddingStore;

import java.nio.file.Paths;
import java.util.List;

public class IngestionDemo {
    public static void main(String[] args) {
        // 1. 加载文档(单个文件)
        Document document = FileSystemDocumentLoader.loadDocument(Paths.get("knowledge.txt"));

        // 2. 创建嵌入模型和向量存储
        EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();
        EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();

        // 3. 构建摄入器
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .documentSplitter(DocumentSplitters.recursive(300, 30))
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .build();

        // 4. 执行摄入
        ingestor.ingest(document);

        System.out.println("文档已成功摄入!");

        // 5. 简单验证:查看向量存储中的条目数
        // InMemoryEmbeddingStore 没有直接提供计数方法,但我们可以通过搜索来验证
        // 为了演示,我们直接打印存储对象的信息
        System.out.println("向量存储内容:" + embeddingStore);
        // 实际输出可能是 InMemoryEmbeddingStore{entries=5} 之类的(取决于分割结果)
    }
}

7.3.3 运行并观察

运行 main 方法,控制台输出类似:

ini 复制代码
文档已成功摄入!
向量存储内容:InMemoryEmbeddingStore{entries=3}

entries 数量取决于你的文档被分割成几块(每块 300 字符)。如果文档短,可能只有 1-2 块。

7.3.4 代码解释

  • FileSystemDocumentLoader.loadDocument :加载单个文件,返回 Document 对象。
  • DocumentSplitters.recursive(300, 30):创建递归分割器,最大块 300 字符,重叠 30 字符。
  • AllMiniLmL6V2EmbeddingModel:本地轻量嵌入模型,自动下载模型文件(首次运行会下载约 80MB 的模型,请耐心等待)。
  • InMemoryEmbeddingStore:内存向量存储。
  • ingestor.ingest(document):执行摄入流程,包括分割、嵌入、存储。

摄入完成后,向量存储中已经保存了文档片段的向量和原始文本,等待被检索。

7.4 验证向量存储中已有数据

虽然我们无法直接查看向量,但可以通过检索来验证。不过检索属于问答阶段,我们留到下一章详细讲解。现在,我们只需要确认摄入过程没有报错即可。

如果你想简单验证,可以在摄入后添加以下代码,手动检索一个问题:

java 复制代码
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import java.util.List;

// 将问题转为向量
String query = "什么是 RAG?";
Embedding queryEmbedding = embeddingModel.embed(query).content();

// 在向量存储中搜索最相似的片段
List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(queryEmbedding, 1);
if (!matches.isEmpty()) {
    EmbeddingMatch<TextSegment> match = matches.get(0);
    System.out.println("最相关的片段:" + match.embedded().text());
    System.out.println("相似度得分:" + match.score());
} else {
    System.out.println("未找到相关片段。");
}

这展示了检索的雏形,我们将在下一章深入。

7.5 本章小结

  • 理解了 RAG 为什么需要以及它的基本流程。
  • 学习了文档加载器、分割器、嵌入模型、向量存储的作用。
  • 使用 EmbeddingStoreIngestor 一键完成文档的摄入。
  • 亲手将本地文本文件转换成了可供检索的向量。

八、构建企业级知识库:RAG 组件(下)

上一章我们完成了知识摄入,将文档变成了向量存储在内存中。现在,我们要让 AI 能够基于这些知识回答问题------这就是 RAG 的在线阶段:检索与生成

8.1 检索与生成:ContentRetriever

ContentRetriever 是 LangChain4j 中负责检索相关内容的接口。它的核心方法是根据用户查询,返回匹配的 Content 列表(Content 包含文本片段及其元数据)。

最常用的实现是 EmbeddingStoreContentRetriever,它会:

  1. 将用户查询转为向量
  2. 在向量存储中搜索最相似的片段
  3. 返回这些片段作为检索结果

8.1.1 创建 EmbeddingStoreContentRetriever

继续使用上一章构建的 embeddingStoreembeddingModel

java 复制代码
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;

ContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
        .embeddingStore(embeddingStore)
        .embeddingModel(embeddingModel)
        .maxResults(3)           // 最多返回3个相关片段
        .minScore(0.6)            // 最低相似度得分(可选)
        .build();
  • maxResults:检索多少个相关片段返回给 AI。太少可能信息不全,太多可能超出模型上下文窗口,一般 3~5 个即可。
  • minScore:相似度阈值,低于此值的片段会被过滤。默认 0,可以根据实际效果调整。

8.1.2 检索器的工作流程

graph TD Q[用户问题] --> E[EmbeddingModel
转为向量] E --> S[EmbeddingStore
相似度搜索] S --> R[返回 Top N 匹配的 TextSegment] R --> C[包装为 Content 列表] C --> A[传递给 AI Service]

8.2 将 ContentRetriever 集成到 AI Service

现在我们把检索器注入到 AI Service 中,让它在每次用户提问时自动检索相关知识,并作为上下文提供给模型。

8.2.1 定义知识库问答接口

java 复制代码
import dev.langchain4j.service.SystemMessage;

interface KnowledgeBaseAssistant {
    @SystemMessage("你是一个知识库助手,请基于提供的上下文回答问题。如果上下文不足以回答,请说明你不知道。")
    String answer(String query);
}

注意:我们不需要在提示词里显式说"根据以下上下文",因为 LangChain4j 会自动将检索到的内容注入到用户消息之前。默认的注入模板是:

复制代码
你是一个助手,请基于以下信息回答问题。
信息:
{{contents}}

问题:{{query}}

我们也可以自定义提示词,稍后介绍。

8.2.2 构建 AI Service 并注入检索器

java 复制代码
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;

// 模型(用于生成答案)
OpenAiChatModel chatModel = OpenAiChatModel.builder()
        .baseUrl("http://langchain4j.dev/demo/openai/v1")
        .apiKey("demo")
        .build();

// 假设我们有之前构建的 embeddingStore 和 embeddingModel
// ContentRetriever retriever = ... 如上所示

KnowledgeBaseAssistant assistant = AiServices.builder(KnowledgeBaseAssistant.class)
        .chatLanguageModel(chatModel)
        .contentRetriever(retriever)   // 注入检索器!
        .build();

// 测试提问
String answer = assistant.answer("什么是 RAG?");
System.out.println(answer);

8.2.3 完整可运行示例

将上一章的摄入代码和本章的检索代码合并,写成一个完整的可运行类 RagDemo.java。为了便于测试,我们直接在内存中加载文档并检索。

java 复制代码
package com.example;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
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 java.nio.file.Paths;

interface RagAssistant {
    String chat(String userMessage);
}

public class RagDemo {
    public static void main(String[] args) {
        // ========== 1. 知识摄入 ==========
        // 加载文档
        Document document = FileSystemDocumentLoader.loadDocument(Paths.get("knowledge.txt"));

        // 嵌入模型和向量存储
        EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();
        EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();

        // 摄入
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .documentSplitter(DocumentSplitters.recursive(300, 30))
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .build();
        ingestor.ingest(document);

        // ========== 2. 创建检索器 ==========
        ContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
                .embeddingStore(embeddingStore)
                .embeddingModel(embeddingModel)
                .maxResults(3)
                .build();

        // ========== 3. 创建聊天模型 ==========
        OpenAiChatModel chatModel = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        // ========== 4. 构建 AI Service ==========
        RagAssistant assistant = AiServices.builder(RagAssistant.class)
                .chatLanguageModel(chatModel)
                .contentRetriever(retriever)
                .build();

        // ========== 5. 测试 ==========
        String answer = assistant.chat("什么是 RAG?");
        System.out.println("AI: " + answer);
    }
}

运行后,AI 应该能基于 knowledge.txt 的内容准确回答 RAG 的定义。如果问题超出知识库范围,AI 会表示不知道。

8.3 高级 RAG:使用 RetrievalAugmentor 定制流程

ContentRetriever 已经能完成基本的 RAG,但实际场景中往往需要更精细的控制,比如:

  • 在检索前对用户查询进行改写(扩展、压缩、假设性问题生成)
  • 从多个数据源检索(本地文档、数据库、网络)
  • 对检索结果进行重排序、过滤
  • 自定义上下文注入方式

LangChain4j 提供了 RetrievalAugmentor 接口和默认实现 DefaultRetrievalAugmentor,允许你组合这些高级组件。

8.3.1 RetrievalAugmentor 的核心组件

graph LR Q[用户查询] --> QT[QueryTransformer
查询转换器] QT --> QR[QueryRouter
查询路由器] QR --> CR1[ContentRetriever 1] QR --> CR2[ContentRetriever 2] CR1 --> CA[ContentAggregator
内容聚合器] CR2 --> CA CA --> CI[ContentInjector
内容注入器] CI --> P[最终提示词]
  • QueryTransformer :对原始查询进行转换,例如生成多个查询变体、压缩历史对话、假设性问题生成等。LangChain4j 内置了 CompressingQueryTransformer(结合对话历史压缩查询)、ExpandingQueryTransformer(扩展查询)等。
  • QueryRouter :决定将查询路由到哪个或哪些 ContentRetriever。可以实现多源检索(如本地知识库 + 网络搜索)。
  • ContentAggregator:合并来自多个检索器的结果,并进行排序、去重、重排(rerank)。
  • ContentInjector:决定如何将检索到的内容注入到用户消息中。你可以自定义提示词模板。

8.3.2 示例:使用 CompressingQueryTransformer

当有对话历史时,用户的后续查询可能不完整(例如"它是什么意思?"),需要结合历史才能理解。CompressingQueryTransformer 可以将对话历史和当前查询压缩成一个独立的查询,提升检索质量。

首先,我们需要 AI Service 支持记忆(使用 @MemoryId),然后配置 RetrievalAugmentor

java 复制代码
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.rag.DefaultRetrievalAugmentor;
import dev.langchain4j.rag.query.transformer.CompressingQueryTransformer;
import dev.langchain4j.service.MemoryId;

interface RagWithMemory {
    String chat(@MemoryId int memoryId, String userMessage);
}

// 创建 RetrievalAugmentor
RetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder()
        .queryTransformer(new CompressingQueryTransformer(chatModel))  // 需要另一个模型来压缩查询
        .contentRetriever(retriever)
        .build();

// 构建 AI Service
RagWithMemory assistant = AiServices.builder(RagWithMemory.class)
        .chatLanguageModel(chatModel)
        .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(10)
                .build())
        .retrievalAugmentor(augmentor)   // 注入 augmentor
        .build();

注意CompressingQueryTransformer 内部需要调用一个模型来执行压缩,因此需要传入 chatModel。这会额外消耗一次模型调用,但能显著提升多轮对话中的检索准确性。

8.3.3 示例:多源检索

假设我们除了本地文档,还想在用户询问天气时检索网络信息。我们可以定义两个 ContentRetriever:一个本地向量检索,一个网络搜索检索(需要你自己实现,或调用搜索 API),然后用 QueryRouter 根据查询内容路由。

java 复制代码
// 定义两个检索器
ContentRetriever localRetriever = ...;
ContentRetriever webRetriever = new WebSearchContentRetriever(); // 假设实现了

// 创建路由规则:根据查询关键词决定走哪个检索器
QueryRouter router = (query, chatMemory) -> {
    if (query.text().contains("天气")) {
        return List.of(webRetriever);
    } else {
        return List.of(localRetriever);
    }
};

RetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder()
        .queryRouter(router)
        .contentAggregator(DefaultContentAggregator.builder()  // 默认聚合器,直接合并
                .build())
        .build();

更复杂的场景可以使用 QueryRouter 同时调用多个检索器,再用 ContentAggregator 进行融合排序。

8.3.4 自定义 ContentInjector

默认情况下,检索到的内容会以如下格式插入到用户消息前面:

css 复制代码
你是一个助手,请基于以下信息回答问题。
信息:
1. [片段1]
2. [片段2]
...
问题:[用户查询]

如果你想自定义提示词,可以实现自己的 ContentInjector

java 复制代码
ContentInjector customInjector = (contents, userMessage) -> {
    StringBuilder sb = new StringBuilder("根据以下参考资料,回答用户的问题。\n\n参考资料:\n");
    for (int i = 0; i < contents.size(); i++) {
        sb.append(i+1).append(". ").append(contents.get(i).textSegment().text()).append("\n");
    }
    sb.append("\n用户问题:").append(userMessage.singleText());
    return UserMessage.from(sb.toString());
};

RetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder()
        .contentRetriever(retriever)
        .contentInjector(customInjector)
        .build();

8.4 实战:构建一个能同时检索本地文档和网页的 RAG 助手

结合以上知识,我们尝试构建一个更贴近实际的应用:它能回答两类问题:

  • 关于公司内部制度的问题(从本地文档检索)
  • 关于实时天气的问题(从网络搜索检索)

我们简化实现:网络搜索用模拟数据代替,重点演示路由和多源检索。

8.4.1 实现一个模拟网络搜索的检索器

java 复制代码
import dev.langchain4j.rag.content.Content;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.query.Query;

import java.util.List;
import java.util.Random;

public class MockWebSearchRetriever implements ContentRetriever {
    @Override
    public List<Content> retrieve(Query query) {
        // 模拟根据查询返回网络搜索结果
        String text = switch (query.text().toLowerCase()) {
            case "今天天气怎么样?" -> "北京今天晴,25℃;上海多云,28℃。";
            default -> "未找到相关天气信息。";
        };
        return List.of(Content.from(text));
    }
}

8.4.2 定义带记忆的 AI Service 接口

java 复制代码
interface MultiSourceAssistant {
    String chat(@MemoryId String sessionId, @UserMessage String message);
}

8.4.3 构建带检索增强器的 AI Service

java 复制代码
public class MultiSourceRagDemo {
    public static void main(String[] args) {
        // 假设已有本地检索器 localRetriever(从文档摄入得来)
        ContentRetriever localRetriever = ...; 
        ContentRetriever webRetriever = new MockWebSearchRetriever();

        // 查询路由器:如果查询包含"天气",用网络检索,否则用本地检索
        QueryRouter router = (query, chatMemory) -> {
            if (query.text().contains("天气")) {
                return List.of(webRetriever);
            } else {
                return List.of(localRetriever);
            }
        };

        // 构建增强器
        RetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder()
                .queryRouter(router)
                .contentAggregator(DefaultContentAggregator.builder().build())
                .build();

        // 模型
        OpenAiChatModel chatModel = OpenAiChatModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        // 记忆提供者
        var memories = new ConcurrentHashMap<Object, ChatMemory>();
        ChatMemoryProvider memoryProvider = memoryId -> 
            memories.computeIfAbsent(memoryId, id ->
                MessageWindowChatMemory.builder().id(id).maxMessages(10).build());

        // 构建 AI Service
        MultiSourceAssistant assistant = AiServices.builder(MultiSourceAssistant.class)
                .chatLanguageModel(chatModel)
                .chatMemoryProvider(memoryProvider)
                .retrievalAugmentor(augmentor)
                .build();

        // 测试
        System.out.println(assistant.chat("user1", "什么是 RAG?"));   // 走本地检索
        System.out.println(assistant.chat("user1", "今天天气怎么样?")); // 走网络检索
        System.out.println(assistant.chat("user2", "我也想知道天气"));   // user2 独立记忆
    }
}

这个例子虽然简化了网络检索的实现,但完整演示了如何构建多源 RAG 系统。你可以将 MockWebSearchRetriever 替换为真实的搜索 API(如 SerpAPI、Bing Search API)。

8.5 本章小结

  • ContentRetriever:连接向量存储,检索相关知识。
  • 集成到 AI Service :通过 contentRetriever 参数让 AI 自动基于检索内容回答问题。
  • 高级定制 :使用 RetrievalAugmentor 和其组件(QueryTransformer、QueryRouter、ContentAggregator、ContentInjector)实现复杂的 RAG 逻辑。
  • 实战:构建了一个能区分本地文档和网络搜索的多源 RAG 助手。
相关推荐
颜酱2 小时前
单调队列:滑动窗口极值问题的最优解(通用模板版)
javascript·后端·算法
Java水解2 小时前
Rust嵌入式开发实战——从ARM裸机编程到RTOS应用
后端·rust
AI探索者2 小时前
LangGraph 条件路由:构建支持工具调用的智能 Agent
后端
苍何2 小时前
终于,我把 Openclaw 加 Seed2.0 Skills 做 AI 漫剧搞定了
后端
苍何2 小时前
阿里出手,最强Coding Plan出炉,OpenClaw可以痛快玩了
后端
风象南3 小时前
Claude Code这个隐藏技能,让我告别PPT焦虑
人工智能·后端
神奇小汤圆3 小时前
为什么 Spring 强烈推荐你用 singleton
后端
Java编程爱好者3 小时前
面试必问:Semaphore 凭什么靠 AQS + CAS 实现限流?
后端