最精简容易上手地使用Langchain4J实现大模型应用开发
使用面向接口和动态代理的方式完成程序的编写,更灵活的实现高级功能。
随着openAI开源chatgpt4o开始,各路ai不断涌现,同时,用来在后端接入大模型并进行改造的方式也如雨后春笋般出现,如spring官方的springAI,根据openAI开源的langchain改造适应后的LangChain4J等
现在我使用langchain4j来进行开发演示
1.基础用法
1.为什么要选择langchain4j?
相比于springAI,langchain4j调用简单,导入maven依赖后,将大模型的配置参数进行集成,直接注入官方为我们配置好的bean,bean会根据我们的properties文件来进行参数构造生成大模型的对象
如下:
@Autowired
private QwenChatModel qwenChatModel;
@Test
public void testDashScopeQwen() {
//向模型提问
String answer = qwenChatModel.chat("你好");
//输出结果
System.out.println(answer);
}
而springAI需要我们手动创建大模型对象进行配置:
@Configuration
public class CommonConfig {
@Bean
public ChatClient client(OpenAiChatModel model){
return ChatClient.builder(model)
.build();
}
}
此外还有更多如:
通过统一的modelExecutor接口来灵活地切换模型,上下文管理自动等优点。
2.配置demo工程进行演示
1.首先创建springboot工程,要求jdk17以上 springboot版本不低于3
2.导入maven依赖
<dependencies>
<!-- web应用程序核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 编写和运行测试用例 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 前后端分离中的后端接口测试工具 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!-- 基于open-ai的langchain4j接口:ChatGPT、deepseek都是open-ai标准下的大模型 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!--引入SpringBoot依赖管理清单-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--引入langchain4j依赖管理清单-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
导入完依赖以后,我们创建一个测试类来进行测试
/**
* gpt-4o-mini语言模型接入测试
*/
@Test
public void testGPTDemo() {
//初始化模型
OpenAiChatModel model = OpenAiChatModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.apiKey("demo")
.modelName("gpt-4o-mini")
.build();
//向模型提问
String answer = model.chat("你好");
//输出结果
System.out.println(answer);
}

ps:上述爆红无需理会,是我idea问题,不影响程序
如果想通过springboot整合,通过properties来进行动态变量处理,用如下方法
3.springboot整合
我们先通过OpenAI官方的实例demo来进行配置
1.配置properties文件
#langchain4j 配置OpenAI聊天模型的基本信息
# 指定OpenAI聊天模型的基URL(Beta3以后,这个是必须的,否则会报错)
langchain4j.open-ai.chat-model.base-url=http://langchain4j.dev/demo/openai/v1
# 设置OpenAI聊天模型的模型名称
langchain4j.open-ai.chat-model.model-name=gpt-4o-mini
# 配置OpenAI聊天模型的API密钥
langchain4j.open-ai.chat-model.api-key=demo
# langchain4j 日志配置打印
# 是否启用记录OpenAI聊天模型的请求日志
langchain4j.open-ai.chat-model.log-requests=true
# 是否启用记录OpenAI聊天模型的响应日志
langchain4j.open-ai.chat-model.log-responses=true
# 设置日志记录级别为info
logging.level.root=info
然后配置测试类:
/**
* 整合SpringBoot
*/
// 使用Spring框架的@Autowired注解自动注入OpenAiChatModel的实例
@Autowired
private OpenAiChatModel openAiChatModel;
/**
* 测试OpenAiChatModel的chat方法
* 通过向模型发送一个简单的问候消息来发起对话,并打印模型的响应
*/
@Test
void testOpenAiChatSpringBootModel() {
// 发送消息"Hello World!"到gpt-4o-mini,并获取模型的响应
String answer = openAiChatModel.chat("Hello World!");
// 打印模型的响应到控制台
System.out.println(answer);
}
运行后如下:

如果想自己接入其他ai,如阿里云百炼,deepseek等,用如下方法
1.配置properties文件(key需要去阿里官网获取)
#阿里云百炼
#langchain4j.open-ai.chat-model.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
langchain4j.community.dashscope.chat-model.api-key=${your_api_key}
langchain4j.community.dashscope.chat-model.model-name=qwen-plus
2. 引入maven依赖(同样可以通过官网获取)
<dependencies>
<!-- 接入阿里云百炼平台 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!--引入百炼依赖管理清单-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3.创建测试类
/**
* 通义千问大模型
*/
@Autowired
private QwenChatModel qwenChatModel;
@Test
public void testDashScopeQwen() {
//向模型提问
String answer = qwenChatModel.chat("你好");
//输出结果
System.out.println(answer);
}

