本篇文章会通篇详细的讲清楚LangChain4j与DashScope集成的各个方面,从Springboot的集成到Ai对话、会话记忆、RAG、FunctionCalling、互联网搜索、结构化的输出、多模态等都给出相应的说明,希望通过这篇文章对于LLM不了解的同仁一样可以扩展出自己的AI应用。
DashScope(Qwen)
DashScope是阿里云开发的一个平台。
Qwen模型是由阿里云开发的一系列生成式AI模型。Qwen系列模型是专门为文本生成、摘要、问答和各种NPL任务而设计的。
简单SpringBoot集成
第一步:新增一个父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.xiaoxie</groupId>
<artifactId>LangChain4j</artifactId>
<packaging>pom</packaging>
<version>1.0.0</version>
<modules>
<module>chat</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<springboot.version>3.3.5</springboot.version>
<langchain4j_version>1.0.0-beta1</langchain4j_version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j_version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>${langchain4j_version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
这里几个版本说明:
- langchain4j:1.0.0-beta1
- SpringBoot:3.3.5
第二步:新增一个chat的子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>
<parent>
<groupId>com.xiaoxie</groupId>
<artifactId>LangChain4j</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>chat</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- springboot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- langchain4j集成dashcope starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
第三步:对于chat模块,修改项目的yml文件,添加关于dashcope的apikey及模型相关信息
XML
langchain4j:
community:
dashscope:
chat-model:
apiKey: ${QWEN_API_KEY}
modelName: qwen-max-latest
注意:这里apikey我配置到了系统的环境变量当中
需要使用大家可以自行去阿里云dashcope去申请apikey,建议也配置到自己的环境变量中
第四步:我们chat模块新增一个Controller类进行聊天对话请求
java
@RestController
@RequestMapping("/chat")
public class ChatController {
@Resource
private ChatLanguageModel chatLanguageModel;
@GetMapping("/ai")
public String chat(@RequestParam("message") String message) {
return chatLanguageModel.chat(UserMessage.from(message)).aiMessage().text();
}
}
使用AiService
第一步:我们新增一个包service,在其中定义一个接口Assistant (这个名称是可以自定义的)
java
public interface Assistant {
String chat(String message);
}
我们要实例化出来这个Assistant的Bean出来,这里实际上使用提反射机制来完成的。
第二步:我们新增config包,其中定义一个配置类AssistantConfig,用来实例化Assistant这个Bean,由于我们在配置类中我们需要使用到AiService这个工具类,所以此时在我们的chat这个模块中添加一个依赖:
XML
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
配置类如下:
java
@Configuration
public class AssistantConfig {
@Resource
private ChatLanguageModel chatLanguageModel;
@Bean
public Assistant getAssistant() {
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.build();
}
}
第三步:新增一个Controller类,使用AiService接口的代理类来实现聊天对话
java
@RestController
@RequestMapping("/assistant")
public class AssistantController {
@Resource
private Assistant assistant;
@GetMapping("/chat")
public String chat(@RequestParam("message") String message) {
return assistant.chat(message);
}
}
调整AI对话中的场景角色
使用ChatLanguageModel
在Controller中我们在调用chat方法的时候可以传入多个Message,其中有一个SystemMessage就可以用来设置角色和场景相关的信息。而UserMessage是来自于用户传递过来的请求消息。
我们此时在配置文件中定义一个SystemMessage文本配置信息
java
ai:
systemMessage: 你的名字中天鉴,拥有强大的智慧
最终ChatController修改为如下:
java
@RestController
@RequestMapping("/chat")
public class ChatController {
@Resource
private ChatLanguageModel chatLanguageModel;
@Value("${ai.systemMessage}")
private String systemMessage;
@GetMapping("/ai")
public String chat(@RequestParam("message") String message) {
return chatLanguageModel.chat(List.of(SystemMessage.systemMessage(systemMessage),
UserMessage.from(message))).aiMessage().text();
// return chatLanguageModel.chat(UserMessage.from(message)).aiMessage().text();
}
}
使用AiService
在初始化AiService的Bean的时候,我们可以在创建Bean的类上加上一个注解@SystemMessage,给定其中的value值则可以完成指定。
java
@Bean
@SystemMessage("${ai.systemMessage}")
public Assistant getAssistant() {
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.build();
}
如果我们不使用@SystemMessage注解的话也可以在构造的链式调用时调用systemMessageProvider()来指定
java
@Value("${ai.systemMessage}")
private String systemMessage;
@Bean
public Assistant getAssistant() {
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.systemMessageProvider(request -> systemMessage)
.build();
}
会话记忆
我们一般来说实现会话记忆的方式如下:
实际上我们调用原生API的时候,我们要实现的话要把所有会话历史内容发送给LLM
我们再新增一个Maven模块:memory
基础的pom.xml如下:
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xiaoxie</groupId>
<artifactId>LangChain4j</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>memory</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<!-- springboot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- langchain4j集成dashcope starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
主配置文件如下:
XML
langchain4j:
community:
dashscope:
chat-model:
apiKey: ${QWEN_API_KEY}
modelName: qwen-max-latest
我们要创建一个配置类来配置ChatMemory这个Bean
java
@Configuration
public class ChatMemoryConfig {
@Bean("messageWindowChatMemory")
public ChatMemory initChatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(10) // 表示会话中最多保存10条消息
.build();
}
}
我们在与ai聊天的时候把用户的消息以及ai回复的所有消息都给到LLM
Controller类写为如下:
java
@RestController
@RequestMapping("/memory")
public class MemoryController {
@Resource
private ChatLanguageModel chatLanguageModel;
@Resource(name = "messageWindowChatMemory")
private ChatMemory chatMemory;
@GetMapping("/chat")
public String chat(@RequestParam("message") String message) {
chatMemory.add(UserMessage.from(message));
// 这里要把chatMemory的所有历史消息都传给chatLanguageModel
ChatResponse response = chatLanguageModel.chat(chatMemory.messages());
chatMemory.add(response.aiMessage());
return response.aiMessage().text();
}
}
这个时候我们再聊天时ai就感知上有了记忆,从上面可以看出来我们与ai聊天的话是无状态的,为了让他有记忆我们得每次转给它的消息中包含它历史的回复信息。
MemoryId
我们在与ai聊天的时候会创建很多会话,每个会话之间需要进行隔离(各自有自己独立的上下文),要达到这个隔离的效果我们就要使用到MemoryId。
我们新增一个AiService接口
java
public interface Assistant {
String chat(@MemoryId String memoryId, @UserMessage String message);
}
这个接口中的方法比这前多了一个参数memoryId,我们就可以使用它来进行会话隔离
@MemoryId:表示参数是memoryId
@UserMessage:表示参数是用户提交的消息
创建一个配置类来创建这个接口对应的代理Bean
java
@Configuration
public class AssistantConfig {
@Resource
private ChatLanguageModel chatLanguageModel;
@Bean
public Assistant assistant() {
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
// 必须要提供这个配置,否则会报错
// 这个时候当传入不同的memoryId的时候,会创建不同的ChatMemory,从而达到按memoryId隔离的chatMemory
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
.build();
}
}
我们的Controller类中可以添加如下代码进行测试
java
@Resource
private Assistant assistant;
@GetMapping("/assistant/chat")
public String assistantChat(@RequestParam("memoryId") String memoryId, @RequestParam("message") String message) {
return assistant.chat(memoryId, message);
}
从上我们使用AiService按memoryId进行了会话记忆隔离,当我们不使用AiService时要实现隔离效果可以如下操作:
在controller类中再新增如下代码,来实现不使用AiService达到按memoryId隔离的效果
java
private final Map<String, ChatMemory> chatMemoryMap = new ConcurrentHashMap<>();
@GetMapping("/memoryId/chat")
public String memoryIdChat(@RequestParam(value = "memoryId", required = false) String memoryId, @RequestParam("message") String message) {
// 判断传入memory是否为空
if (memoryId == null || memoryId.isEmpty()) {
memoryId = "default";
}
// 获取或者创建 ChatMemory对象
ChatMemory chatMemory = chatMemoryMap.computeIfAbsent(memoryId, key -> MessageWindowChatMemory.withMaxMessages(10));
chatMemory.add(UserMessage.from(message));
ChatResponse response = chatLanguageModel.chat(chatMemory.messages());
chatMemory.add(response.aiMessage());
return response.aiMessage().text();
}
会话记忆存储
MessageWeindowChatMemory中有一个属性:ChatMemoryStore
这个ChatMemoryStore有一个默认实现是InMemoryChatMemoryStore,它的存储是在内存中的,如果我们要存储到指定的存储设备(如:数据库)则我们需要自行去实现这个chatMemoryStore接口。
如果我们要把这个会话记忆存到mysql数据库中具体实现如下:
第一步:新增一个数据库及数据表,用来存储会话记录
sql
CREATE TABLE `chat_memory_message` (
`id` int NOT NULL AUTO_INCREMENT,
`memory_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`message` json DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
第二:我们使用了druid作为数据源,mybatis操作数据库,数据库使用mysql,所以对应的需要添加如下依赖:
XML
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
第三步:新增数据源配置
XML
# 数据源配置
spring:
datasource:
druid:
driverClassName: com.mysql.cj.jdbc.Driver
# 默认据源
url: jdbc:mysql://localhost:3306/llm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: A7#mZ9!pL3$q
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: druid
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
由于数据源的配置比较长,且一般不会动,我们单独把它放在application-druid.yml当中,在主配置文件application.yml激活这个配置即可,主配置文件激活这个配置,最终主配置文件变为如下:
XML
langchain4j:
community:
dashscope:
chat-model:
apiKey: ${QWEN_API_KEY}
modelName: qwen-max-latest
# 启用Druid数据源配置,在application-druid.yml中
spring:
profiles:
active: druid
################# mybatis ###############
mybatis:
mapper-locations: classpath:mapper/**/*Mapper.xml
type-aliases-package: com.xiaoxie.**.domain
configuration:
map-underscore-to-camel-case: true
type-handlers-package: com.xiaoxie.**.mapper.typehandler
第三步:添加实体类
java
@Data
public class ChatMemoryMessage {
private Integer id;
private String memoryId;
private String message;
}
第四步:新增mapper接口及对应的mapper映射文件
java
@Mapper
public interface ChatMemoryMessageMapper {
String getMessageByMemoryId(@Param("memoryId") String memoryId);
void update(ChatMemoryMessage chatMemoryMessage);
void insert(ChatMemoryMessage chatMemoryMessage);
void deleteByMemoryId(@Param("memoryId") String memoryId);
}
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaoxie.mapper.ChatMemoryMessageMapper">
<insert id="insert">
insert into chat_memory_message(memory_id, message)
values (#{memoryId}, #{message})
</insert>
<update id="update">
update chat_memory_message
<set>
<if test="message != null">
message = #{message},
</if>
</set>
where memory_id = #{memoryId}
</update>
<delete id="deleteByMemoryId">
delete from chat_memory_message where memory_id = #{memoryId}
</delete>
<select id="getMessageByMemoryId" resultType="java.lang.String">
select message from chat_memory_message where memory_id = #{memoryId}
</select>
</mapper>
第五步:新增一个ChatMemoryStore的实现
java
@Component("chatMemoryMessage4MySQL")
public class ChatMemoryMessage4MySQL implements ChatMemoryStore {
@Resource
private ChatMemoryMessageMapper chatMemoryMessageMapper;
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String messages = chatMemoryMessageMapper.getMessageByMemoryId(memoryId.toString());
return ChatMessageDeserializer.messagesFromJson(messages);
}
@Transactional
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String messagesJson = ChatMessageSerializer.messagesToJson(messages);
ChatMemoryMessage chatMemoryMessage = ChatMemoryMessage.builder()
.memoryId(memoryId.toString())
.message(messagesJson)
.build();
if (chatMemoryMessageMapper.getMessageByMemoryId(memoryId.toString()) != null) {
chatMemoryMessageMapper.update(chatMemoryMessage);
} else {
chatMemoryMessageMapper.insert(chatMemoryMessage);
}
}
@Transactional
@Override
public void deleteMessages(Object memoryId) {
chatMemoryMessageMapper.deleteByMemoryId(memoryId.toString());
}
}
第六步:在AssistantConfig这个类中再配置一个使用这个存储方案的Assistant的Bean
java
@Resource(name="chatMemoryMessage4MySQL")
private ChatMemoryStore chatMemoryStore;
@Bean("memoryAssistantMysql")
public Assistant memoryAssistantMysql() {
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder()
.maxMessages(10)
.id(memoryId) // 这个不能少,在传到mybati时需要这个memoryid
.chatMemoryStore(chatMemoryStore)
.build())
.build();
}
第七步:Controller中我们则可以使用这个Bean来进聊天
java
@Resource(name="memoryAssistantMysql")
private Assistant assistantMySQL;
@GetMapping("/memoryStore/chat/assistant")
public String memoryStoreChatAssistant(@RequestParam("memoryId") String memoryId, @RequestParam("message") String message) {
return assistantMySQL.chat(memoryId, message);
}
RAG能力
Rag叫做增强检索技术。
当我们使用LLM进行聊天的时候,LLM通常是无法知晓我们内部的信息的。我们如果希望要让LLM在聊天中可以读取到我们内部私有的知识库文档,就要用到RAG
我们先了准备一个东西:EmbeddingModel,它可以把我们的文档数据转为向量化的数据。
我们新增一个Maven子模块:rag
基于内存的向量存储
新的rag模块需要添加如下基础依赖
XML
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
</dependency>
<!-- springboot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- langchain4j集成dashcope starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
注意:langchain4j-easy-rag 依赖我们继承父项目的,所以我们要在父亲项目中添加如下管理依赖
XML
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>${langchain4j_version}</version>
</dependency>
基础的配置文件内容如下:
XML
langchain4j:
community:
dashscope:
chat-model:
apiKey: ${QWEN_API_KEY}
modelName: qwen-max-latest
声明一个AiService接口
java
public interface Assistant {
String chat(@MemoryId String memoryId, @UserMessage String message);
}
注意:这里还使用了memoryId进行隔离,当然这里不是必须的!
创建配置类来配置Assistant的Bean
java
@Configuration
public class AssistantConfig {
@Resource
private ChatLanguageModel chatLanguageModel;
// 基于内存的向量存储
@Bean
public EmbeddingStore<TextSegment> embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
@Bean
public Assistant assistant(EmbeddingStore<TextSegment> embeddingStore) {
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
// 这里直接使用一个基于内存的向量存储库
.contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
.build();
}
}
后续我们要读取我们内部的文档信息,我们先新增一个工具类DocumentUtils
java
public class DocumentUtils {
/**
* 根据指定的目录加载目录下所有文档
* @param dir 目录
* @return 文档列表
*/
public static List<Document> loadDocuments(String dir) {
return FileSystemDocumentLoader.loadDocuments(dir);
}
/**
* 根据指定的目录加载目录下指定类型的文件
* @param dir 指定要加载的目录
* @param filter 文件类型
* @return 文档列表
*/
public static List<Document> loadDocuments(String dir, String filter) {
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:*" + filter);
return FileSystemDocumentLoader.loadDocuments(dir, pathMatcher);
}
/**
* 根据指定的目录加载目录下所有文档
* @param dir 目录
* @param containsSubDir 是否包含子目录
* @return 文档列表
*/
public static List<Document> loadDocuments(String dir,Boolean containsSubDir) {
if (containsSubDir) {
return FileSystemDocumentLoader.loadDocumentsRecursively(dir);
}
return FileSystemDocumentLoader.loadDocuments(dir);
}
/**
* 根据指定的目录加载目录下指定类型的文件
* @param dir 指定要加载的目录
* @param filter 文件类型
* @param containsSubDir 是否包含子目录
* @return 文档列表
*/
public static List<Document> loadDocuments(String dir,String filter,Boolean containsSubDir) {
// 多级目录时写**
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:**" + filter);
if (containsSubDir) {
return FileSystemDocumentLoader.loadDocumentsRecursively(dir, pathMatcher);
}
return FileSystemDocumentLoader.loadDocuments(dir, pathMatcher);
}
}
默认的langchang4j中使用了Apache的Tika来读取解析各种不同类型的文档
在controller中新增两个请求方法一个用来加载文档到指定的embeddingStore,一个用来与ai聊天对话
java
@RestController
@RequestMapping("/rag")
public class RagController {
@Resource
private Assistant assistant;
@Resource
private EmbeddingStore<TextSegment> embeddingStore;
@GetMapping("/load")
public String load() {
List<Document> documents = DocumentUtils.loadDocuments("E:\\project\\IdeaProjects\\AI\\LangChain4j1.0\\Langchain4j\\rag\\src\\main\\resources\\documents");
// 读取到的文档集合写到指定的embeddingStore,当前这里是memory中
EmbeddingStoreIngestor.ingest(documents, embeddingStore);
return "Load Success!!";
}
@GetMapping("/chat")
public String chat(@RequestParam("memoryId") String memoryId, @RequestParam("message") String message) {
return assistant.chat(memoryId, message);
}
}
这个时候我们先请求/load,此时会把目录下所有文档进行解析分片存储到embeddingStore当中,然后我们请求/chat进行对话的时候就LLM可以基于我们的embeddingStroe中的内容进行回答,原因就在于我们创建AiService的Bean的时候指定了.contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
基于pgvector的向量存储
pgvector的安装
第一步:先安装postgresql
第二步:下载pgvector插件:下载地址:vector: Open-source vector similarity search for Postgres / PostgreSQL Extension Network
第三步: 安装visual studio,这里主要是选"使用C++的桌面开发" 用于后面插件的编译与安装
在上面都完成后使用下面的方式进行插件安装
bash
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
cd C:\Users\xxx\Downloads\vector-0.7.3
set "PGROOT=C:\Program Files\PostgreSQL\16"
nmake /F Makefile.win
nmake /F Makefile.win install
最后在数据库连接工具中,选中具体的数据库实例,执行下面的命令就可以扩展vector类型了
sql
CREATE EXTENSION vector;
langchain4j中使用
首先我们在配置文件中添加pgvector配置信息
sql
pgvector:
database: vectorTest
host: localhost
port: 5433
user: postgres
password: A7***$q
table: my_embeddings
dimension: 1024
这里根据你本地的postgresql进行配置,其中有一个配置dimension配置为了1024表示向量维度我们设置为了1024,这个是由于后面要使用的text-embedding-v3模型有关(这个embeddingModel支持的向量维度有1024)。
pom.xml中需要添加postgresql和pgvector的依赖
XML
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-pgvector</artifactId>
</dependency>
由于我们依赖说明
postgresql:继承了SpringBoot的依赖,这里不需要指定版本
langchain4j-pgvector:继承我们自己的父项目的版本,所以我们在父项目中pom管理中此时需要添加如下依赖的管理
XML
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-pgvector</artifactId>
<version>${langchain4j_version}</version>
</dependency>
我使用计划使用dashScope的text-embedding-v3模型,所以配置文件中添加相关配置,所以关于模型的相关配置最终如下:
XML
langchain4j:
community:
dashscope:
chat-model:
apiKey: ${QWEN_API_KEY}
modelName: qwen-max-latest
embedding-model:
apiKey: ${QWEN_API_KEY}
modelName: text-embedding-v3
注意:我们要注意一下模型的向量维度!这里我们使用text-embedding-v3可以使用向量维度1024
在config包下我们添加一个配置信息映射类:PgVectorProp
java
/**
* pgvector配置
*/
@Configuration
@ConfigurationProperties(prefix = "pgvector")
@Data
public class PgVectorProp {
private String host;
private int port;
private String database;
private String user;
private String password;
private String table;
private int dimension;
}
新增一个EmbeddingStore的Bean的配置类:EmbeddingStore4PgConfig
java
@Configuration
public class EmbeddingStore4PgConfig {
@Resource
private PgVectorProp pgVectorProp;
@Bean("embeddingStore4pg")
public EmbeddingStore<TextSegment> embeddingStore() {
return PgVectorEmbeddingStore.builder()
.table(pgVectorProp.getTable())
.dropTableFirst(true) // 表示每次启动都先删除表 重新创建
.database(pgVectorProp.getDatabase())
.host(pgVectorProp.getHost())
.port(pgVectorProp.getPort())
.user(pgVectorProp.getUser())
.password(pgVectorProp.getPassword())
.dimension(pgVectorProp.getDimension())
.build();
}
}
在AssistanConfig类中我们新增一个Assistant的Bean的创建,整体代码如下:
java
@Configuration
public class AssistantConfig {
@Resource
private ChatLanguageModel chatLanguageModel;
@Resource
private EmbeddingModel embeddingModel;
// pgvector向量存储
@Resource(name="embeddingStore4pg")
private EmbeddingStore<TextSegment> embeddingStore4pg;
// 内存向量存储
@Bean(name="embeddingStore4Mem")
public EmbeddingStore<TextSegment> embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
@Bean("assistant4Mem")
public Assistant assistant(@Qualifier("embeddingStore4Mem") EmbeddingStore<TextSegment> embeddingStore) {
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
// 这里直接使用一个基于内存的向量存储库
.contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
.build();
}
@Bean("assistant4pg")
public Assistant assistant4pg() {
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
// 这里直接使用一个基于pg的向量存储库
.contentRetriever(EmbeddingStoreContentRetriever.builder()
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore4pg)
.maxResults(5)
.minScore(0.75)
.build())
.build();
}
}
我们此时关注assistant4pg()这个方法,在.contentRetriever()
中我们指定使用我们指定的embeddingModel(也就是我们配置的:text-embedding-v3模型),同时我们指定embeddingStore中使用我们创建的embeddingStore4pg,这样的话存储会存储到指定的库中。
Controller中我们要进行文档的解析加载与聊天添加如下代码实现加载文档到数据库和进行聊天
java
@Resource(name="assistant4pg")
private Assistant assistant4pg;
@Resource(name="embeddingStore4pg")
private EmbeddingStore<TextSegment> embeddingStore4pg;
@Resource
private EmbeddingModel embeddingModel;
@GetMapping("/pg/load")
public String pgload() {
List<Document> documents = DocumentUtils.loadDocuments("E:\\project\\IdeaProjects\\AI\\LangChain4j1.0\\Langchain4j\\rag\\src\\main\\resources\\documents");
// 读取到的文档集合写到指定的embeddingStore,当前这里是memory中
// EmbeddingStoreIngestor.ingest(documents, embeddingStore4pg);
// 这里要调整一下不能使用默认的,修改为如下
EmbeddingStoreIngestor.builder()
.embeddingStore(embeddingStore4pg) // 使用的embeddingStore4pg是存储到pgvector的
.embeddingModel(embeddingModel) // 使用的embeddingModel是text-embedding-v3
.build().ingest(documents);
return "Load Success!!";
}
@GetMapping("/pg/chat")
public String pgchat(@RequestParam("memoryId") String memoryId, @RequestParam("message") String message) {
return assistant4pg.chat(memoryId, message);
}
切分文档方式

从上面我们可以看到DocumentSplitter有7个实现,如果我们加载文档到embeddingStore的时候不指定切分的方式则使用默认的切分方式:DocumentByParagraphSplitter(按段落去进行切分),如果我们需要调整这个切分的方式,则我们需要手动指定文档切分的方式。
如下所示我们在加载文档到embeddingStore的时候,指定按行来进行切分,并指定每段多少个字符,每段有多少重叠的字符。
注意:这里的重叠表示,上一个切出来的文本内容与下一个切出来的文本内容有多少个字符是重叠的,这个设置也有一些讲究,如果是对于连贯的内容则要重叠多一些字符这样的话在与LLM对话进行查询知识的时候它会更加精确找到相关的知识,对于非连贯的内容比较每一块都是不同的具体知识则可以考虑少一些重叠,因为多的重叠并会提高它的精度反而会导致查询的相关数据更多。
此时我们/load请求的方法写为如下:
java
@GetMapping("/pg/load")
public String pgload() {
List<Document> documents = DocumentUtils.loadDocuments("E:\\project\\IdeaProjects\\AI\\LangChain4j1.0\\Langchain4j\\rag\\src\\main\\resources\\documents");
// 读取到的文档集合写到指定的embeddingStore,当前这里是memory中
// EmbeddingStoreIngestor.ingest(documents, embeddingStore4pg);
// 这里要调整一下不能使用默认的,修改为如下
EmbeddingStoreIngestor.builder()
.embeddingStore(embeddingStore4pg) // 使用的embeddingStore4pg是存储到pgvector的
.embeddingModel(embeddingModel) // 使用的embeddingModel是text-embedding-v3
.documentSplitter(new DocumentByLineSplitter(200,10))
.build().ingest(documents);
return "Load Success!!";
}
FunctionCalling
基础使用
接下来我们新增一个新的Maven子模块functionCall
其础的pom依赖如下:
XML
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<!-- springboot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- langchain4j集成dashcope starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
基础配置文件
XML
langchain4j:
community:
dashscope:
chat-model:
apiKey: ${QWEN_API_KEY}
modelName: qwen-max-latest
在接下来操作前我们先简单说明一下FunctionCalling
通常来说我们使用LLM都是用来对话的,如果我们希望使用我们的语言来调用一些自定义函数(完成业务功能而不仅仅只是对话),那要实现这个则就是使用FunctionCalling来完成的。
我们在func包下定义一个类Calculator
java
@Data
public class Calculator {
private int a;
private int b;
public int add() {
return a + b;
}
}
Controller中对于聊天请求返回的结果中判断是否有toolExecutionRequests,有就执行工具,没有就直接使用ai返回的响应信息
java
@Slf4j
@RestController
@RequestMapping("/functionCall")
public class FunctionCallController {
@Resource
private ChatLanguageModel chatLanguageModel;
@GetMapping("/chat")
public String chat(@RequestParam("message") String message) {
// 定义ToolSpecification
ToolSpecification tool1 = ToolSpecification.builder()
.name("Calculator")
.description("输入两个数,对这两个数进行求和")
.parameters(JsonObjectSchema.builder()
.addIntegerProperty("a", "第一个数")
.addIntegerProperty("b", "第二个数")
.required("a","b") // 指定必须的参数
.build())
.build();
// 聊天时候,使用toolSpecifications
ChatResponse response = chatLanguageModel.chat(ChatRequest.builder()
.messages(UserMessage.from(message))
.parameters(ChatRequestParameters.builder()
.toolSpecifications(tool1)
.build()
)
.build());
// 在用户聊天时判断是否有toolExecutionRequests,如果没有,但是ai返回了响应信息则返回ai的响应信息
List<ToolExecutionRequest> toolExecutionRequests = response.aiMessage().toolExecutionRequests();
if (toolExecutionRequests == null || toolExecutionRequests.isEmpty()) {
return response.aiMessage().text().isEmpty() ? "未收到ai有效响应" : response.aiMessage().text();
}
String result = "";
for (ToolExecutionRequest toolExecutionRequest : toolExecutionRequests) {
try{
String className = "com.xiaoxie.func." + toolExecutionRequest.name();
Class<?> clazz = Class.forName(className);
Calculator calculator = (Calculator) JsonUtils.fromJson(toolExecutionRequest.arguments(), clazz);
result = "对这两个数:【" + calculator.getA() + "," + calculator.getB() + "】求和的结果是:" + calculator.add();
break; // 工具执行一次则退出
} catch (Exception e) {
log.error("执行工具失败", e);
throw new RuntimeException("执行工具失败", e);
}
}
return result;
}
}
像上面这样我们手动去实现函数调用的过程还是比较麻烦的!!
除了上面这个手动去实现的方式,我们还可以使用注解
func包下新增一个AiServiceCalculator
java
public class AiServiceCalculator {
@Tool("计算两数之和")
public int add(int a, int b) {
return a + b;
}
}
这里我们使用了@Tool注解
在AiService的Bean创建中需要使用.tools来指定对应的工具类对象
java
@Configuration
public class AssistantConfig {
@Resource
private ChatLanguageModel chatLanguageModel;
@Bean
public Assistant assistant() {
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.tools(new AiServiceCalculator())
.build();
}
}
接下来在Controller当中则可以使用这个AiService的Bean来进行聊天
javascript
@Resource
private Assistant assistant;
@GetMapping("/assistant/chat")
public String assistantChat(@RequestParam("message") String message)
{
return assistant.chat(message);
}
注意:此时我们与ai进行对话的过程中如果需要进行调用工具函数会自动去调用。
如果我们的函数工具类中有多个@Tool方法,LLM会根据我们的语义自动选择相应的函数执行
联网搜索能力
我们要有联网搜索能力使用的是serarchapi
首先我们要去申请对应的apikey
地址:Google Search API for real-time SERP scraping
添加对应的pom依赖
XML
<!-- web search -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-web-search-engine-searchapi</artifactId>
</dependency>
yml的配置中我们配置一下关于searchapi相关的配置参数
XML
websearch:
apiKey: ${SEARCH_API_KEY}
engine: baidu
同时我们添加一个百炼的新的模型:qwq-plus,注意这个模型只支持流式输出所以整体的配置文件改为如下:
XML
langchain4j:
community:
dashscope:
chat-model:
apiKey: ${QWEN_API_KEY}
modelName: qwen-max-latest
streaming-chat-model:
apiKey: ${QWEN_API_KEY}
modelName: qwq-plus
websearch:
apiKey: ${SEARCH_API_KEY}
engine: baidu
由于它只支持流式输出,所以不要添加依赖:
XML
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
</dependency>
添加一个新的AiService的接口
java
public interface WebAssistant {
Flux<String> chat(String message);
}
由于我们需要进行联网搜索,所以先要初始化一个SearchApiWebSearchEngine的Bean,新增配置类:WebSearchConfig
java
@Configuration
public class WebSearchConfig {
@Resource
private WebSearchProp webSearchProp;
@Bean
public SearchApiWebSearchEngine SearchApiWebSearchEngine() {
return SearchApiWebSearchEngine.builder()
.engine(webSearchProp.getEngine())
.apiKey(webSearchProp.getApiKey())
.build();
}
}
其中WebSearchProp用来读取映射配置文件中的值
java
@Configuration
@ConfigurationProperties(prefix = "websearch")
@Data
public class WebSearchProp {
private String apiKey;
private String engine;
}
在AiService配置类中进行配置添加代码如下:
java
@Resource
private StreamingChatLanguageModel streamingChatLanguageModel;
@Bean("webAssistant")
public WebAssistant webAssistant(SearchApiWebSearchEngine searchApiWebSearchEngine) {
return AiServices.builder(WebAssistant.class)
.streamingChatLanguageModel(streamingChatLanguageModel)
.tools(new AiServiceCalculator(),new WebSearchTool(searchApiWebSearchEngine))
.build();
}
这里我们可以看到在.tools中我们不仅仅是添加了我们前面创建 一个工具类对象,我们构造了一个WebSearchTool对象,这个对象在构造时传入了我们创建的searchApiWebSearchEngine
接下来在Controller当中我们就可以使用到联网的查询能力
java
@RestController
@RequestMapping("/webSearch")
public class WebSearchController {
@Resource
private WebAssistant webAssistant;
@GetMapping(value = "/chat", produces = "text/plain;charset=UTF-8")
public Flux<String> chat(@RequestParam("message") String message) {
return webAssistant.chat(message);
}
}
这里我们要使用Flux<String>
输出的原因就是qwq-plus只支持流式输出
结构化输出
注意:如果是要指定输也JSON格式的数据qwen是不支持的!!
我们新增一个子模块struct_output
相关的pom依赖如下:
XML
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<!-- web search -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-web-search-engine-searchapi</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
</dependency>
<!-- springboot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- langchain4j集成dashcope starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
主配置文件如下:
XML
langchain4j:
community:
dashscope:
chat-model:
apiKey: ${QWEN_API_KEY}
modelName: qwen-max-latest
streaming-chat-model:
apiKey: ${QWEN_API_KEY}
modelName: qwq-plus
websearch:
apiKey: ${SEARCH_API_KEY}
engine: baidu
我们使用的是qwen的模型是不支持JSON格式输出的,现在我们做的功能是要输出一个实体对象
新增一个Person类
java
@Data
public class Person {
private String name;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date birthday;
private String address;
@Description("头衔及职位")
private String title;
}
新增一个Assistant的接口方法,此方法返回的类型就是Person对象
java
public interface Assistant {
String chat(String message);
// 这个方法用来返回一个person对象
Person getPerson(String message);
}
配置类中我们要做如下几件事:
1、关联联网搜索的配置信息
java
@Configuration
@ConfigurationProperties(prefix = "websearch")
@Data
public class WebSearchProp {
private String apiKey;
private String engine;
}
2、配置SearchApiWebSearchEngine
java
@Configuration
public class WebSearchConfig {
@Resource
private WebSearchProp webSearchProp;
@Bean
public SearchApiWebSearchEngine webSearchEngine() {
return SearchApiWebSearchEngine.builder()
.engine(webSearchProp.getEngine())
.apiKey(webSearchProp.getApiKey())
.build();
}
}
3、配置一个基顾的AiService的Bean
java
@Configuration
public class AssistantConfig {
@Resource
private ChatLanguageModel chatLanguageModel;
@Bean
public Assistant assistant(SearchApiWebSearchEngine searchApiWebSearchEngine) {
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.build();
}
}
完成上面的操作后我们在Controller类中进行请求,联网查询信息并让它返回对应的对象
java
@RestController
@RequestMapping("/structOutput")
public class StructOutputController {
@Resource
private Assistant assistant;
@Resource
private ChatLanguageModel chatLanguageModel;
@Resource
private SearchApiWebSearchEngine searchApiWebSearchEngine;
@GetMapping("/chat")
public Person chat(@RequestParam("message") String message) {
List<ToolSpecification> toolSpecifications = ToolSpecifications.toolSpecificationsFrom(WebSearchTool.class);
ChatResponse response = chatLanguageModel.chat(ChatRequest.builder()
.messages(UserMessage.from(message))
.parameters(ChatRequestParameters.builder()
.toolSpecifications(toolSpecifications)
.build())
.build());
// 联网查询
WebSearchTool webSearchTool = WebSearchTool.from(searchApiWebSearchEngine);
List<ToolExecutionRequest> toolExecutionRequests = response.aiMessage().toolExecutionRequests();
if (toolExecutionRequests == null || toolExecutionRequests.isEmpty()) {
return null;
}
for (ToolExecutionRequest toolExecutionRequest : toolExecutionRequests) {
// 联网查询结果
String s = webSearchTool.searchWeb(toolExecutionRequest.arguments());
if (!s.isEmpty()) {
// 对于查询到的结果让ai进行解析返回一个Person对象
return assistant.getPerson("从以下信息中获取雷军的信息:" + s);
}
}
return null;
}
}
此时我们请求:
java
http://localhost:8080/structOutput/chat?message=帮我查一下雷军个人简历的信息
最终返回结果是:
java
{
"name": "雷军",
"birthday": "1969-12-16",
"address": "中国",
"title": "小米集团创始人、董事长兼首席执行官;金山软件前董事长;武汉大学校友"
}
实际上我们现在可以拿到对象了我们想输出是JSON也是可以的!!
如果我们要让ai输出的时候以Json格式输出可以类似像下面这样处理
java
@GetMapping("/chat2json")
public String chat2json(@RequestParam("message") String message) {
// 定义响应的格式
ResponseFormat responseFormat = ResponseFormat.builder()
.type(ResponseFormatType.JSON)
.jsonSchema(JsonSchema.builder()
.rootElement(JsonObjectSchema.builder()
.addStringProperty("name", "姓名")
.addStringProperty("birthday", "生日")
.addStringProperty("address", "地址")
.addStringProperty("title", "头衔及职位")
.build())
.build())
.build();
ChatResponse response = chatLanguageModel.chat(ChatRequest.builder()
.messages(UserMessage.from(message))
.parameters(ChatRequestParameters.builder()
.responseFormat(responseFormat)
.build())
.build());
return response.aiMessage().text();
}
多模态
新增一个子模块mutil
添加相关的基础pom依赖
XML
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<!-- web search -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-web-search-engine-searchapi</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
</dependency>
<!-- springboot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- langchain4j集成dashcope starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
配置文件
XML
langchain4j:
community:
dashscope:
chat-model:
apiKey: ${QWEN_API_KEY}
modelName: qwen-vl-max
注意:这里更换了模型为qwen-vl-max,因为这个模型是支持图片理解的,后续我们要传入图片让模型去识别
在resources下新增一个documents存入我们的图片文档

新增一个controller类
java
@RestController
@RequestMapping("/multi")
public class MultiController {
@Resource
private ChatLanguageModel chatLanguageModel;
@GetMapping("/chat")
public String chat(@RequestParam("message") String message) throws IOException {
// 读取到图片
File file = new File("E:\\project\\IdeaProjects\\AI\\LangChain4j1.0\\Langchain4j\\mutil\\src\\main\\resources\\documents\\1.png");
// 把file转为一个字节数组
byte[] bytes = Files.readAllBytes(file.toPath());
UserMessage userMessage = UserMessage.userMessage(TextContent.from(message), // 文本消息
ImageContent.from(Base64.getEncoder().encodeToString(bytes), "image/png") // 图片信息
);
return chatLanguageModel.chat(userMessage).aiMessage().text();
}
}
这里注意,我们需要手动去封装一下UserMessage,把我们对话的用户文本和图片封装为一个UserMessage对象,然后再使用这个对象来向LLM发起请求对话
最终我们请求
java
http://localhost:8080/multi/chat?message=这个图片中所有数字的和是多少?
LLM返回结果
java
图片中的数字分别是 5、6、8、9 和 4。将这些数字相加:
\[ 5 + 6 + 8 + 9 + 4 = 32 \]
所以,这些数字的和是 32。