一、开篇:为什么Java开发者需要LangChain4j
1.1 大模型浪潮下,Java 开发者的机遇与挑战
过去两年,大语言模型(LLM)席卷全球,从 ChatGPT 到各种垂直领域模型,AI 能力正以前所未有的速度渗透到各行各业。作为企业级后端的中流砥柱,Java 开发者自然需要思考:如何将大模型的能力无缝集成到现有的 Java 系统中?
直接调用大模型 API(例如 OpenAI 的接口)听起来很简单——发一个 HTTP 请求,拿回一段文本。但在实际生产环境中,我们会面临一系列棘手的问题:
- 复杂的调用细节:需要手动构建 JSON 请求体、处理 HTTP 连接、解析流式响应、处理鉴权和错误重试。
- 提示词管理困难:业务场景往往需要动态构造提示词,拼接用户输入、历史对话、系统指令,代码很快就变得难以维护。
- 对话状态维护:实现一个多轮对话机器人,必须自己维护会话历史,并在每次请求时把历史消息都带上。
- 输出不可控:大模型返回的是自然语言文本,如果想让 AI 返回结构化的数据(例如 JSON、对象),还需要自己编写解析器和异常处理。
- 知识库集成复杂:要让模型基于企业内部知识回答问题(RAG),需要自己实现文档加载、文本分割、向量化、向量检索等一系列组件。
如果每个项目都从零开始重复造这些轮子,不仅开发效率低,而且容易出错,更难以应对模型切换、版本升级等变化。
1.2 原始调用方式:一个直观的对比
让我们先看一段最原始的 Java 代码,它使用 HttpClient 调用 OpenAI 的聊天接口:
// 原始方式:直接调用 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 重写上面的功能:
// 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<br>声明式接口] --> B[ChatLanguageModel<br>对话模型]
A --> C[ChatMemory<br>记忆管理]
A --> D[Tool<br>工具调用]
A --> E[OutputParser<br>输出解析]
A --> F[ContentRetriever<br>知识检索]
F --> G[EmbeddingStore<br>向量存储]
F --> H[EmbeddingModel<br>嵌入模型]
B --> I[OpenAI]
B --> J[Azure]
B --> K[本地模型<br>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 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 文件,内容如下:
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:
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-mini、gpt-3.5-turbo。如果不指定,会使用默认模型。
model.chat(userMessage):这是最核心的方法。传入用户消息(字符串),返回 AI 的回复(字符串)。背后自动处理了 HTTP 请求、JSON 解析、错误处理等。- 打印输出:简单展示对话内容。
运行效果
在 IDE 中直接运行 main 方法,控制台将输出类似以下内容:
用户: 你好,请用一句话介绍你自己。
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 镜像(如阿里云镜像)。
- 运行时报错:确认
baseUrl和apiKey正确无误;如果使用真实 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 {
<<interface>>
+Role role()
+String text()
}
class SystemMessage {
+String text
}
class UserMessage {
+String text
}
class AiMessage {
+String text
}
ChatMessage <|-- SystemMessage
ChatMessage <|-- UserMessage
ChatMessage <|-- AiMessage
在调用模型时,我们需要传入一个包含多条消息的列表。例如,包含系统消息和用户消息的请求:
List<ChatMessage> messages = Arrays.asList(
new SystemMessage("你是一个乐于助人的Java编程助手,回答要简洁。"),
new UserMessage("Java 8 和 Java 11 的主要区别是什么?")
);
3.2 使用系统消息设定 AI 角色
我们先通过一个例子来感受系统消息的作用。创建一个新类 SystemMessageDemo.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:
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:
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:
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用户消息,调用模型后addAI 回复。如果不addAI 回复,AI 就看不到自己的历史回答,可能造成上下文断裂。 - 调用
model.chat(allMessages)时,传入的是当前完整的历史记录,模型据此生成有上下文的回复。
3.4.4 使用 ChatMemory 的注意事项
- 记忆的持久化:
MessageWindowChatMemory是内存存储,应用重启后对话消失。如果需要长期保存(如数据库),可以实现自己的ChatMemory或结合ChatMemoryStore。 - 多用户隔离:在多用户 Web 应用中,每个用户应该拥有独立的
ChatMemory实例。我们将在第五章详细讲解如何通过@MemoryId实现。 - Token 消耗:保留的消息越多,每次请求消耗的 Token 也越多。合理设置
maxMessages平衡上下文长度和成本。
3.5 本章小结
- 消息类型:
SystemMessage、UserMessage、AiMessage的作用和用法。 - 提示词模板:用
PromptTemplate动态构造用户消息,避免字符串拼接。 - 多轮对话:从手动维护消息列表到使用
ChatMemory管理对话历史,实现带上下文的聊天机器人。
现在,你可以用这些知识构建一个简单的客服机器人或知识问答助手了。
四、声明式AI:AI Services 组件
通过前几章的学习,已经能够用几行代码调用大模型,并实现多轮对话。但是,你是否觉得每次都要手动构造消息列表、处理历史记忆还是有点繁琐?如果能让 AI 像调用本地方法一样简单,那该多好!
这正是 AI Services 要解决的问题。它是 LangChain4j 最强大的高级抽象,让你通过定义 Java 接口来声明式地使用 AI,框架会在背后自动实现所有细节。
4.1 什么是 AI Services?为何它能极大简化代码?
AI Services 是 LangChain4j 的核心概念之一。它的工作方式是:
- 你定义一个 Java 接口,并在接口方法上添加注解来描述 AI 的行为(例如使用什么系统提示、如何处理输出)。
- LangChain4j 在运行时动态生成该接口的实现类。
- 当你调用接口方法时,框架自动处理:
- 根据注解和方法参数构造提示词
- 调用大模型
- 解析模型输出为你期望的 Java 类型(String、POJO、List 等)
- 如果配置了记忆,自动管理对话历史
- 如果配置了工具,自动触发函数调用
这意味着你几乎不用写任何实现代码,只需要关注业务逻辑。这非常符合 Java 开发者熟悉的“面向接口编程”风格。
工作原理示意图:
graph TD
A[定义AI Service接口<br>如 Assistant] --> B[在接口方法上添加注解<br>SystemMessage, UserMessage]
B --> C[使用 AiServices.create<br>生成实现类]
C --> D[调用接口方法]
D --> E[LangChain4j 自动处理:<br>1. 构造提示词<br>2. 调用模型<br>3. 解析输出<br>4. 管理记忆]
E --> F[返回结果给调用方]
4.2 定义第一个 AI Service 接口:一个简单的聊天助手
我们从最简单的例子开始:定义一个聊天助手接口,只有一个方法,接收用户消息并返回 AI 回复。
代码实现
创建一个新类 AiServiceDemo.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:
import dev.langchain4j.service.SystemMessage;
interface Assistant {
@SystemMessage("你是一个 Java 编程导师,回答要简洁并鼓励用户学习。")
String chat(String userMessage);
}
然后重新运行 AiServiceDemo,观察输出是否更符合导师角色。
4.3.2 使用变量占位符:{{it}} 和 {{变量名}}
有时候系统消息或用户消息需要动态插入变量。例如,导师可以根据不同的用户水平调整语气。这时可以使用占位符。
占位符规则:
{{it}}代表方法的第一个参数(如果只有一个参数)。{{变量名}}代表方法参数中带有@V("变量名")注解的参数。
示例 1:使用 {{it}}
interface Assistant {
@SystemMessage("你是一个乐于助人的助手。")
@UserMessage("请用中文回答:{{it}}")
String chat(String userMessage);
}
这里 {{it}} 会被方法参数 userMessage 的值替换。这样即使方法参数名随意,也能正确填充。
示例 2:使用多个变量
如果方法有多个参数,需要给每个参数命名,然后用 @V 注解标记:
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 方法中:
Translator translator = AiServices.create(Translator.class, model);
String result = translator.translate("Hello, world", "中文");
System.out.println(result); // 输出:你好,世界
框架会自动将 text 和 targetLanguage 填充到模板中,生成完整的用户消息。
4.3.3 实践:带角色和变量的 AI Service
创建一个完整的示例 AnnotatedAiServiceDemo.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 类:
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 对象:
import dev.langchain4j.service.UserMessage;
interface PersonExtractor {
@UserMessage("从以下文本中提取人物信息:{{it}}")
Person extractPerson(String text);
}
在 main 方法中:
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);
String text = "我叫张三,今年28岁,住在上海。";
Person person = extractor.extractPerson(text);
System.out.println(person);
运行后,你会看到类似输出:
Person{name='张三', age=28, city='上海'}
背后原理:LangChain4j 会在发送给模型的提示词中隐式地要求模型以 JSON 格式输出,然后将 JSON 反序列化为目标类。这一切对开发者透明。
4.4.2 支持的类型
除了 POJO,AI Services 还支持:
- 基础类型:
int、double、boolean等 - 集合类型:
List<String>、Set<Integer>等 - 枚举:例如将情感分类为
POSITIVE、NEUTRAL、NEGATIVE
示例:返回枚举列表
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>:
import dev.langchain4j.service.Result;
interface Assistant {
@SystemMessage("你是一个 Java 编程导师。")
Result<String> chat(String userMessage);
}
在 main 中调用并获取元数据:
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);
输出类似:
回答:多态是面向对象编程的重要特性,指同一个方法在不同对象上有不同实现...
消耗 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:
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 注解:
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() 方法正确实现。常见类型:
- 整数类型:
int、long - 字符串:
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 表达式或方法引用:
ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder()
.id(memoryId) // 给记忆设置一个ID(可选,便于调试)
.maxMessages(10) // 每个记忆最多保留10条消息
.build();
然后将这个 provider 传递给 AiServices。
5.3.2 完整示例:多用户隔离的助手
创建 MemoryIsolationDemo.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);
}
}
运行输出示例:
用户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 定义客服助手接口
interface CustomerServiceAssistant {
@SystemMessage("你是一个电商客服助手,友好、专业,帮助用户解答购物相关问题。")
String chat(@MemoryId String sessionId, @UserMessage String message);
}
这里使用 String 类型的 sessionId,可以是用户 ID 或会话令牌。
5.4.2 实现支持多用户的客服服务
创建一个可运行的类 CustomerServiceDemo.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();
}
}
运行演示:
> 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 示例(生产级):
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 接口
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 实现。
示例伪代码(持久化到数据库):
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 时使用:
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 最简单的工具:获取当前时间
让我们从一个极简的例子开始。创建一个工具类,其中包含一个获取当前时间的方法:
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() 来添加工具实例:
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 应该能正确回答当前时间。背后的过程:
- AI 识别出需要知道当前时间才能回答。
- AI 返回一个工具调用请求(调用
getCurrentTime)。 - LangChain4j 自动执行
timeTool.getCurrentTime()。 - 将结果(例如 "14:30:45")返回给 AI。
- AI 生成最终回答,如“现在是下午2点30分45秒。”
6.3 带参数的工具
很多工具需要参数。例如,查询天气需要城市名,查询订单需要订单号。工具方法可以定义参数,AI 在调用时会自动提取参数值。
6.3.1 示例:查询天气
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 的构建方式与之前相同:
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 注解中通过 value 和 array 提供更详细的参数描述。LangChain4j 也支持使用 Javadoc 或 @Parameter 注解来描述参数,但目前最简单的是在工具描述中说明。
如果需要更精细的控制(如参数是否必填、参数说明),可以使用 @Tool 的 value 和 name 属性,结合工具方法签名。
示例:带有参数描述的 Javadoc(会被 LangChain4j 自动提取)
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 定义工具类
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 并测试
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 是一个函数式接口,定义如下:
public interface ToolProvider {
List<ToolSpecification> provideTools(ToolProviderRequest request);
default ToolExecutor toolExecutor(String toolName) { ... }
}
实现这个接口可以更精细地控制工具的提供和执行。
6.5.2 示例:基于用户角色提供不同工具
假设我们有用户对象,包含角色信息。我们需要在调用时传入用户角色,让 AI Service 根据角色决定是否提供某些敏感工具。
首先,定义两个工具类:普通工具和管理员工具。
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 方法内部进行权限检查,而不是动态移除工具。这样实现简单,且易于理解。
权限检查示例:
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 工具调用的注意事项
- 工具方法应该是线程安全的:如果多个用户同时调用同一个工具实例,方法需要能够正确处理并发。
- 工具方法的执行时间:如果工具执行耗时较长(如调用外部API),可能会影响用户体验。建议将耗时操作异步化,或设置合理的超时。
- 错误处理:工具方法可能抛出异常,你应该捕获并返回友好的错误信息,或者让异常传播(框架会捕获异常并告诉 AI 调用失败)。
- 工具描述的重要性:清晰的描述能大大提高 AI 调用工具的准确率。描述应包括工具的作用、何时使用、参数的格式等。
- 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 通过引入一个外部知识检索步骤来解决这些问题。它的基本流程是:
- 用户提问
- 系统从知识库中检索与问题相关的片段(比如文档段落)
- 将检索到的片段作为上下文,连同问题一起发给大模型
- 模型基于提供的上下文生成答案
这样,模型的回答就有了事实依据,大大降低幻觉,而且可以随时更新知识库而无需重新训练模型。
RAG 标准流程示意图:
graph TD
subgraph 离线阶段(知识摄入)
A[原始文档<br>PDF/Word/TXT] --> B[文档加载器]
B --> C[文档分割器<br>将文档切成小块]
C --> D[嵌入模型<br>将每个小块转为向量]
D --> E[向量数据库<br>存储向量+原始文本]
end
subgraph 在线阶段 (问答)
F[用户问题] --> G[嵌入模型<br>将问题转为向量]
G --> H[向量检索<br>在数据库中找相似片段]
H --> I[将检索到的片段作为上下文]
I --> J[大语言模型<br>基于上下文生成答案]
J --> K[最终回答]
end
从图中可以看出,RAG 分为两大阶段:知识摄入(Ingestion)和问答检索(Retrieval & Generation)。本章我们先完成知识摄入部分,下一章实现问答。
7.2 知识摄入(Ingestion)详解
知识摄入是将原始文档处理成可供检索的形式的过程。LangChain4j 提供了完整的工具链,让我们一步步操作。
7.2.1 文档加载器(DocumentLoader)
文档加载器负责从各种来源读取文档。LangChain4j 内置了文件系统加载器,支持从目录加载多个文件。
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 工具类,包含多种分割策略。最常用的是递归分割器,它会根据段落、句子、单词等层级递归切割,尽量保持语义完整。
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 密钥。
首先添加依赖(如果之前没加的话):
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
<version>${langchain4j.version}</version>
</dependency>
创建嵌入模型实例:
import dev.langchain4j.model.embedding.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();
7.2.4 向量存储(EmbeddingStore)
向量存储负责保存向量以及对应的原始文本块,并提供相似度检索功能。LangChain4j 提供了多种实现:
- InMemoryEmbeddingStore:内存存储,适合测试和小型应用。
- ElasticsearchEmbeddingStore、PineconeEmbeddingStore、ChromaEmbeddingStore:对接专业向量数据库。
- PGvector:通过 PostgreSQL 的 pgvector 插件存储。
入门阶段,我们使用 InMemoryEmbeddingStore:
import dev.langchain4j.store.embedding.InMemoryEmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStore;
EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
7.2.5 使用 EmbeddingStoreIngestor 一键完成摄入
手动一步步操作(分割、嵌入、存储)虽然清晰,但代码稍显繁琐。LangChain4j 提供了 EmbeddingStoreIngestor,它将分割器、嵌入模型、向量存储组合起来,只需一行代码完成摄入。
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:
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 方法,控制台输出类似:
文档已成功摄入!
向量存储内容: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 验证向量存储中已有数据
虽然我们无法直接查看向量,但可以通过检索来验证。不过检索属于问答阶段,我们留到下一章详细讲解。现在,我们只需要确认摄入过程没有报错即可。
如果你想简单验证,可以在摄入后添加以下代码,手动检索一个问题:
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,它会:
- 将用户查询转为向量
- 在向量存储中搜索最相似的片段
- 返回这些片段作为检索结果
8.1.1 创建 EmbeddingStoreContentRetriever
继续使用上一章构建的 embeddingStore 和 embeddingModel:
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<br>转为向量]
E --> S[EmbeddingStore<br>相似度搜索]
S --> R[返回 Top N 匹配的 TextSegment]
R --> C[包装为 Content 列表]
C --> A[传递给 AI Service]
8.2 将 ContentRetriever 集成到 AI Service
现在我们把检索器注入到 AI Service 中,让它在每次用户提问时自动检索相关知识,并作为上下文提供给模型。
8.2.1 定义知识库问答接口
import dev.langchain4j.service.SystemMessage;
interface KnowledgeBaseAssistant {
@SystemMessage("你是一个知识库助手,请基于提供的上下文回答问题。如果上下文不足以回答,请说明你不知道。")
String answer(String query);
}
注意:我们不需要在提示词里显式说“根据以下上下文”,因为 LangChain4j 会自动将检索到的内容注入到用户消息之前。默认的注入模板是:
你是一个助手,请基于以下信息回答问题。
信息:
{{contents}}
问题:{{query}}
我们也可以自定义提示词,稍后介绍。
8.2.2 构建 AI Service 并注入检索器
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。为了便于测试,我们直接在内存中加载文档并检索。
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<br>查询转换器]
QT --> QR[QueryRouter<br>查询路由器]
QR --> CR1[ContentRetriever 1]
QR --> CR2[ContentRetriever 2]
CR1 --> CA[ContentAggregator<br>内容聚合器]
CR2 --> CA
CA --> CI[ContentInjector<br>内容注入器]
CI --> P[最终提示词]
QueryTransformer:对原始查询进行转换,例如生成多个查询变体、压缩历史对话、假设性问题生成等。LangChain4j 内置了CompressingQueryTransformer(结合对话历史压缩查询)、ExpandingQueryTransformer(扩展查询)等。QueryRouter:决定将查询路由到哪个或哪些ContentRetriever。可以实现多源检索(如本地知识库 + 网络搜索)。ContentAggregator:合并来自多个检索器的结果,并进行排序、去重、重排(rerank)。ContentInjector:决定如何将检索到的内容注入到用户消息中。你可以自定义提示词模板。
8.3.2 示例:使用 CompressingQueryTransformer
当有对话历史时,用户的后续查询可能不完整(例如“它是什么意思?”),需要结合历史才能理解。CompressingQueryTransformer 可以将对话历史和当前查询压缩成一个独立的查询,提升检索质量。
首先,我们需要 AI Service 支持记忆(使用 @MemoryId),然后配置 RetrievalAugmentor。
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 根据查询内容路由。
// 定义两个检索器
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
默认情况下,检索到的内容会以如下格式插入到用户消息前面:
你是一个助手,请基于以下信息回答问题。
信息:
1. [片段1]
2. [片段2]
...
问题:[用户查询]
如果你想自定义提示词,可以实现自己的 ContentInjector。
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 实现一个模拟网络搜索的检索器
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 接口
interface MultiSourceAssistant {
String chat(@MemoryId String sessionId, @UserMessage String message);
}
8.4.3 构建带检索增强器的 AI Service
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 助手。