4.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.atguigu.java.ai.langchain4j.assistant;
public interface Assistant {
String chat(String userMessage);
}
2.3测试类
@SpringBootTest
@ContextConfiguration(classes = yourApplication.class)
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 注解,如果有多个大语言模型就需要指定beanName
//因为我们在配置文件中同时配置了多个大语言模型,所以需要在这里明确指定(EXPLICIT)模型的beanName(qwenChatModel)
import dev.langchain4j.service.spring.AiService;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
@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 对象,代理对象会将其转换 为字符串。
简单理解就是:代理对象的作用是输入转换和输出转换
2.进阶用法
1.聊天记忆功能
通常来说,我们通过调用api来实现对话时,是没有聊天记忆的,如下列代码所示:
@Autowired
private Assistant assistant;
@Test
public void testChatMemory() {
String answer1 = assistant.chat("我是环环");
System.out.println(answer1);
String answer2 = assistant.chat("我是谁");
System.out.println(answer2);
}

可以看到没有根据第一条聊天内容来进行对话
如果我们要实现聊天记忆,就需要记录上一次或者多次的对话,利用下列代码来进行简单演示:
@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());
}
上述代码我们可以看到,在第二轮对话中,给ai的userMessage中包含了上次对话的内容,所以ai就记录了对话的内容。

因此我们也可以使用AIService的ChatMemory来实现聊天记忆
使用AIService可以封装多轮对话的复杂性,使聊天记忆功能的实现变得简单
@Test
public void testChatMemory3() {
// 创建chatMemory
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
// 创建AIService
Assistant assistant = AiServices
.builder(Assistant.class)
.chatModel(qwenChatModel) // 修复点:使用 chatModel 而不是 chatLanguageModel
.chatMemory(chatMemory)
.build();
// 调用service的接口
String answer1 = assistant.chat("我是环环");
System.out.println(answer1);
String answer2 = assistant.chat("我是谁");
System.out.println(answer2);
}
我们可以使用chatMemory来构建一个AIService,这样就可以记录每次聊天的内容了
2.利用AIService实现聊天记忆
当AIService由多个组件(大模型,聊天记忆,等)组成的时候,我们就可以称他为 智能体了
我们将配置信息包装成一个接口方便后续使用
@AiService(
wiringMode = EXPLICIT,
chatModel = "qwenChatModel",
chatMemory = "chatMemory"
)
public interface MemoryChatAssistant {
String chat(String userMessage);
}
随后用langchain4j里的config进行配置,设置记录的message数量
package com.atguigu.java.ai.langchain4j.config;
@Configuration
public class MemoryChatAssistantConfig {
@Bean
ChatMemory chatMemory() {
//设置聊天记忆记录的message数量
return MessageWindowChatMemory.withMaxMessages(10);
}
}
然后我们用如下代码进行测试
@Autowired
private MemoryChatAssistant memoryChatAssistant;
@Test
public void testChatMemory4() {
String answer1 = memoryChatAssistant.chat("我是环环");
System.out.println(answer1);
String answer2 = memoryChatAssistant.chat("我是谁");
System.out.println(answer2);
}
发现可以我们之前的配置生效了,可以记录聊天内容

3.隔离聊天记忆
由于我们的程序服务器只有少数,对于不同用户的聊天记录肯定是会冲突的,所以我们有必要为每个用户的新聊天或者不同的用户区分聊天记忆
我们可以使用AIService里chat方法里的MemoryId来为每个用户分配一个id,根据id不同来进行聊天记录的读取。
@AiService(
wiringMode = EXPLICIT,
chatMemoryProvider = "chatMemoryProvider",//chatMemoryProvider已经集成了chatMemory
chatModel = "qwenChatModel"
)
public interface SeparateChatAssistant {
/**
* 分离聊天记录
* @param memoryId 聊天id
* @param userMessage 用户消息
* @return
*/
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
}
同时要注意,配置的时候要配置ChatMemoryProvider
这里的Provider就是我们在configration里配置的bean,类似于前面的chatMemory配置
@Configuration
public class SeparateChatAssistantConfig {
@Bean
ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(10)
.build();
}
}
可以看到,我们将id,聊天记录数等信息进行配置后然后返回,这样就配置完成了
然后我们用两个不同的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);
}
可以看到,我们传入了两个id,看看它们的效果如何
可以看到第三个对话就没有记录前面的信息了
这里,我们将每次聊天的记录都存储到了chatMemory里,我们进入源码,可以发现一个store属性

进入ChatMemoryStore,我们可以看到这个属性实现了两个方法

我们这个测试用例使用的就是SingleSlotChatMemoryStore方法来实现的存储,进入到里面可以看到一个是messages另一个是memoryId,其中messages就是用来记录聊天内容的列表

