LangChain4j 学习笔记:AIService、聊天记忆与提示词实践
本文介绍了人工智能服务AIService的核心概念及实现方式。AIService通过面向接口和动态代理技术,简化了与大语言模型的交互流程,支持输入格式化、输出解析等基础功能,以及聊天记忆、工具调用等高级特性。文章详细展示了创建AIService的步骤:引入依赖、定义接口、使用注解配置,并阐述了其通过代理对象转换输入输出的工作原理。重点讲解了聊天记忆功能的三种实现方式:直接调用模型、手动维护对话历史
一、人工智能服务 AIService
1、什么是AIService
AIService使用面向接口和动态代理的方式完成程序的编写,更灵活的实现高级功能。
1.1、链 Chain(旧版)
链的概念源自 Python 中的 LangChain。其理念是针对每个常见的用例都设置一条链,比如聊天机器人、检索增强生成(RAG)等。链将多个底层组件组合起来,并协调它们之间的交互。链存在的主要问题是不灵活,我们不进行深入的研究。
1.2、人工智能服务 AIService
在LangChain4j中我们使用AIService完成复杂操作。底层组件将由AIService进行组装。
AIService可处理最常见的操作:
- 为大语言模型格式化输入内容
- 解析大语言模型的输出结果
它们还支持更高级的功能:
- 聊天记忆 Chat memory
- 工具 Tools
- 检索增强生成 RAG
2、创建AIService
2.1、引入依赖
<!--langchain4j高级功能-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
</dependency>
2.2、创建接口
package com.donglin.java.ai.langchain4j.assistant;
public interface Assistant {
String chat(String userMessage);
}
2.3、测试用例
@SpringBootTest
public class AIServiceTest {
@Autowired
private QwenChatModel qwenChatModel;
@Test
public void testChat() {
//创建AIService
Assistant assistant = AiServices.create(Assistant.class, qwenChatModel);
//调用service的接口
String answer = assistant.chat("Hello");
System.out.println(answer);
}
}
2.4、@AiService
也可以在Assistant
接口上添加@AiService
注解
package com.donglin.java.ai.langchain4j.assistant;
import dev.langchain4j.service.spring.AiService;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
//因为我们在配置文件中同时配置了多个大语言模型,所以需要在这里明确指定(EXPLICIT)模型的beanName(qwenChatModel)
//@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,chatModel = "qwenChatModel")
@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel")
public interface Assistant {
String chat(String userMessage);
}
测试用例中,我们可以直接注入Assistant对象
@Autowired
private Assistant assistant;
@Test
public void testAssistant() {
String answer = assistant.chat("Hello");
System.out.println(answer);
}
2.5、工作原理
AiServices会组装Assistant接口以及其他组件,并使用反射机制创建一个实现Assistant接口的代理对象。这个代理对象会处理输入和输出的所有转换工作。在这个例子中,chat方法的输入是一个字符串,但是大模型需要一个UserMessage
对象。所以,代理对象将这个字符串转换为UserMessage
,并调用聊天语言模型。chat方法的输出类型也是字符串,但是大模型返回的是 AiMessage
对象,代理对象会将其转换为字符串。
简单理解就是:代理对象的作用是输入转换和输出转换
二、聊天记忆 Chat memory
1、测试对话是否有记忆
package com.donglin.java.ai.langchain4j;
@SpringBootTest
public class ChatMemoryTest {
@Autowired
private Assistant assistant;
@Test
public void testChatMemory() {
String answer1 = assistant.chat("我是华仔");
System.out.println(answer1);
String answer2 = assistant.chat("我是谁");
System.out.println(answer2);
}
}
很显然,目前的接入方式,大模型是没有记忆的。
2、聊天记忆的简单实现
可以使用下面的方式实现对话记忆。
package com.donglin.java.ai.langchain4j;
import com.donglin.java.ai.langchain4j.assistant.Assistant;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.response.ChatResponse;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Arrays;
@SpringBootTest
public class ChatMemoryTest {
@Autowired
private Assistant assistant;
@Autowired
private QwenChatModel qwenChatModel;
@Test
public void testChatMemory2() {
UserMessage userMessage1 = UserMessage.userMessage("我是华仔");
ChatResponse chatResponse1 = qwenChatModel.chat(userMessage1);
AiMessage aiMessage1 = chatResponse1.aiMessage();
System.out.println(aiMessage1.text());
UserMessage userMessage2 = UserMessage.userMessage("我是谁");
ChatResponse chatResponse2= qwenChatModel.chat(Arrays.asList(userMessage1,aiMessage1,userMessage2));
AiMessage aiMessage2 = chatResponse2.aiMessage();
System.out.println(aiMessage2.text());
}
}
3、使用ChatMemory实现聊天记忆
使用AIService可以封装多轮对话的复杂性,使聊天记忆功能的实现变得简单
@Test
public void testChatMemory3() {
//创建chatMemory
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
//创建AIService
Assistant assistant = AiServices
.builder(Assistant.class)
.chatLanguageModel(qwenChatModel)
.chatMemory(chatMemory)
.build();
//调用service的接口
String answer1 = assistant.chat("我是华仔");
System.out.println(answer1);
String answer2 = assistant.chat("我是谁");
System.out.println(answer2);
}
4、使用AIService实现聊天记忆
4.1、创建记忆对话智能体
当AIService由多个组件(大模型,聊天记忆,等)组成的时候,我们就可以称他为智能体
了
package com.donglin.java.ai.langchain4j.assistant;
import dev.langchain4j.service.spring.AiService;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
@AiService(
wiringMode = EXPLICIT,
chatModel = "qwenChatModel",
chatMemory = "chatMemory"
)
public interface Assistant {
String chat(String userMessage);
}
4.2、配置ChatMemory
package com.donglin.java.ai.langchain4j.config;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MemoryChatAssistantConfig {
@Bean
ChatMemory chatMemory() {
//设置聊天记忆记录的message数量
return MessageWindowChatMemory.withMaxMessages(10);
}
}
4.3、测试
@Autowired
private Assistant assistant;
@Test
public void testChatMemory4() {
String answer1 = assistant.chat("我是华仔");
System.out.println(answer1);
String answer2 = assistant.chat("我是谁");
System.out.println(answer2);
}
5、隔离聊天记忆
为每个用户的新聊天或者不同的用户区分聊天记忆
5.1、创建记忆隔离对话智能体
package com.donglin.java.ai.langchain4j.assistant;
@AiService(
wiringMode = EXPLICIT,
chatModel = "qwenChatModel",
chatMemoryProvider = "chatMemoryProvider"
)
public interface SeparateChatAssistant {
/**
* 分离聊天记录
* @param memoryId 聊天id
* @param userMessage 用户消息
* @return
*/
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
}
5.2、配置ChatMemoryProvider
package com.donglin.java.ai.langchain4j.config;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SeparateChatAssistantConfig {
@Bean
ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(10)
.build();
}
}
5.3、测试对话助手
用两个不同的memoryId测试聊天记忆的隔离效果
@Autowired
private SeparateChatAssistant separateChatAssistant;
@Test
public void testChatMemory5() {
String answer1 = separateChatAssistant.chat(1,"我是华仔");
System.out.println(answer1);
String answer2 = separateChatAssistant.chat(1,"我是谁");
System.out.println(answer2);
String answer3 = separateChatAssistant.chat(2,"我是谁");
System.out.println(answer3);
}
三、持久化聊天记忆 Persistence
默认情况下,聊天记忆存储在内存中。如果需要持久化存储,可以实现一个自定义的聊天记忆存储类,以便将聊天消息存储在你选择的任何持久化存储介质中。
1、存储介质的选择
大模型中聊天记忆的存储选择哪种数据库,需要综合考虑数据特点、应用场景和性能要求等因素,以下是一些常见的选择及其特点:
-
MySQL
-
特点:关系型数据库。支持事务处理,确保数据的一致性和完整性,适用于结构化数据的存储和查询。
-
适用场景:如果聊天记忆数据结构较为规整,例如包含固定的字段如对话 ID、用户 ID、时间戳、消息内容等,且需要进行复杂的查询和统计分析,如按用户统计对话次数、按时间范围查询特定对话等,MySQL 是不错的选择。
-
-
Redis
-
特点:内存数据库,读写速度极高。它适用于存储热点数据,并且支持多种数据结构,如字符串、哈希表、列表等,方便对不同类型的聊天记忆数据进行处理。
-
适用场景:对于实时性要求极高的聊天应用,如在线客服系统或即时通讯工具,Redis 可以快速存储和获取最新的聊天记录,以提供流畅的聊天体验。
-
-
MongoDB
-
特点:文档型数据库,数据以 JSON - like 的文档形式存储,具有高度的灵活性和可扩展性。它不需要预先定义严格的表结构,适合存储半结构化或非结构化的数据。
-
适用场景:当聊天记忆中包含多样化的信息,如文本消息、图片、语音等多媒体数据,或者消息格式可能会频繁变化时,MongoDB 能很好地适应这种灵活性。例如,一些社交应用中用户可能会发送各种格式的消息,使用 MongoDB 可以方便地存储和管理这些不同类型的数据。
-
-
Cassandra
-
特点:是一种分布式的 NoSQL 数据库,具有高可扩展性和高可用性,能够处理大规模的分布式数据存储和读写请求。适合存储海量的、时间序列相关的数据。
-
适用场景:对于大型的聊天应用,尤其是用户量众多、聊天数据量巨大且需要分布式存储和处理的场景,Cassandra 能够有效地应对高并发的读写操作。例如,一些面向全球用户的社交媒体平台,其聊天数据需要在多个节点上进行分布式存储和管理,Cassandra 可以提供强大的支持。
-
2、MongoDB
2.1、简介
MongoDB 是一个基于文档的 NoSQL 数据库,由 MongoDB Inc. 开发。
NoSQL,指的是非关系型的数据库。NoSQL有时也称作Not Only SQL的缩写,是对不同于传统的关系型数据库的数据库管理系统的统称。
MongoDB 的设计理念是为了应对大数据量、高性能和灵活性需求。
MongoDB使用集合(Collections)来组织文档(Documents),每个文档都是由键值对组成的。
- 数据库(Database):存储数据的容器,类似于关系型数据库中的数据库。
- 集合(Collection):数据库中的一个集合,类似于关系型数据库中的表。
- 文档(Document):集合中的一个数据记录,类似于关系型数据库中的行(row),以 BSON 格式存储。
MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成,文档类似于 JSON 对象,字段值可以包含其他文档,数组及文档数组:
2.2、安装MongoDB
服务器:mongodb-windows-x86_64-8.0.6-signed.msi
https://www.mongodb.com/try/download/community
命令行客户端 :mongosh-2.5.0-win32-x64.zip
https://www.mongodb.com/try/download/shell
图形客户端:mongodb-compass-1.39.3-win32-x64.exe
https://www.mongodb.com/try/download/compass
2.3、使用mongosh
启动 MongoDB Shell:
在命令行中输入 mongosh 命令,启动 MongoDB Shell,如果 MongoDB 服务器运行在本地默认端口(27017),则可以直接连接。
mongosh
连接到 MongoDB 服务器:
如果 MongoDB 服务器运行在非默认端口或者远程服务器上,可以使用以下命令连接:
mongosh --host <hostname>:<port>
其中 <hostname>
是 MongoDB 服务器的主机名或 IP 地址,<port>
是 MongoDB 服务器的端口号。
执行基本操作:
连接成功后,可以执行各种 MongoDB 数据库操作。例如:
- 查看当前数据库:
db
- 显示数据库列表:
show dbs
- 切换到指定数据库:
use <database_name>
- 执行查询操作:
db.<collection_name>.find()
- 插入文档:
db.<collection_name>.insertOne({ ... })
- 更新文档:
db.<collection_name>.updateOne({ ... })
- 删除文档:
db.<collection_name>.deleteOne({ ... })
- 退出 MongoDB Shell:
quit()
或者exit
CRUD
# 插入文档
test> db.mycollection.insertOne({ name: "Alice", age: 30 })
# 查询文档
test> db.mycollection.find()
# 更新文档
test> db.mycollection.updateOne({ name: "Alice" }, { $set: { age: 31 } })
# 删除文档
test> db.mycollection.deleteOne({ name: "Alice" })
# 退出 MongoDB Shell
test> quit()
2.4、使用mongodb-compass
2.5、整合SpringBoot
引入MongoDB依赖:
<!-- Spring Boot Starter Data MongoDB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
添加远程连接配置:
#MongoDB连接配置
spring.data.mongodb.uri=mongodb://localhost:27017/chat_memory_db
2.6、CRUD测试
创建实体类:映射MongoDB中的文档(相当与MySQL的表)
package com.donglin.java.ai.langchain4j.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document("chat_messages")
public class ChatMessages {
//唯一标识,映射到 MongoDB 文档的 _id 字段
@Id
private ObjectId messageId;
//private Long messageId;
private String content; //存储当前聊天记录列表的json字符串
}
创建测试类:
package com.donglin.java.ai.langchain4j;
import com.donglin.java.ai.langchain4j.bean.ChatMessages;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
@SpringBootTest
public class MongoCrudTest {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 插入文档
*/
/* @Test
public void testInsert() {
mongoTemplate.insert(new ChatMessages(1L, "聊天记录"));
}*/
/**
* 插入文档
*/
@Test
public void testInsert2() {
ChatMessages chatMessages = new ChatMessages();
chatMessages.setContent("聊天记录列表");
mongoTemplate.insert(chatMessages);
}
/**
* 根据id查询文档
*/
@Test
public void testFindById() {
ChatMessages chatMessages = mongoTemplate.findById("6801ead733ba9c4a0d9b6c7b", ChatMessages.class);
System.out.println(chatMessages);
}
/**
* 修改文档
*/
@Test
public void testUpdate() {
Criteria criteria = Criteria.where("_id").is("6801ead733ba9c4a0d9b6c7b");
Query query = new Query(criteria);
Update update = new Update();
update.set("content", "新的聊天记录列表");
//修改或新增
mongoTemplate.upsert(query, update, ChatMessages.class);
}
/**
* 新增或修改文档
*/
@Test
public void testUpdate2() {
Criteria criteria = Criteria.where("_id").is("100");
Query query = new Query(criteria);
Update update = new Update();
update.set("content", "新的聊天记录列表");
//修改或新增
mongoTemplate.upsert(query, update, ChatMessages.class);
}
/**
* 删除文档
*/
@Test
public void testDelete() {
Criteria criteria = Criteria.where("_id").is("100");
Query query = new Query(criteria);
mongoTemplate.remove(query, ChatMessages.class);
}
}
3、持久化聊天
3.1、优化实体类
package com.donglin.java.ai.langchain4j.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document("chat_messages")
public class ChatMessages {
//唯一标识,映射到 MongoDB 文档的 _id 字段
@Id
private ObjectId id;
private int messageId;
private String content; //存储当前聊天记录列表的json字符串
}
3.2、创建持久化类
创建一个类实现ChatMemoryStore接口
package com.donglin.java.ai.langchain4j.store;
import com.donglin.java.ai.langchain4j.bean.ChatMessages;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;
import java.util.LinkedList;
import java.util.List;
@Component
public class MongoChatMemoryStore implements ChatMemoryStore {
@Autowired
private MongoTemplate mongoTemplate;
@Override
public List<ChatMessage> getMessages(Object memoryId) {
Criteria criteria = Criteria.where("memoryId").is(memoryId);
Query query = new Query(criteria);
ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);
if(chatMessages == null) return new LinkedList<>();
return ChatMessageDeserializer.messagesFromJson(chatMessages.getContent());
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
Criteria criteria = Criteria.where("memoryId").is(memoryId);
Query query = new Query(criteria);
Update update = new Update();
update.set("content", ChatMessageSerializer.messagesToJson(messages));
//根据query条件能查询出文档,则修改文档;否则新增文档
mongoTemplate.upsert(query, update, ChatMessages.class);
}
@Override
public void deleteMessages(Object memoryId) {
Criteria criteria = Criteria.where("memoryId").is(memoryId);
Query query = new Query(criteria);
mongoTemplate.remove(query, ChatMessages.class);
}
}
在SeparateChatAssistantConfig中,添加MongoChatMemoryStore对象的配置
package com.donglin.java.ai.langchain4j.config;
import com.donglin.java.ai.langchain4j.store.MongoChatMemoryStore;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SeparateChatAssistantConfig {
//注入持久化对象
@Autowired
private MongoChatMemoryStore mongoChatMemoryStore;
//实现会话隔离的
@Bean
ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(10)
.chatMemoryStore(mongoChatMemoryStore)//配置持久化对象
.build();
}
}
4、测试
发现MongoDB中已经存储了会话记录
四、提示词 Prompt
1、系统提示词
@SystemMessage 设定角色,塑造AI助手的专业身份,明确助手的能力范围
1.1、配置@SystemMessage
在SeparateChatAssistant类的chat方法上添加@SystemMessage注解
@SystemMessage("你是我的好朋友,请用东北话回答问题。")//系统消息提示词
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
@SystemMessage
的内容将在后台转换为 SystemMessage
对象,并与 UserMessage
一起发送给大语言模型(LLM)。
SystemMessaged的内容只会发送给大模型一次。
如果你修改了SystemMessage的内容,新的SystemMessage会被发送给大模型,之前的聊天记忆会失效。
1.2、测试
package com.donglin.java.ai.langchain4j;
import com.donglin.java.ai.langchain4j.assistant.SeparateChatAssistant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class PromptTest {
@Autowired
private SeparateChatAssistant separateChatAssistant;
@Test
public void testSystemMessage() {
String answer = separateChatAssistant.chat(3,"今天几号");
System.out.println(answer);
}
}
如果要显示今天的日期,我们需要在提示词中添加当前日期的占位符{{current_date}}
@SystemMessage("你是我的好朋友,请用东北话回答问题。今天是{{current_date}}")//系统消息提示词
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
1.3、从资源中加载提示模板
@SystemMessage
注解还可以从资源中加载提示模板:
@SystemMessage(fromResource = "my-prompt-template.txt")
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
my-prompt-template.txt
你是我的好朋友,请用东北话回答问题,回答问题的时候适当添加表情符号。
{{current_date}}表示当前日期
你是我的好朋友,请用东北话回答问题,回答问题的时候适当添加表情符号。
今天是 {{current_date}}。
2、用户提示词模板
@UserMessage: 获取用户输入
2.1、配置@UserMessage
在MemoryChatAssistant
的chat
方法中添加注解
@UserMessage("你是我的好朋友,请用上海话回答问题,并且添加一些表情符号。 {{it}}") //{{it}}表示这里唯一的参数的占位符
String chat(String message);
2.2、测试
@Autowired
private MemoryChatAssistant memoryChatAssistant;
@Test
public void testUserMessage() {
String answer = memoryChatAssistant.chat("我是华仔");
System.out.println(answer);
}
3、指定参数名称
3.1、配置@V
@V 明确指定传递的参数名称
@UserMessage("你是我的好朋友,请用上海话回答问题,并且添加一些表情符号。{{message}}")
String chat(@V("message") String userMessage);
3.2、多个参数的情况
如果有两个或两个以上的参数,我们必须要用@V
,在SeparateChatAssistant
中定义方法chat2
@UserMessage("你是我的好朋友,请用粤语回答问题。{{message}}")
String chat2(@MemoryId int memoryId, @V("message") String userMessage);
测试:@UserMessage
中的内容每次都会被和用户问题组织在一起发送给大模型
@Test
public void testV() {
String answer1 = separateChatAssistant.chat2(1, "我是华仔");
System.out.println(answer1);
String answer2 = separateChatAssistant.chat2(1, "我是谁");
System.out.println(answer2);
}
3.3、@SystemMessage和@V
也可以将@SystemMessage
和@V
结合使用
在SeparateChatAssistant
中添加方法chat3
@SystemMessage(fromResource = "my-prompt-template3.txt")
String chat3(
@MemoryId int memoryId,
@UserMessage String userMessage,
@V("username") String username,
@V("age") int age
);
创建提示词模板my-prompt-template3.txt,添加占位符
你是我的好朋友,我是{{username}},我的年龄是{{age}},请用东北话回答问题,回答问题的时候适当添加表情符号。
今天是 {{current_date}}。
测试:
@Test
public void testUserInfo() {
String answer = separateChatAssistant.chat3(1, "我是谁,我多大了", "翠花", 18);
System.out.println(answer);
}
更多推荐
所有评论(0)