目录
LangChain官网:LangChain4j,官方英文文档:Get Started | LangChain4j,API 文档:Overview (LangChain4j)
非官方中文文档:快速开始 | LangChain4j 中文文档
官方示例代码仓库:langchain4j/langchain4j-examples
LangChain4j版本:1.14.0-beta24
一、聊天记忆ChatMemory
1.1、什么是聊天记忆
大语言模型本身是无状态的,每次调用都是独立的请求,并不记得上一次对话说了什么,如果问“我的订单号是多少”,它回答了;你再问“帮我取消它”,模型会胡言乱语,因为它并不知道“它”指什么。
而给对话加入 聊天记忆(Chat Memory) 就可以解决这个问题,即在每次对话中,把历史消息(SystemMessage、UserMessage、AiMessage、ToolMessage)按顺序存起来,在请求时一起发给模型,这样一来模型知道前面的对话聊了什么,本次回答就能跟着之前对话的进度回答了。
在 LangChain4j 框架中,ChatMemory 就是用来实现聊天记忆的接口,它本质上是一个消息容器,主要负责维护当前会话的历史消息列表,并在每次请求时决定保留哪些、丢弃哪些、如何组装发送给模型。
在目前的大语言模型中,每个模型都有上下文的最大 token 限制,当上下文过长时,给大模型发送的聊天记录就得通过淘汰策略只保留部分数据,根据淘汰策略的不同,ChatMemory 有两个实现类:
| 实现类 | 淘汰策略 | 适用场景 | 性能消耗 |
|---|---|---|---|
| MessageWindowChatMemory | 超出时按消息条数淘汰最早的消息,保留最近的 N 条 | 对话轮数不多,但每轮可能很长(如代码生成) | 内存占用低,CPU 消耗低 |
| TokenWindowChatMemory | 按Token 数量淘汰,保留最近的 N 个 Token 的消息,超限则移除最早的消息,直至总 token 数不超过阈值 | 对话很长,需要按 Token 预算精准控制 | 内存占用中,CPU 消耗高 |
关于 TokenWindowChatMemory 的分词方式:不同模型的分词方式不同,用错 TokenCountEstimator 会导致估算偏差。
在 LangChain4j 中,TokenWindowChatMemory 并不是按照消息条数或字符长度来管理上下文,而是按照 Token 数量 来控制历史消息窗口。它会在每次新增消息后,统计当前会话的总 Token 数,如果超过设定的上限,则自动移除较早的整条消息,以确保最终发送给大模型的上下文不会超出模型的上下文窗口限制。
由于不同大模型采用的 Tokenizer(分词器)实现并不相同,因此同一段文本在不同模型下得到的 Token 数量可能存在明显差异。例如,OpenAI GPT 系列使用基于 BPE(Byte Pair Encoding)的分词算法,Qwen 系列、DeepSeek 系列以及部分国产模型则可能采用各自优化的分词策略。对于中文、英文、代码、特殊符号等内容,不同 Tokenizer 的切分结果往往并不一致。
对于许多国产模型,例如 Qwen、DeepSeek、GLM、Moonshot 等,LangChain4j 的社区包中有可能提供对应的 TokenCountEstimator,需要额外导入依赖才能使用。
1.2、实现记忆功能
官方文档:Chat Memory | LangChain4j
1.2.1、编程式
只需要新建一个 ChatMemory 对象实例,然后在 AiServices.builder() 时使用 chatMemory() 方法传入 ChatMemory 对象即可:
// 基于消息条数,保留最近 10 条消息
ChatMemory messageWindowChatMemory = MessageWindowChatMemory.withMaxMessages(10);
// 基于 Token 数量,保留最近 2000 Tokens,需配合 OpenAiTokenCountEstimator 或其他分词器
ChatMemory tokenWindowChatMemory = TokenWindowChatMemory.withMaxTokens(2000, new OpenAiTokenCountEstimator("gpt-4"));
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.chatMemory(messageWindowChatMemory) // 注入 ChatMemory 对象
.build();
// 多轮对话
String answer1 = assistant.chat("我叫张三"); // 模型回复:"你好张三"
String answer2 = assistant.chat("我叫什么名字?"); // 模型回复:"你叫张三"
1.2.2、声明式 + @MemoryId实现会话隔离
在多用户、多会话场景下,不同的用户、不同的对话线程需要有独立的聊天记忆,互不干扰。LangChain4j 通过 @MemoryId 注解和 ChatMemoryProvider 来实现会话隔离功能。
@MemoryId:用于标注 AI Service 接口方法的参数,表示该参数是会话或用户的唯一标识符,其通常是用户 ID 或会话 ID,值可以是 String、Long 等任意类型。框架会根据这个标识符,通过 ChatMemoryProvider 获取或创建对应的 ChatMemory 实例。
ChatMemoryProvider:根据 @MemoryId 动态获取或创建对应的 ChatMemory 实例。
在 @AiService 声明的接口中,在方法中添加一个 @MemoryId 注解的参数:
@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel")
public interface Assistant {
@SystemMessage("你是一个友好的AI助手")
String chat(@MemoryId String memoryId, String userMessage);
}
调用时传入不同的 memoryId,框架会自动为每个ID创建和维护独立的聊天记忆:
@RestController
public class ChatController {
@Autowired
private Assistant assistant;
@GetMapping("/chat")
public String chat(@RequestParam("userId") String userId,
@RequestParam("message") String message) {
// userId被当作memoryId,不同用户有独立的记忆
return assistant.chat(userId, message);
}
}
如果不想提前创建所有用户的 ChatMemory,可以单独在配置类中创建一个 ChatMemoryProvider:
@Configuration
public class ChatMemoryConfig {
@Bean
public ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.builder()
.maxMessages(20)
.id(memoryId)
.build();
}
}
ChatMemoryProvider 是一个函数式接口,每次遇到新的 memoryId 时自动创建新的 ChatMemory。与直接注入 chatMemory 的区别在于:chatMemory() 直接绑定一个固定的 ChatMemory 实例,chatMemoryProvider 是按 memoryId 隔离的多例。
注入创建的这个 ChatMemoryProvider 实例也很简单,可以通过 AiServices.builder() 时调用 chatMemoryProvider() 方法:
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.chatMemoryProvider(provider) // 注入自定义的chatMemoryProvider
.build();
或者通过 @AiService 注解的 “chatMemoryProvider” 属性绑定:
@AiService(wiringMode = EXPLICIT
, chatModel = "qwenChatModel"
, chatMemoryProvider = "chatMemoryProvider"
)
public interface Assistant {
@SystemMessage("你是一个能力强大的AI助手")
String chat(@MemoryId String userId, String userQuestion);
}
1.3、记忆持久化(ChatMemoryStore)
官方文档:chat-memory#persistence | LangChain4j
默认情况下,MessageWindowChatMemory 和 TokenWindowChatMemory 都使用 InMemoryChatMemoryStore,也就是将所有的聊天记忆存储在内存中,当应用重启后记忆会丢失。
如果要自定义记忆的持久化方式,就需要重写 LangChain4j 提供的 ChatMemoryStore 接口,其默认存在 InMemoryChatMemoryStore 和 SingleSlotChatMemoryStore 两个基于内存的实现类:
- InMemoryChatMemoryStore:支持多用户/多会话的数据隔离,内部维护了一个线程安全的哈希表,以
memoryId作为键(Key),将每个用户的聊天记录列表作为值(Value)进行存储。适用于需要为不同用户或不同会话提供独立记忆空间的场景 - SingleSlotChatMemoryStore:所有消息共享同一个存储空间,不支持会话隔离,内部仅维护了一个简单的列表,无论传入什么
memoryId,它都会直接操作这唯一的一个集合。所有的增删改查都在这一个列表中完成。适用于单一的全局机器人或测试环境
ChatMemoryStore 接口本身有三个方法,实现这三个方法即可:
public interface ChatMemoryStore {
List<ChatMessage> getMessages(Object memoryId); // 获取某用户的记忆
void updateMessages(String memoryId, List<ChatMessage> messages); // 更新记忆
void deleteMessages(String memoryId); // 删除记忆
}
以持久化到 Redis 和 MongoDB 为例来说明。
1.3.1、Redis
引入 相关 Maven 坐标:
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Redis 的配置信息:
spring:
data:
redis:
host: localhost
port: 6379
password:
database: 0
LangChain4j 内置了消息列表的 JSON 序列化/反序列化工具类,使用 StringRedisTemplate 存储 JSON 格式的消息列表:
@Component
public class RedisChatMemoryStore implements ChatMemoryStore {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String KEY_PREFIX = "chat:memory:";
@Override
public List<ChatMessage> getMessages(String memoryId) {
String key = KEY_PREFIX + memoryId;
String json = redisTemplate.opsForValue().get(key);
if (json == null || json.isEmpty()) {
return new ArrayList<>();
}
return ChatMessageDeserializer.messagesFromJson(json);
}
@Override
public void updateMessages(String memoryId, List<ChatMessage> messages) {
String key = KEY_PREFIX + memoryId;
String json = ChatMessageSerializer.messagesToJson(messages);
// 设置过期时间,比如 7 天,避免 Redis 无限膨胀
redisTemplate.opsForValue().set(key, json, Duration.ofDays(7));
}
@Override
public void deleteMessages(String memoryId) {
String key = KEY_PREFIX + memoryId;
redisTemplate.delete(key);
}
}
1.3.2、MongoDB
引入 Spring Data Mongodb相关 Maven 坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
MongoDB 的配置信息:
spring:
data:
mongodb:
host: localhost
port: 27017
database: chat_memory_db # 当没有此数据库时会自动创建
username: admin
password: admin
authentication-database: admin # 认证库,默认是admin
# 或者合并写成:
# uri: mongodb://admin:admin@localhost:27017/chat_memory_db?authSource=admin&ssl=false
创建一个实体类 ChatMessageBean 用于映射 MongoDB 中的文档集合:
@Data
@NoArgsConstructor
@AllArgsConstructor
// @Document 声明一个 Java 类与 MongoDB 中的集合相对应
@Document("chatMemory")
public class ChatMessageBean {
/**
* 消息ID
*/
@Id
private ObjectId messageId;
/**
* 记忆ID
*/
private String memoryId;
/**
* 存储当前聊天记录列表的 Json 字符串
*/
private String content;
}
新建一个类 MongoChatMemoryStore 实现三个方法:
@Component
public class MongoChatMemoryStore implements ChatMemoryStore {
@Autowired
private MongoTemplate mongoTemplate;
@Override
public List<ChatMessage> getMessages(String memoryId) {
Criteria criteria = Criteria.where("memoryId").is(memoryId);
Query query = new Query(criteria);
ChatMessageBean chatMessage = mongoTemplate.findOne(query, ChatMessageBean.class);
if (chatMessage == null) {
return new LinkedList<>();
}
// 将数据库中的Json聊天信息提取出来
return ChatMessageDeserializer.messagesFromJson(chatMessage.getContent());
}
@Override
public void updateMessages(String memoryId, List<ChatMessage> chatMessageList) {
Criteria criteria = Criteria.where("memoryId").is(memoryId);
Query query = new Query(criteria);
Update update = new Update();
update.set("content", ChatMessageSerializer.messagesToJson(chatMessageList));
mongoTemplate.upsert(query, update, ChatMessageBean.class);
}
@Override
public void deleteMessages(String memoryId) {
Criteria criteria = Criteria.where("memoryId").is(memoryId);
Query query = new Query(criteria);
mongoTemplate.remove(query, ChatMessageBean.class);
}
}
1.3.3、注入ChatMemoryStore
在实现了自定义的持久化方式之后,通过 MessageWindowChatMemory 将 RedisChatMemoryStore 注入到 ChatMemoryProvider 中,并在创建 AiService 时注入:
// 在 AI Service 配置中使用
@Bean
public Assistant assistant(ChatModel chatModel, RedisChatMemoryStore redisChatMemoryStore) {
return AiServices.builder(Assistant.class)
.chatModel(chatModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder()
.maxMessages(10)
.chatMemoryStore(redisChatMemoryStore) // 注入持久化存储
.build())
.build();
}
二、检索增强生成RAG
2.1、RAG介绍
2.1.1、解决什么问题
大语言模型尽管能力很强,但是也有两个绕不开的问题:
- 知识边界优先:模型的知识库只截止于训练日期,它不知道训练日期之后产生的知识和训练数据中没有涉及到的知识。
- 大模型幻觉:当遇到不知道的问题时,大模型可能会自信地编造根本不存在的东西,显出一副“一本正经的胡说八道”的样子
而对此也有一个比较有效的解决方法,就是 RAG(Retrieval-Augmented Generation),即检索增强生成。
RAG(检索增强生成)就是让模型在回答问题之前,先在一个知识库里搜索相关的内容片段,把这些片段作为参考资料拼到提示词里,再让模型根据这些事实来回答。这样既扩展了模型的知识边界,又提高了回答的准确性。
RAG的适用场景:
- 私有知识库场景,可避免将知识直接纳入模型训练(如 HR、IT 支持、产品文档)
- 数据处理分析(如根据PDF文档回答相关问题)
- 法律、医疗等专业领域,用来补充大模型的知识数据
2.1.2、核心流程
主要分成索引(Indexing)和检索生成(Retrieval & Generation)两个阶段。
LangChain4j 对 RAG 流程进行了封装,主要由以下几个组件完成:
- 文档加载器:
DocumentLoader,负责从 PDF、TXT、Word 等文件解析出纯文本 - 文档分割器:
DocumentSplitter,将长文档切分成适合向量化的片段 - 嵌入模型:
EmbeddingModel,将文本片段转换为高维向量 - 向量存储:
EmbeddingStore,将向量存入向量数据库,支持相似度检索 - 内容检索器:
ContentRetriever,根据用户问题负责从知识库中检索相关片段,拼接进提示词,是用户在 AI Service中 最主要交互的组件
2.2、文档处理步骤
2.2.1、文档加载器(Document Loader)
官方文档:RAG (Retrieval-Augmented Generation) #document-loader| LangChain4j
首先要把知识库文件加载成 Document 对象。LangChain4j 提供了多种文档加载器,支持常见的文档格式:
| 文档加载器 | 说明 | 依赖包 |
|---|---|---|
| TextDocumentParser | 解析纯文本文件 | langchain4j |
| ApachePdfBoxDocumentParser | 解析PDF文件 | langchain4j-document-parser-apache-pdfbox |
| ApacheTikaDocumentParser | 通用格式(Word、PPT 等) | langchain4j-document-parser-apache-tika |
| UrlDocumentLoader | 网页 | langchain4j |
加载 PDF 需要额外解析器,引入依赖:
<!--底层基于Apache PDFBox的PDF文档解析器-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
</dependency>
<!--底层基于Apache Tika的通用解析器-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-tika</artifactId>
</dependency>
通过 FileSystemDocumentLoader.loadDocument() 对象根据路径加载文件,然后指定文件的加载器:
// 加载单个指定路径的 pdf 文件
String textPath = "./knowledge/testFile.pdf";
Document document = FileSystemDocumentLoader.loadDocument(textPath, new ApachePdfBoxDocumentParser());
System.out.println(document.text());
// 加载单个指定路径的 txt 文件
String textPath = "./knowledge/testFile.txt";
Document textDocument = FileSystemDocumentLoader.loadDocument(textPath, new TextDocumentParser());
System.out.println(document.text());
ApachePdfBoxDocumentParser 和 TextDocumentParser 会把文件都被封装成 Document 对象,后续分割、嵌入都围绕它操作。
对于加载后的文档 Document 对象,一般还需要文档本身的信息,即元数据。LangChain4j 中的元数据被封装为 Metadata 类,Metadata 对外表现为键值存储结构,使用 put()添加元数据,并通过 getString(), getInteger() 等方法按类型安全地提取数据:
// 手动向文档中追加自定义的业务元数据
document.metadata().put("author", "张三");
document.metadata().put("department", "研发部");
document.metadata().put("loadTime", java.time.LocalDateTime.now().toString());
// 验证自定义元数据是否成功写入
System.out.println("作者: " + document.metadata().getString("author"));
System.out.println("部门: " + document.metadata().getString("department"));
2.2.2、文档分割器(Document Splitter)
官方文档:RAG (Retrieval-Augmented Generation) #document-splitter)| LangChain4j
加载进来的文档通常是一整个长文本,不能直接拿去嵌入,因为一方面 LLM 上下文窗口有限,不能塞入长篇大论,另一方面向量搜索需要细粒度的段落,匹配度才高,所以需要将其切分成语义完整、粒度适中的片段。
LangChain4j 提供了多种文本分割器:
| 分割器 | 分割依据 | 适用场景 |
|---|---|---|
| DocumentByParagraphSplitter | 按段落分割 | 自然语言文档 |
| DocumentBySentenceSplitter | 按句子分割 | 需要保持句子语义完整 |
| DocumentByLineSplitter | 按行分割 | 结构化文档,如日志、CSV |
| DocumentByWordSplitter | 按单词分割 | 需要控制单词级别的分割 |
| DocumentByCharacterSplitter | 按字符(字)分割 | 对字符数有精确要求的场景 |
| DocumentByRegexSplitter | 按正则表达式分割 | 自定义分割规则 |
| DocumentSplitters.recursive() | 按照段落/行/句子/词的顺序从高到低尝试递归分割,每当单次分割的文本超出最大字符数限制便降级为更小层级 | 长文档,能在不同级别尝试分割 |
示例代码:
// 按段落分割文档
DocumentSplitter documentByParagraphSplitter = new DocumentByParagraphSplitter(
300, // maxSegmentsPerParagraph: 每个片段最多 300 token
50, // overlapSize: 相邻片段重叠 50 token
new HuggingFaceTokenCountEstimator() // 指定分词方式,为空时则按字符分割
);
// 按照段落/行/句子/词的顺序从高到低尝试递归分割
DocumentSplitter documentSplitters = DocumentSplitters.recursive(
512, // maxSegmentSizeInTokens:每个片段最多 512 token
64, // maxOverlapSizeInTokens:相邻片段重叠 64 token
new HuggingFaceTokenCountEstimator() // 指定分词方式,为空时则按字符分割
);
// 切割单个文档
List<TextSegment> segmentList = documentByParagraphSplitter.split(pdfDocument);
// 如果是多个文档 document1、document2、document3
List<TextSegment> segmentList = documentSplitters.splitAll(document1, document2, document3);
// 如果是文档集合 documentList
List<TextSegment> segmentList = documentByParagraphSplitter.splitAll(documentList);
TextSegment 是 Document 分割后的结果,通常包含:
text:分割后的文本内容(字符串)。metadata:内部以键值方式存储,用于携带从原Document继承或自定义的元数据(如来源、页码、时间戳等)。同时,分割器还会自动为每个片段添加一个唯一的标识符(如index=0,index=1)。在检索阶段,这些元数据可用于过滤结果或帮助大模型更好地理解上下文
Token计算
DocumentSplitter 及其子类在分割文本时,为了判断片段大小是否超限,通过两种模式来计算:
- 字符模式(默认):构造分割器时不提供
TokenCountEstimator实例,分割器直接以字符数量为单位进行切分 - Token 模式:如果需要更精确地控制分割粒度,在显式提供一个
TokenCountEstimator实例,以实现精准计算出文本或聊天消息中的真实 Token 数量。LangChain4j 提供的TokenCountEstimator实现中,针对 OpenAI 系列模型,可以使用OpenAiTokenCountEstimator;而在使用本地嵌入模型时,也可以配合HuggingFaceTokenCountEstimator等工具。
为什么需要重叠(overlap)
假设文档有一段:"LangChain4j 是一个 Java 框架。它简化了与大语言模型的交互。开发者可以通过声明式 API 快速构建 AI 应用。"
如果不重叠,按 300 token 硬切,可能把"它简化了与大语言模型的交互"这句话拦腰截断,前半段在一个片段,后半段在另一个片段。设置 50 token 的重叠,可以保证边界处的语义不被破坏,保证语义的完整性。
2.2.3、向量化(Embedding)
官方文档:RAG (Retrieval-Augmented Generation) #embedding| LangChain4j
分割好的文本段需要通过向量模型转换成向量,也就是 Embedding。
向量的特点是:语义相近的文本,向量距离也相近。
LangChain4j 中将向量模型封装为 EmbeddingModel,类似于 ChatModel。LangChain4j 支持多种向量模型:
- 内置轻量模型:
langchain4j-embeddings-all-minilm-l6-v2(本地运行,无网络依赖) - 调用外部 API:DashScope 的
text-embedding-v2、OpenAI 的text-embedding-3-small - ONNX 模型:
langchain4j-embeddings-onnx
更多支持可在官方文档查看:Embedding Models | LangChain4j
导入 DashScope 的依赖包:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
EmbeddingModel textEmbeddingModel = OpenAiEmbeddingModel.builder()
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.apiKey(System.getenv("Key_AliBaiLian"))
.modelName("text-embedding-v3")
.build();
EmbeddingModel qwenEmbeddingModel = QwenEmbeddingModel.builder()
.apiKey(System.getenv("Key_AliBaiLian"))
.modelName("text-embedding-v3")
.build();
// 对单个片段向量化
Embedding embedding = qwenEmbeddingModel.embed("Hello World").content();
System.out.println("向量维度:" + embedding.vector().length);
System.out.println("向量输出:" + embedding.toString());
2.2.4、向量存储(EmbeddingStore)
官方文档:RAG (Retrieval-Augmented Generation) #embedding-store| LangChain4j
向量存储是 RAG 的核心,负责存储文本向量并支持高效的相似性检索。LangChain4j 将向量存储这部分抽象为了 EmbeddingStore 接口。
LangChain4j 同样支持多种向量数据库:
- InMemoryEmbeddingStore:纯内存,开发用,重启丢失
- Redis:需要在 Redis 中安装 RedisStack 或 RediSearch 模块
- Elasticsearch、Milvus、Pinecone、Qdrant 等
更多支持可在官方文档查看:Comparison table of all supported Embedding Stores | LangChain4j
EmbeddingStore 所使用数据库中向量集合的向量维度必须与 EmbeddingModel 输出维度保持一致,否则无法存储。
@Configuration
public class ModelConfig {
/**
* 基于pinecone向量数据库的向量存储
* @return
*/
@Bean("pineconeEmbeddingStore")
public EmbeddingStore<TextSegment> pineconeEmbeddingStore(@Qualifier("textEmbeddingModel") EmbeddingModel embeddingModel) {
// 创建向量存储
return PineconeEmbeddingStore.builder()
.apiKey("xxxxxxx")
// 如果指定的索引不存在,将创建一个新的索引
.index("xiaozhi-index")
// 如果指定的命名空间不存在,将创建一个新的命名空间
.nameSpace("xiaozhi-namespace")
.createIndex(
PineconeServerlessIndexConfig.builder()
.cloud("AWS") // 指定索引部署的云服务商
.region("us-east-1") // 指定索引的所在主机区域
.dimension(embeddingModel.dimension())// 指定索引的向量维度,该维度与 embeddedModel 生成的向量维度相同
.build())
.build();
}
/**
* 基于Qdrant向量数据库的向量存储
* @return
*/
@Bean("qdrantEmbeddingStore")
public EmbeddingStore qdrantEmbeddingStore() {
return QdrantEmbeddingStore.builder()
.host("127.0.0.1")
.port(6334)
.collectionName("xiaozhi_collection")
.build();
}
/**
* 基于内存的向量存储
* @return
*/
@Bean("inMemoryEmbeddingStore")
public EmbeddingStore inMemoryEmbeddingStore() {
return new InMemoryEmbeddingStore<>();
}
}
2.2.5、数据存储(EmbeddingStoreIngestor)
官方文档:RAG (Retrieval-Augmented Generation) #embedding-store-ingestor| LangChain4j
前面已经完成了文档加载(DocumentLoader)、文档切分(DocumentSplitter)和向量模型(EmbeddingModel)的配置,手动将数据存入向量数据库,通常需要以下几个步骤:
// 1. 切分文档
List<TextSegment> segments = documentSplitter.split(document);
// 2. 对每个片段生成向量
for (TextSegment segment : segments) {
Embedding embedding = embeddingModel.embed(segment.text()).content();
// 3. 存入向量库
embeddingStore.add(embedding, segment);
}
当知识库文件较多时,每次都手动编写这套流程会比较繁琐。
因此 LangChain4j 提供了 EmbeddingStoreIngestor,用于将文档加载、切分、向量模型、向量存储这一整套流程封装起来,实现文档的数据导入。比如:
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingModel(embeddingModel) // 向量模型
.embeddingStore(embeddingStore) // 向量存储
.documentSplitter(recursiveSplitter) // 文档分割器
.textSegmentTransformer(textSegment -> {
// 自定义转换逻辑
textSegment.metadata().put("source", "knowledge-base");
return textSegment;
})
.build();
// 加载知识库文档
List<Document> documentList = FileSystemDocumentLoader.loadDocuments("/path/to/docs");
ingestor.ingest(documentList); // 分割 → 向量化 → 入库
// 如果是单个文件
ingestor.ingest(document);
// 多个文件
ingestor.ingest(document1, document2, document3);
执行 ingest() 方法时,LangChain4j 内部会自动完成以下操作:
- 使用
DocumentSplitter将文档切分成多个TextSegment - 调用
EmbeddingModel为每个片段生成向量 - 调用
EmbeddingStore将向量与对应的文本片段写入向量数据库
后续用户提问时,通过 ContentRetriever 根据问题向量进行相似度检索,并将检索到的相关片段返回给大模型。
2.2.6、检索查询(ContentRetriever)
对于最后的检索查询,由 ContentRetriever 完成,ContentRetriever 是 LangChain4j 中负责知识检索的核心组件,用于根据用户问题从知识库中检索相关内容。最常用的实现类是 EmbeddingStoreContentRetriever:
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingModel(embeddingModel) // 向量模型
.embeddingStore(embeddingStore) // 向量存储
.maxResults(3) // 最多返回 3 个相关片段
.minScore(0.6) // 相似度低于 0.6 的不要
.build();
手动调用 ContentRetriever:
Query query = Query.from("我的名字是什么?");
List<Content> contentList = contentRetriever.retrieve(query);
contentList.forEach(System.out::println);
当用户提问时,框架会先调用 contentRetriever.retrieve(query) 获取相关文档片段,然后将 ContentRetriever 检索到的内容通过 RAG 组件包装后加入 Prompt(通常放在 System Message 或 User Message 之前)。大模型就可以基于这些“外挂知识”生成回答。
但通常不会手动调用 ContentRetriever,更多时候直接注入到 AI Service 中:
@AiService(chatModel = "chatModel", contentRetriever = "contentRetriever", ...)
public interface Assistant {
String chat(String message);
}
或者通过 AiServices.builder() 注入:
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.contentRetriever(contentRetriever)
.build();
三、ChatMemory和RAG整合
实现聊天记忆与记忆持久化的部分代码参考 1.2 与 1.3,不再赘述。
3.1、Maven 依赖
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
</dependency>
<!-- Pinecone 向量数据库 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-pinecone</artifactId>
</dependency>
<!-- Qdrant 向量数据库 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-qdrant</artifactId>
</dependency>
3.2、配置类
在配置类 ModelConfig 中添加 ChatModel、EmbeddingModel、EmbeddingStore、EmbeddingStoreContentRetriever
@Configuration
public class ModelConfig {
// ================================== ChatModel Bean ==================================
@Bean("qwenChatModel")
public QwenChatModel qwenChatModel() {
return QwenChatModel.builder()
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.apiKey(System.getenv("Key_AliBaiLian"))
.modelName("qwen-max")
.build();
}
// ================================== Memory Bean ==================================
// MongoChatMemoryStore是之前自定义记忆持久化到 MongoDB 的实现类
@Bean("chatMemoryMongoProvider")
public ChatMemoryProvider chatMemoryMongoProvider(MongoChatMemoryStore mongoChatMemoryStore) {
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId) // 在 ChatMemoryStore 中标识不同的会话
.maxMessages(20)
.chatMemoryStore(mongoChatMemoryStore) // 聊天记忆的持久化方式实现类
.build();
}
// ================================== EmbeddingModel Bean ==================================
@Bean("textEmbeddingModel")
public EmbeddingModel textEmbeddingModel() {
return QwenEmbeddingModel.builder()
.apiKey(System.getenv("Key_AliBaiLian"))
.modelName("text-embedding-v3")
.build();
}
// ================================== EmbeddingStore Bean ==================================
/**
* 基于Qdrant向量数据库的向量存储
*/
@Bean("qdrantEmbeddingStore")
public EmbeddingStore qdrantEmbeddingStore() {
return QdrantEmbeddingStore.builder()
.host("127.0.0.1")
.port(6334)
.collectionName("xiaozhi_collection")
.build();
}
/**
* 配置 ContentRetriever,使用 qdrantEmbeddingStore
*/
@Bean("contentRetriever")
ContentRetriever contentRetriever(@Qualifier("textEmbeddingModel") EmbeddingModel embeddingModel,
@Qualifier("qdrantEmbeddingStore") EmbeddingStore embeddingStore) {
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore) // 指定要使用的嵌入存储
.embeddingModel(embeddingModel) // 设置用于生成嵌入向量的嵌入模型
.maxResults(1) // 设置返回的最大数量为1
.minScore(0.8) // 设置最小相似度
.dynamicTopK(5) // 可添加动态TopK
.dynamicMinScore(0.6) // 可添加动态阈值
.build();
}
}
3.3、将数据存入向量数据库
使用测试类手动加载文件中的数据,并分割、嵌入到向量数据库中(以 Qdrant 为例,使用其他向量数据库只需要更换注入的 EmbeddingStore 实例即可):
@Autowired
private EmbeddingModel textEmbeddingModel;
@Autowired
@Qualifier("qdrantEmbeddingStore")
private EmbeddingStore qdrantEmbeddingStore;
@Test
public void UploadKnowledgeLibraryTest() {
// 使用FileSystemDocumentLoader读取指定目录下的文档,并且使用默认的文档解析器对文档进行解析
String path1 = ".\path\resources\file1.md";
Document document1 = FileSystemDocumentLoader.loadDocument(path1);
String path2 = ".path\resources\file2.md";
Document document2 = FileSystemDocumentLoader.loadDocument(path2);
String path3 = ".\path\resources\file3.md";
Document document3 = FileSystemDocumentLoader.loadDocument(path3);
List<Document> documentList = Arrays.asList(document1, document2, document3);
// 将文本向量存入向量数据库
EmbeddingStoreIngestor.builder()
.embeddingModel(textEmbeddingModel)
.embeddingStore(qdrantEmbeddingStore)
// 按照段落切分
.documentSplitter(new DocumentByParagraphSplitter(300, 50, new HuggingFaceTokenCountEstimator()))
.build()
.ingest(documentList);
}
3.4、AI Service
配置好 ContentRetriever、向量数据库中嵌入数据之后,只需在 AI Service 中声明即可自动启用 RAG 能力。
方式一:@AiService 注解
@AiService(wiringMode = EXPLICIT,
chatModel = "qwenChatModel",
chatMemoryProvider = "chatMemoryMongoProvider",
contentRetriever = "contentRetriever") // 注入检索器
public interface Assistant {
String chat(@MemoryId String memoryId, String userMessage);
}
方式二:编程式构建
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(qwenChatModel)
.chatMemoryProvider(chatMemoryMongoProvider)
.contentRetriever(contentRetriever) // 绑定检索器
.build();
参考资料:
官方英文文档:Get Started | LangChain4j,API 文档:Overview (LangChain4j)
非官方中文文档:快速开始 | LangChain4j 中文文档
评论 (0)