其中有一个方法,就是用来实现根据id来存储聊天记录的(getMessages)
public SingleSlotChatMemoryStore(Object memoryId) {
this.memoryId = memoryId;
}
public List<ChatMessage> getMessages(Object memoryId) {
this.checkMemoryId(memoryId);
return this.messages;
}
44
4.持久化聊天记忆Persistence
默认情况下,聊天记忆存储在内存中。如果需要持久化存储,可以实现一个自定义的聊天记忆存储类, 以便将聊天消息存储在你选择的任何持久化存储介质中。
1、存储介质的选择
大模型中聊天记忆的存储选择哪种数据库,需要综合考虑数据特点、应用场景和性能要求等因素,以下 是一些常见的选择及其特点:
- MySQL
- 特点:关系型数据库。支持事务处理,确保数据的一致性和完整性,适用于结构化数据的存储 和查询。
- 适用场景:如果聊天记忆数据结构较为规整,例如包含固定的字段如对话 ID、用户 ID、时间 戳、消息内容等,且需要进行复杂的查询和统计分析,如按用户统计对话次数、按时间范围查 询特定对话等,MySQL 是不错的选择。
- Redis
- 特点:内存数据库,读写速度极高。它适用于存储热点数据,并且支持多种数据结构,如字符 串、哈希表、列表等,方便对不同类型的聊天记忆数据进行处理。
- 适用场景:对于实时性要求极高的聊天应用,如在线客服系统或即时通讯工具,Redis 可以快 速存储和获取最新的聊天记录,以提供流畅的聊天体验。
- MongoDB
- 特点:文档型数据库,数据以 JSON - like 的文档形式存储,具有高度的灵活性和可扩展性。它 不需要预先定义严格的表结构,适合存储半结构化或非结构化的数据。
- 适用场景:当聊天记忆中包含多样化的信息,如文本消息、图片、语音等多媒体数据,或者消 息格式可能会频繁变化时,MongoDB 能很好地适应这种灵活性。例如,一些社交应用中用户可 能会发送各种格式的消息,使用 MongoDB 可以方便地存储和管理这些不同类型的数据。
- Cassandra
- 特点:是一种分布式的 NoSQL 数据库,具有高可扩展性和高可用性,能够处理大规模的分布 式数据存储和读写请求。适合存储海量的、时间序列相关的数据。
- 适用场景:对于大型的聊天应用,尤其是用户量众多、聊天数据量巨大且需要分布式存储和处 理的场景,Cassandra 能够有效地应对高并发的读写操作。例如,一些面向全球用户的社交媒 体平台,其聊天数据需要在多个节点上进行分布式存储和管理,Cassandra 可以提供强大的支持。
我们使用MongoDB来进行存储聊天内容
MongoDB
简介:
MongoDB 是一个基于文档的 NoSQL 数据库,由 MongoDB Inc. 开发。 NoSQL,指的是非关系型的数据库。
NoSQL有时也称作Not Only SQL的缩写,是对不同于传统的关系型数 据库的数据库管理系统的统称。
MongoDB 的设计理念是为了应对大数据量、高性能和灵活性需求。 MongoDB使用集合(Collections)来组织文档(Documents),每个文档都是由键值对组成的
- 数据库(Database):存储数据的容器,类似于关系型数据库中的数据库。
- 集合(Collection):数据库中的一个集合,类似于关系型数据库中的表。
- 文档(Document):集合中的一个数据记录,类似于关系型数据库中的行(row),以 BSON 格式 存储。
MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成,文档类似于 JSON 对象,字段值 可以包含其他文档,数组及文档数组:

例如我们用mongoDB的图形客户端来演示

可以看到存储类型为上图所示
SpringBoot整合MongoDB
首先引入依赖
<!-- Spring Boot Starter Data MongoDB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
然后在properties里添加连接配置
#MongoDB连接配置
spring.data.mongodb.uri=mongodb://localhost:27017/chat_memory_db
然后我们创建一个实体类:映射MongoDB中的文档(相当于MySQL的表)
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document("chat_messages")
创建测试类:
public class ChatMessages {
//唯一标识,映射到 MongoDB 文档的 _id 字段
@Id
private ObjectId messageId;
//private Long messageId;
private String content; //存储当前聊天记录列表的json字符串
}
然后测试即可
@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);
}
}
以第一个测试插入文档为例

使用MongoDB持久化聊天
优化实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document("chat_messages")
public class ChatMessages {
//唯一标识,映射到 MongoDB 文档的 _id 字段
@Id
private ObjectId id;
private int messageId;
private String content; //存储当前聊天记录列表的json字符串
}
创建持久化类
创建一个类实现ChatMemoryStore接口
@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对象的配置
@Configuration
public class SeparateChatAssistantConfig {
//注入持久化对象
@Autowired
private MongoChatMemoryStore mongoChatMemoryStore;
@Bean
ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.builder()
尚硅⾕
.id(memoryId)
.maxMessages(10)
.chatMemoryStore(mongoChatMemoryStore)//配置持久化对象
.build();
}
}
随后我们运行就可以发现MongoDB已经存储了会话记录

5.提示词工程Prompt
1.系统提示词
@SystemMessage设定角色,塑造AI助手的专业身份,明确助手的能力范围
1.1、配置@SystemMessage
在SeparateChatAssistant类的chat方法上添加@SystemMessage注解
public interface SeparateChatAssistant {
/**
* 分离聊天记录
* @param memoryId 聊天id
* @param userMessage 用户消息
* @return
*/
@SystemMessage("你是我的好朋友,请用东北话回答问题。")//系统消息提示词
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
}

我们可以通过在提示词中添加日期来获取准确日期

1.2从资源中加载提示模板
有时候我们在配置我们业务专属的机器人的时候需要配置大量的提示词,不像用户提示的一样。因此我们可以在资源中加载提示模板
@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:获取用户输入
@UserMessage("你是我的好朋友,请用上海话回答问题,并且添加一些表情符号。 {{it}}") //{{it}}表示这里
唯一的参数的占位符
String chat(String message);
然后我们进行测试
@Autowired
private MemoryChatAssistant memoryChatAssistant;
@Test
public void testUserMessage() {
String answer = memoryChatAssistant.chat("我是坤坤");
System.out.println(answer);
}

3.指定参数名称
3.1配置@V
@V 明确指定传递的参数名称
@V 明确指定传递的参数名称
尚硅⾕
@UserMessage("你是我的好朋友,请用上海话回答问题,并且添加一些表情符号。{{message}}")
String chat(@V("message") String userMessage);
这样我们就可以通过指定的参数来进行配置自定义的内容了,如message添加在系统提示词后面,就是用户内容
3.2、多个参数的情况
如果有两个或两个以上的参数,我们必须要用@V,在 SeparateChatAssistant 中定义方法
@UserMessage("你是我的好朋友,请用粤语回答问题。{{message}}") String chat2(@MemoryId int memoryId, @V("message") String userMessage); chat
测试: @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 结合使用
在S eparateChatAssistant 中添加方法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);
}
3.实战测试
3.1创建智能体并测试
我们用尚硅谷的医疗智能体硅谷小智作为测试
创建硅谷小智
首先创建智能体Agent
import dev.langchain4j.service.*;
import dev.langchain4j.service.spring.AiService;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
@AiService(
wiringMode = EXPLICIT,
chatModel = "qwenChatModel",
chatMemoryProvider = "chatMemoryProviderXiaozhi")
public interface XiaozhiAgent {
@SystemMessage(fromResource = "zhaozhi-prompt-template.txt")
String chat(@MemoryId Long memoryId, @UserMessage String userMessage);
}
然后导入提示词模板
zhaozhi-prompt-template.txt
你的名字是“硅谷小智”,你是一家名为“北京协和医院”的智能客服。
你是一个训练有素的医疗顾问和医疗伴诊助手。
你态度友好、礼貌且言辞简洁。
1、请仅在用户发起第一次会话时,和用户打个招呼,并介绍你是谁。
2、作为一个训练有素的医疗顾问:
请基于当前临床实践和研究,针对患者提出的特定健康问题,提供详细、准确且实用的医疗建议。请同时考虑可能的病
因、诊断流程、治疗方案以及预防措施,并给出在不同情境下的应对策略。对于药物治疗,请特别指明适用的药品名
称、剂量和疗程。如果需要进一步的检查或就医,也请明确指示。
3、作为医疗伴诊助手,你可以回答用户就医流程中的相关问题,主要包含以下功能:
AI分导诊:根据患者的病情和就医需求,智能推荐最合适的科室。
AI挂号助手:实现智能查询是否有挂号号源服务;实现智能预约挂号服务;实现智能取消挂号服务。
4、你必须遵守的规则如下:
在获取挂号预约详情或取消挂号预约之前,你必须确保自己知晓用户的姓名(必选)、身份证号(必选)、预约科室
(必选)、预约日期(必选,格式举例:2025-04-14)、预约时间(必选,格式:上午 或 下午)、预约医生(可
选)。
当被问到其他领域的咨询时,要表示歉意并说明你无法在这方面提供帮助。
5、请在回答的结果中适当包含一些轻松可爱的图标和表情。
6、今天是 {{current_date}}。
然后进行配置
配置持久化和记忆隔离
package com.AI.config;
import com.AI.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 XiaozhiAgentConfig {
@Autowired
private MongoChatMemoryStore mongoChatMemoryStore; // 确保已正确注入
@Bean
ChatMemoryProvider chatMemoryProviderXiaozhi() {
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20)
.chatMemoryStore(mongoChatMemoryStore) // 正确使用注入的实例
.build();
}
}
然后配置对话对象
@Data
public class ChatForm {
private Long memoryId;//对话id
private String message;//用户问题
}
最后添加Controller方法
@Tag(name = "硅谷小智")
@RestController
@RequestMapping("/xiaozhi")
public class XiaozhiController {
@Autowired
private XiaozhiAgent xiaozhiAgent;
@Operation(summary = "对话")
@PostMapping("/chat")
public String chat(@RequestBody ChatForm chatForm) {
return xiaozhiAgent.chat(chatForm.getMemoryId(), chatForm.getMessage());
}
}
测试
由于我们先前导入了MongoChatMemory的bean,所以我们这边直接使用先前的即可,然后由于我们配置了Knife4j的地址,所以我们基于Knife4j平台进行测试

首先进行第一次对话
然后再测试


发现基本能够按照我们的提示词进行回答,同时也能够记忆前面的内容
我们再看看MongoDB里的内容

基本也没有问题
3.2 Function Calling函数调用
在前面我们演示了大模型的基本使用,但是我们如果真的想要大模型为我们服务,就需要用Function Calling
例如,大语言模型本身并不擅长数学运算。如果应用场景中偶尔会涉及到数学计算,我们可以为他提供 一个 “数学工具”。当我们提出问题时,大语言模型会判断是否使用某个工具。
Function Calling 函数调用 也叫 Tools 工具
1.创建工具类
用@Tool注解方法:
- 既可以是静态的,也可以是非静态的
- 可以具有任何可见性(公有,私有)
@Component
public class CalculatorTools {
@Tool
double sum(double a, double b) {
System.out.println("调用加法运算");
return a + b;
}
@Tool
double squareRoot(double x) {
System.out.println("调用平方根运算");
return Math.sqrt(x);
}
}
2.配置工具类
在SeparateChatAssistant中添加tools属性配置
@AiService(
wiringMode = EXPLICIT,
chatModel = "qwenChatModel",
chatMemoryProvider = "chatMemoryProvider",
tools = "calculatorTools" //配置tools
)
最主要的是最后的tools,用来配置要使用的tools
3.测试工具类
@SpringBootTest
public class ToolsTest {
@Autowired
private SeparateChatAssistant separateChatAssistant;
@Test
public void testCalculatorTools() {
String answer = separateChatAssistant.chat(1, "1+2等于几,475695037565的平方根是多
少?");
//答案:3,689706.4865
System.out.println(answer);
}
}
测试后就可以看到持久化存储中SYSTEM.USER,AI以及Tools的消息,分析tools的调用流程
如下图所示,由于我们在前面的方法中定义了每当调用tools方法时就会输出一句话

如图表示我们成功地让大模型使用了tools方法
3.@Tool注解的可选字段
@Tool注解有两个可选字段:
- name(工具名称) :工具的名称,如果未提供该字段,方法名会作为工具的名称
- value(工具描述):工具的描述信息
根据工具的不同,即使没有任何描述,大语言模型可能也能很好地理解它(例如, add(a, b) 就很直观),但通常最好提供清晰且有意义的名称和描述。这样,大语言模型就能获得更多信息,以决定是否 调用给定的工具以及如何调用。
4.@P注解
方法参数可以选择使用@P注解进行标注
@P注解有两个字段:
- value: 参数的描述信息,这是必填字段
- required:表示该参数是否未必须项,默认值未true,此为可选字段
5.@ToolMemoryId
如果你的AIService方法中有一个参数使用 @MemoryId 注解,那么你也可以使用 @Tool 方法中的一个参数。提供给AIService方法的值将自动传递给 @ToolMemoryId 注解 @Tool 方法。如果你有多个用户, 或每个用户有多个聊天记忆,并且希望在 @Tool 方法中对它们进行区分,那么这个功能会很有用。
当注入了这个注解,我们的AIService里的id就会传递给Tool方法,就就可以对不同id做区分了
public class CalculatorTools {
@Tool(name = "加法", value = "返回两个参数相加之和")
double sum(
@ToolMemoryId int memoryId,
@P(value="加数1", required = true) double a,
@P(value="加数2", required = true) double b) {
System.out.println("调用加法运算 " + memoryId);
return a + b;
}
@Tool(name = "平方根", value = "返回给定参数的平方根")
double squareRoot(
@ToolMemoryId int memoryId, double x) {
System.out.println("调用平方根运算 " + memoryId);
return Math.sqrt(x);
}
}
4.高级用法
如果我们需要大模型通过其他数据,不仅限于我们的提示词来进行学习,训练,就需要使用RAG
1、如何让大模型回答专业领域的知识
LLM 的知识仅限于它所训练的数据。 如果你想让 LLM 了解特定领域的知识或专有数据,你可以:
- 使用 RAG
- 使用你的数据微调 LLM
- 结合 RAG 和微调
1.1、微调大模型
在现有大模型的基础上,使用小规模的特定任务数据进行再次训练,调整模型参数,让模型更精确地处 理特定领域或任务的数据。更新需重新训练,计算资源和时间成本高。
- 优点:一次会话只需一次模型调用,速度快,在特定任务上性能更高,准确性也更高。
- 缺点:知识更新不及时,模型训成本高、训练周期长。
- 应用场景:适合知识库稳定、对生成内容准确性和风格要求高的场景,如对上下文理解和语言生成 质量要求高的文学创作、专业文档生成等。
1.2、RAG
Retrieval-Augmented Generation 检索增强生成
将原始问题以及提示词信息发送给大语言模型之前,先通过外部知识库检索相关信息,然后将检索结果 和原始问题一起发送给大模型,大模型依据外部知识库再结合自身的训练数据,组织自然语言回答问 题。通过这种方式,大语言模型可以获取到特定领域的相关信息,并能够利用这些信息进行回复。
1.3、RAG常用方法
- 全文(关键词)搜索。这种方法通过将问题和提示词中的关键词与知识库文档数据库进行匹配来搜 索文档。根据这些关键词在每个文档中的出现频率和相关性对搜索结果进行排序。
- 向量搜索,也被称为 “语义搜索”。文本通过 嵌入模型被转换为 数字向量。然后,它根据查询向量 与文档向量之间的余弦相似度或其他相似性 / 距离度量来查找和排序文档,从而捕捉更深层次的语 义含义。
- 混合搜索。结合多种搜索方法(例如,全文搜索 + 向量搜索)通常可以提高搜索的效果。
2、向量搜索 vector search
2.1、向量 Vectors
可以将向量理解为从空间中的一个点到另一个点的移动。例如,在下图中,我们可以看到一些二维空间 中的向量:
a是一个从 (100, 50) 到 (-50, -50) 的向量,b 是一个从 (0, 0) 到 (100, -50) 的向量。

很多时候,我们处理的向量是从原点 (0, 0) 开始的,比如b。这样我们可以省略向量起点部分,直接说 b 是向量 (100, -50)。
如何将向量的概念扩展到非数值实体上呢(例如文本)?
2.2、维度 Dimensions
如我们所见,每个数值向量都有 x 和 y 坐标(或者在多维系统中是 x、y、z,...)。x、y、z... 是这个向量 空间的轴,称为维度。对于我们想要表示为向量的一些非数值实体,我们首先需要决定这些维度,并为 每个实体在每个维度上分配一个值。
例如,在一个交通工具数据集中,我们可以定义四个维度:“轮子数量”、“是否有发动机”、“是否可以在 地上开动”和“最大乘客数”。然后我们可以将一些车辆表示为:

因此,我们的汽车Car向量将是 (4, yes, yes, 5),或者用数值表示为 (4, 1, 1, 5)(将 yes 设为 1,no 设为 0)。
向量的每个维度代表数据的不同特性,维度越多对事务的描述越精确,我们可以使用“是否有翅膀”、“是 否使用柴油”、“最高速度”、“平均重量”、“价格”等等更多的维度信息。
2.3、相似度 Similarity
如果用户搜索 “轿车Car” ,你希望能够返回所有与 结果。向量搜索就是实现这个目标的一种方法。 “汽车automobile” 和 “车辆vehicle” 等信息相关
如何确定哪些是最相似的?
每个向量都有一个长度和方向。例如,在这个图中,p 和 a 指向相同的方向,但长度不同。p 和 b 正好指 向相反的方向,但有相同的长度。然后还有c,长度比p短一点,方向不完全相同,但很接近。

那么,哪一个最接近 p 呢?
如果“相似”仅仅意味着指向相似的方向,那么a 是最接近 p 的。接下来是 c。b 是最不相似的,因为它正 好指向与p 相反的方向。如果“相似”仅仅意味着相似的长度,那么 b 是最接近 p 的(因为它有相同的长 度),接下来是 c,然后是 a。'
由于向量通常用于描述语义意义,仅仅看长度通常无法满足需求。大多数相似度测量要么仅依赖于方 向,要么同时考虑方向和大小。
3、RAG的过程
RAG 过程分为 2 个不同的阶段:索引和检索。
3.1索引阶段
在索引阶段,对知识库文档进行预处理,可实现检索阶段的高效搜索。
加载知识库文档 ==> 将文档中的文本分段 ==> 利用向量大模型将分段后的文本转换成向量 ==> 将向量存 入向量数据库

为什么要进行文本分段?
大语言模型(LLM)的上下文窗口有限,所以整个知识库可能无法全部容纳其中。
- 你在提问中提供的信息越多,大语言模型处理并做出回应所需的时间就越长。
- 你在提问中提供的信息越多,花费也就越多。
- 提问中的无关信息可能会干扰大语言模型,增加产生幻觉(生成错误信息)的几率。
3.2、检索阶段
以下是检索阶段的简化图:
通过向量模型将用户查询转换成向量 ==> 在向量数据库中根据用户查询进行相似度匹配 ==> 将用户查询 和向量数据库中匹配到的相关内容一起交给LLM处理

4、文档加载器 Document Loader
4.1、常见文档加载器
- 来自 langchain4j 模块的文件系统文档加载器(FileSystemDocumentLoader)
- 来自 langchain4j 模块的类路径文档加载器(ClassPathDocumentLoader)
- 来自 langchain4j 模块的网址文档加载器(UrlDocumentLoader)
- 来自 langchain4j-document-loader-amazon-s3 模块的亚马逊 S3 文档加载器 (AmazonS3DocumentLoader)
- 来自 langchain4j-document-loader-azure-storage-blob 模块的 Azure Blob 存储文档加载器 (AzureBlobStorageDocumentLoader)
- 来自 langchain4j-document-loader-github 模块的 GitHub 文档加载器(GitHubDocumentLoader) 来自 langchain4j-document-loader-google-cloud-storage 模块的谷歌云存储文档加载器 (GoogleCloudStorageDocumentLoader)
- 来自 langchain4j-document-loader-selenium 模块的 Selenium 文档加载器 (SeleniumDocumentLoader)
4.2、测试文档加载
@SpringBootTest
public class RAGTest {
@Test
public void testReadDocument() {
//使用FileSystemDocumentLoader读取指定目录下的知识库文档
//并使用默认的文档解析器TextDocumentParser对文档进行解析
Document document = FileSystemDocumentLoader.loadDocument("E:/knowledge/测
试.txt");
System.out.println(document.text());
}
}
其他加载文档的方式
// 加载单个文档
Document document = FileSystemDocumentLoader.loadDocument("E:/knowledge/file.txt", new
TextDocumentParser());
// 从一个目录中加载所有文档
List<Document> documents = FileSystemDocumentLoader.loadDocuments("E:/knowledge", new
TextDocumentParser());
// 从一个目录中加载所有的.txt文档
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:*.txt");
List<Document> documents = FileSystemDocumentLoader.loadDocuments("E:/knowledge",
pathMatcher, new TextDocumentParser());
// 从一个目录及其子目录中加载所有文档
List<Document> documents =
FileSystemDocumentLoader.loadDocumentsRecursively("E:/knowledge", new
TextDocumentParser());
5、文档解析器 Document Parser
5.1、常见文档解析器
文档可以是各种格式的文件,比如 PDF、DOC、TXT 等等。为了解析这些不同格式的文件,有一个 “文档 解析器”(DocumentParser)接口,并且我们的库中包含了该接口的几种实现方式:
- 来自 langchain4j 模块的文本文档解析器(TextDocumentParser),它能够解析纯文本格式的文件 (例如 TXT、HTML、MD 等)。
- 来自 langchain4j-document-parser-apache-pdfbox 模块的 Apache PDFBox 文档解析器 (ApachePdfBoxDocumentParser),它可以解析 PDF 文件。
- 来自 langchain4j-document-parser-apache-poi 模块的 Apache POI 文档解析器 (ApachePoiDocumentParser),它能够解析微软办公软件的文件格式(例如 DOC、DOCX、PPT、 PPTX、XLS、XLSX 等)。
- 来自 langchain4j-document-parser-apache-tika 模块的 Apache Tika 文档解析器 (ApacheTikaDocumentParser),它可以自动检测并解析几乎所有现有的文件格式。
假设如果我们想解析PDF文档,那么原有的 TextDocumentParser 就无法工作了,我们需要引入 langchain4j-document-parser-apache-pdfbox
5.2、添加依赖
<!--解析pdf文档-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
</dependency>
5.3、解析pdf文档
/**
* 解析PDF
*/
@Test
public void testParsePDF() {
Document document = FileSystemDocumentLoader.loadDocument(
"E:/knowledge/医院信息.pdf",
new ApachePdfBoxDocumentParser()
);
System.out.println(document);
}
6、文档分割器 Document Splitter
6.1、常见文档分割器
LangChain4j 有一个 “文档分割器”(DocumentSplitter)接口,并且提供了几种开箱即用的实现方式:
按段落文档分割器(DocumentByParagraphSplitter)
按行文档分割器(DocumentByLineSplitter)
按句子文档分割器(DocumentBySentenceSplitter)
按单词文档分割器(DocumentByWordSplitter)
按字符文档分割器(DocumentByCharacterSplitter)
按正则表达式文档分割器(DocumentByRegexSplitter)
递归分割:DocumentSplitters.recursive (...)
默认情况下每个文本片段最多不能超过300个token
6.2、测试向量转换和向量存储
Embedding (Vector) Stores 常见的意思是 “嵌入(向量)存储” 。在机器学习和自然语言处理领域, Embedding 指的是将数据(如文本、图像等)转换为低维稠密向量表示的过程,这些向量能够保留数据 的关键特征。而 Stores 表示存储,即用于存储这些嵌入向量的系统或工具。它们可以高效地存储和检索 向量数据,支持向量相似性搜索,在文本检索、推荐系统、图像识别等任务中发挥着重要作用。
Langchain4j支持的向量存储:https://docs.langchain4j.dev/integrations/embedding-stores/
添加依赖:
<!--简单的rag实现-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
</dependency>
测试:
/**
* 加载文档并存入向量数据库
*/
@Test
public void testReadDocumentAndStore() {
//使用FileSystemDocumentLoader读取指定目录下的知识库文档
//并使用默认的文档解析器对文档进行解析(TextDocumentParser)
Document document = FileSystemDocumentLoader.loadDocument("E:/knowledge/人工智
能.md");
//为了简单起见,我们暂时使用基于内存的向量存储
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>
();
//ingest
//1、分割文档:默认使用递归分割器,将文档分割为多个文本片段,每个片段包含不超过 300个token,并且
有 30个token的重叠部分保证连贯性
//DocumentByParagraphSplitter(DocumentByLineSplitter(DocumentBySentenceSplitter(Docume
ntByWordSplitter)))
//2、文本向量化:使用一个LangChain4j内置的轻量化向量模型对每个文本片段进行向量化
//3、将原始文本和向量存储到向量数据库中(InMemoryEmbeddingStore)
EmbeddingStoreIngestor.ingest(document, embeddingStore);
//查看向量数据库内容
System.out.println(embeddingStore);
}
6.3、测试文档分割
/**
* 文档分割
*/
@Test
public void testDocumentSplitter() {
();
//使用FileSystemDocumentLoader读取指定目录下的知识库文档
//并使用默认的文档解析器对文档进行解析(TextDocumentParser)
Document document = FileSystemDocumentLoader.loadDocument("E:/knowledge/人工智
能.md");
//为了简单起见,我们暂时使用基于内存的向量存储
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>
//自定义文档分割器
//按段落分割文档:每个片段包含不超过 300个token,并且有 30个token的重叠部分保证连贯性
//注意:当段落长度总和小于设定的最大长度时,就不会有重叠的必要。
DocumentByParagraphSplitter documentSplitter = new DocumentByParagraphSplitter(
300,
30,
//token分词器:按token计算
new HuggingFaceTokenizer());
//按字符计算
//DocumentByParagraphSplitter documentSplitter = new
DocumentByParagraphSplitter(300, 30);
EmbeddingStoreIngestor
.builder()
.embeddingStore(embeddingStore)
.documentSplitter(documentSplitter)
.build()
.ingest(document);
}
6.4、token和token计算
DeepSeek:Token 用量计算 | DeepSeek API Docs
阿里百炼:百炼控制台
LangChain4j:
@Test
public void testTokenCount() {
String text = "这是一个示例文本,用于测试 token 长度的计算。";
UserMessage userMessage = UserMessage.userMessage(text);
//计算 token 长度
//QwenTokenizer tokenizer = new QwenTokenizer(System.getenv("DASH_SCOPE_API_KEY"),
"qwen-max");
HuggingFaceTokenizer tokenizer = new HuggingFaceTokenizer();
int count = tokenizer.estimateTokenCountInMessage(userMessage);
System.out.println("token长度:" + count);
}
6.5、工作方式
- 实例化一个 “文档分割器”(DocumentSplitter),指定所需的 “文本片段”(TextSegment)大小,并 且可以选择指定characters 或token的重叠部分。
- “文档分割器”(DocumentSplitter)将给定的文档(Document)分割成更小的单元,这些单元的性 质因分割器而异。例如,“按段落分割文档器”(DocumentByParagraphSplitter)将文档分割成段落 (由两个或更多连续的换行符定义),而 “按句子分割文档器”(DocumentBySentenceSplitter)使 用 OpenNLP 库的句子检测器将文档分割成句子,依此类推。
- 然后,“文档分割器”(DocumentSplitter)将这些较小的单元(段落、句子、单词等)组合成 “文本 片段”(TextSegment),尝试在单个 “文本片段”(TextSegment)中包含尽可能多的单元,同时不 超过第一步中设置的限制。如果某些单元仍然太大,无法放入一个 “文本片段”(TextSegment) 中,它会调用一个子分割器。这是另一个 “文档分割器”(DocumentSplitter),能够将不适合的单 元分割成更细粒度的单元。会向每个文本片段添加一个唯一的元数据条目 “index”。第一个 “文本片 段”(TextSegment)将包含 index=0 ,第二个是 index=1 ,依此类推
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)