一、前言
1、本文介绍
-
本文介绍了
LangChain4j+SpringBoot实现大模型应用开发,通过一系列测试用例演示了核心功能(提示词、聊天记忆、函数调用、RAG等)的实现。最后通过搭建一个健康助手智能体将知识点全部串了起来。 -
你只需要一步步按照本文实操,即可快速熟悉所有知识点。
-
本文使用的开发工具:
IntelliJ IDEA Community Edition 2025.1 -
本文使用的LangChain4j版本:
1.0.0-beta3 -
本文使用
WSL+Docker来安装所需的数据库- 使用mongdb实现聊天记忆持久化,pgvector作为向量存储,mysql作为业务数据库
docker run --name mongo8 -d -p 27017:27017 mongo:8.0 docker run --name pgvector16 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=123456 -e POSTGRES_DB=ff_ai -p 5432:5432 -d pgvector/pgvector:pg16 docker run --name mysql8 -e MYSQL_ROOT_PASSWORD=123456 -d -p 3306:3306 mysql:8.0 -
copy代码测试过程中的注意点:
- api-key要替换成你自己的
- pom中每次引入或更换依赖后,确保成功
- auto import失败的情况,需要手动选择引入
- package需要你自己创建:"项目实战"章节的代码在
cn.freyfang.health目录及子目录下;其他章节的代码在“cn.freyfang.demo“目录及子目录下
-
遇到的挑战
- RAG:检索方式的选择、嵌入模型的选择、相似度阈值的设置、分片的Token大小以及重叠的大小、以及文档自身的特点,都会影响最终的检索效果
- Tool:模型进行选择时会出现幻觉,即使不断调整工具描述及参数描述,目前执行的结果也很难达到成熟使用的标准。
- 总之,需要大量的进行测试,修改设置后再调试,循环往复进行微调
2、LangChain4j介绍
- LangChain4j 于 2023 年初在 ChatGPT 热潮中启动开发。目标是简化 LLM 与 Java 应用程序的集成。
- 官网:https://docs.langchain4j.dev/
-
与 LLM 和 Vector Stores 轻松交互
- 所有主要的商业和开源 LLM 和 Vector Stores 都可以,从而让您能够构建聊天机器人、助手等。
-
专为 Java 量身定制
- 得益于 Quarkus 和 Spring Boot 的集成,LLM 可以顺利集成到您的 Java 应用程序中。LLM 与 Java 之间是双向集成:您可以从 Java 调用 LLM,并允许 LLM 调用您的 Java 代码。
-
Agents、Tools、RAG
- 丰富的工具箱为常见的 LLM 操作提供了广泛的工具,从低级提示模板、聊天内存管理和输出解析到Agent和 RAG 等高级模式。
-
- 支持的产品
- LangChain4j包结构
langchain4j-core模块定义核心抽象(例如ChatModel和EmbeddingStore)及其 API。langchain4j模块包含文档加载器、聊天记忆、AI 服务等高级功能。langchain4j-{integration},每个模块都支持与各种 LLM 提供程序集成。
二、快速开始
1、实现初次对话
-
创建SpringBoot项目
ff-ai-langchain4j,LangChain4j需要Java 17+和Spring Boot 3.2+
-
在
pox.xml中引入依赖<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- 指定版本 --> <spring-boot.version>3.3.11</spring-boot.version> <langchain4j.version>1.0.0-beta3</langchain4j.version> </properties> <dependencies> <!-- 1、引入SpringBoot及测试--> <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> </dependency> <!-- 2、引入langchain4j-core基础功能,默认提供对OpenAI标准的支持--> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</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> <repositories> <!-- 使用阿里云公共仓库作为下载源--> <repository> <id>aliyun-maven</id> <url>https://maven.aliyun.com/repository/public</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> -
创建启动类
AiLangChain4jApppackage cn.freyfang; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class AiLangChain4jApp { public static void main(String[] args) { System.out.println("ff-ai-langchain4j start......"); SpringApplication.run(AiLangChain4jApp.class); } } -
创建测试类
Test01_Basicpackage cn.freyfang; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ChatRequestParameters; import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.openai.OpenAiChatModelName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class Test01_Basic { /** * langchain4j 提供的测试配置: * apikey:demo * model:gpt-4o-mini * baseUrl:http://langchain4j.dev/demo/openai/v1 */ @Test public void testOpenAiDemo() { // 自己创建实例 OpenAiChatModel openAiChatModel = OpenAiChatModel .builder() .apiKey("demo") .baseUrl("http://langchain4j.dev/demo/openai/v1") .modelName(OpenAiChatModelName.GPT_4_O_MINI) .build(); String answer = openAiChatModel.chat("你是谁?"); System.out.println(answer);//我是一个人工智能助手,旨在回答你的问题并提供帮助。不管你想了解什么,尽管问我吧! // 通过chatRequest 指定温度、工具或 JSON 模式 ChatRequest chatRequest = ChatRequest.builder() .messages(UserMessage.from("我是谁")) .parameters(ChatRequestParameters.builder() .temperature(0.8) .build() ) .build(); ChatResponse chatResponse = openAiChatModel.chat(chatRequest); System.out.println(chatResponse.aiMessage());//AiMessage { text = "你是提问者,可能在寻找某种信息或对话。如果你愿意,可以分享更多关于你的信息或你想讨论的内容!" toolExecutionRequests = null } } }
2、与SpringBoot集成
-
在
pom.xml中替换依赖<!-- <dependency>--> <!-- <groupId>dev.langchain4j</groupId>--> <!-- <artifactId>langchain4j-open-ai</artifactId>--> <!-- </dependency>--> <!-- 替换为与SpringBoot集成的starter--> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId> </dependency> -
在resources下创建配置文件
application.ymllangchain4j: open-ai: chat-model: # langchain4j 提供的用于测试apiKey api-key: demo model-name: gpt-4o-mini base-url: http://langchain4j.dev/demo/openai/v1 log-requests: true #打印日志 log-responses: true # 需要开启debug logging: level: root: debug -
在
Test01_Basic中编写测试方法@Autowired public OpenAiChatModel openAiChatModel; /** * 测试与SpringBoot的集成 */ @Test public void testSpringBoot() { String answer = openAiChatModel.chat("我是张三,你是谁?"); /* 请求 { "model" : "gpt-4o-mini", "messages" : [ { "role" : "user", "content" : "我是张三,你是谁?" } ], "stream" : false } */ /* 响应 你好,张三!我是一款人工智能助手,旨在帮助你解答问题和提供信息。有需要帮助的吗? */ System.out.println(answer); }
3、接入阿里百炼
- 阿里云百炼平台官网:https://bailian.console.aliyun.com/,初次开通会赠送每个模型100万Token的使用额度,有效期半年。
- 目前平台支持200+模型的接入,包括通义系列、DeepSeek等
- 开通百炼平台后先创建一个api-key,用于代码测试
1)方式1:使用OpenAi兼容方式
-
修改
application.yml中的api-key、model-name、base-urllangchain4j: open-ai: chat-model: # langchain4j 提供的用于测试apiKey # api-key: demo # model-name: gpt-4o-mini # base-url: http://langchain4j.dev/demo/openai/v1 # 使用 OpenAI 兼容模式 访问 阿里千问模型,修改api-key,model-name,base-url api-key: sk-xxx model-name: qwen-max base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 log-requests: true #打印日志 log-responses: true -
重新运行
Test01_Basic中的方法testSpringBoot()/* 请求 { "model" : "qwen-max", "messages" : [ { "role" : "user", "content" : "我是张三,你是谁?" } ], "stream" : false } */ /* 响应 你好,张三!我是Qwen,是阿里云开发的一款超大规模语言模型。我被设计用来帮助用户生成各种类型的文本,如文章、故事、诗歌、故事等,并能够根据不同的场景和需求提供有用的信息和帮助。无论是解答问题、提供创意还是进行对话交流,我都在这里为你服务。有什么我可以帮到你的吗? */
2)方式2:使用百炼平台自己的方式
-
在
pom.xml中引入依赖<!-- 3、引入阿里百炼--> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId> </dependency> -
在
pom.xml中引入版本管理<!-- 引入百炼依赖版本清单--> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-community-bom</artifactId> <version>${langchain4j.version}</version> <type>pom</type> <scope>import</scope> </dependency> -
修改yml,注意这里将open-ai模式下的模型还原成
gpt-4o-mini是为了后续在容器中将同时使用openAiChatModel和qwenChatModellangchain4j: open-ai: chat-model: # langchain4j 提供的用于测试apiKey api-key: demo model-name: gpt-4o-mini base-url: http://langchain4j.dev/demo/openai/v1 # 使用 OpenAI 兼容模式 访问 阿里千问模型 # api-key: sk-xxx # model-name: qwen-max # base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 log-requests: true #打印日志 log-responses: true # 添加 阿里的千问模型,这种方式没有参数设置打印日志 community: dashscope: chat-model: api-key: sk-xxx model-name: qwen-plus-latest -
在
Test01_Basic中编写测试方法@Autowired public QwenChatModel qwenChatModel; /** * 测试接入百炼千问 */ @Test public void testQwenModel() { String answer = qwenChatModel.chat("我是张三,你是谁?"); System.out.println(answer);//你好,张三!我是通义千问,英文名Qwen,是阿里巴巴集团旗下的通义实验室自主研发的超大规模语言模型。我可以帮助你回答问题、创作文字、进行逻辑推理、编程等任务。有什么我可以帮你的吗? }
4)多模态支持
-
在
Test01_Basic中编写测试方法/** * 测试多模态:gpt-4o-mini图片理解 * @throws IOException */ @Test public void testOpenAiMultiModal() { UserMessage userMessage = UserMessage.from(TextContent.from("描述这张图片"), ImageContent.from("https://picx.zhimg.com/v2-e083258a3a0db84ae6f68f32c9d13a01_r.jpg?source=2c26e567")); ChatResponse chatResponse = openAiChatModel.chat(userMessage); /* 请求 { "model" : "gpt-4o-mini", "messages" : [ { "role" : "user", "content" : [ { "type" : "text", "text" : "描述这张图片" }, { "type" : "image_url", "image_url" : { "url" : "https://picx.zhimg.com/v2-e083258a3a0db84ae6f68f32c9d13a01_r.jpg?source=2c26e567", "detail" : "low" } } ] } ], "stream" : false } */ /* 响应 AiMessage { text = "这张图片展示了一个美丽的海滩日落场景。金色的阳光透过云层洒在海面上,形成闪耀的倒影。海浪轻轻拍打着沙滩,水面波光粼粼,营造出宁静而迷人的氛围。整体色调温暖,给人一种放松和宁静的感觉。" toolExecutionRequests = null } */ System.out.println(chatResponse.aiMessage()); } @Value("${langchain4j.community.dashscope.chat-model.api-key}") private String dashscopeKey; /** * 测试多模态:通义万相生成图片 */ @Test public void testWanx() { WanxImageModel wanxImageModel = WanxImageModel.builder() .modelName(WanxModelName.WANX2_1_T2I_PLUS) .apiKey(dashscopeKey) .build(); Response<Image> response = wanxImageModel.generate("我从未见过这样简陋的房子。它在路边的低洼处,用泥土和茅草盖成,只有一扇破旧的门。屋里很黑,只有一张床、一张桌子和几个椅子。闰土的父亲是一位竹匠,他正在编一个竹篮。闰土的母亲在灶台前烧饭。"); //返回生成图片的url System.out.println(response.content().url()); }
三、AI Service
-
将与 LLM 和其他组件交互的复杂性隐藏在简单的 API 背后。你声明式地定义一个包含所需 API 的接口,LangChain4j 会提供一个实现该接口的对象(代理)。你可以将 AI Service 视为应用程序中服务层的一个组件
-
支持常见的操作:格式化 LLM 的输入、解析 LLM 的输出,还支持更多高级功能:聊天记忆、Tool、RAG
-
在pom.xml中引入依赖
<!-- 4、引入 langchain4j高级功能,如AiService、记忆--> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> </dependency> -
创建AI Service接口(不使用注解):
Assistantpackage cn.freyfang.demo; public interface Assistant { String chat(String userMessage); } -
创建AI Service接口(使用注解):
Assistant2package cn.freyfang.demo; import dev.langchain4j.service.spring.AiService; import dev.langchain4j.service.spring.AiServiceWiringMode; /** * 它将使用应用程序上下文中所有可用的 LangChain4j 组件创建该接口的实现,并将其注册为 bean * 组件包括:ChatLanguageModel、StreamingChatLanguageModel、ChatMemory、ChatMemoryProvider。。。 * 如果您有多个 AI Service,并希望将不同的 LangChain4j 组件连接到每个服务,则可以使用显式连接模式 */ @AiService( // 因为容器中有2个chatModel,所以需要显示指定使用哪个 wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "qwenChatModel" ) public interface Assistant2 { String chat(String userMessage); } -
编写测试类
Test02_AIServicepackage cn.freyfang; import cn.freyfang.demo.Assistant; import cn.freyfang.demo.Assistant2; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.service.AiServices; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class Test02_AIService { @Autowired private OpenAiChatModel openAiChatModel; /** * 使用AI Service */ @Test public void testAiService() { Assistant assistant = AiServices.create(Assistant.class, openAiChatModel); String answer = assistant.chat("你是谁"); System.out.println(answer);//我是一个人工智能助手,旨在回答问题和提供信息。有什么我可以帮助你的吗? } @Autowired private Assistant2 assistant2; /** * SpringBoot中的AI Service * 使用@AiService 注解 */ @Test public void testSpringBootAiService() { String answer = assistant2.chat("你是谁"); System.out.println(answer);//我是通义千问,阿里巴巴集团旗下的通义实验室自主研发的超大规模语言模型。我可以帮助你回答问题、创作文字,比如写故事、写公文、写邮件、写剧本、逻辑推理、编程等等,还能表达观点,玩游戏等。如果你有任何问题或需要帮助,欢迎随时告诉我! } }
四、提示词
-
系统提示词
- 可以定义LLM 在对话中的角色、身份、能力、回答风格等说明,内容只会发送给大模型一次
- 通过注解
@SystemMessage定义
-
用户提示词
- 会每次和用户的输入一起发送给大模型
- 通过注解
@UserMessage定义
-
创建AI Service接口:
Assistant3package cn.freyfang.demo; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; import dev.langchain4j.service.spring.AiService; import dev.langchain4j.service.spring.AiServiceWiringMode; @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel" ) public interface Assistant3 { // 使用提示词模版变量 current_date @SystemMessage("你是股票专家。今天是{{current_date}}") String chatBySysPrompt(String userMessage); // 从文件中加载提示词 @SystemMessage(fromResource = "system-prompt-template.txt") String chatBySysPromptFromResource(String userMessage); //注意:1个方法只能有一个@UserMessage,可以修饰方法或参数 //{{it}}表示参数占位符,但前提是方法只能有1个参数。如果有多个参数请使用@V指定参数占位符 @UserMessage("请用四川话回答.{{message}}") String chatByUserPrompt(@V("message") String userMessage); // userMessage contains "{{yourName}}" template variable String chatByUserPrompt2(@UserMessage String userMessage, @V("yourName") String yourName); @UserMessage(fromResource = "user-prompt-template.txt") String chatByUserPromptFromResource(@V("message") String userMessage); @SystemMessage("你是股票领域的专家,且具有丰富的经济知识和{{year}}年炒股经验。") @UserMessage("请用东北话回答.{{message}}") String chatByPrompt(@V("message") String userMessage, @V("year") String yourYear); } -
在resources下创建提示词模版文件
-
system-prompt-template.txt你是健身教练,今天是{{current_date}} -
user-prompt-template.txt请用东北话回答,{{message}}
-
-
创建测试类
Test03_Promptpackage cn.freyfang; import cn.freyfang.demo.Assistant; import cn.freyfang.demo.Assistant3; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.service.AiServices; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class Test03_Prompt { @Autowired private OpenAiChatModel openAiChatModel; /** * 系统提示词1: * 可以通过程序动态定义系统提示词,可以实现根据MemoryId(用户或对话)提供不同的提示词 */ @Test public void testSystemPrompt1() { Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(openAiChatModel) .systemMessageProvider(memoryId -> "你是一名经验丰富的保险代理人") .build(); /* 请求 { "model" : "gpt-4o-mini", "messages" : [ { "role" : "system", "content" : "你是一名经验丰富的保险代理人" }, { "role" : "user", "content" : "你是谁" } ], "stream" : false } */ /* 响应 我是一名经验丰富的保险代理人,专注于帮助客户了解保险产品并找到适合他们需求的解决方案。如果你有关于保险的问题或者需要咨询,请随时问我! */ System.out.println(assistant.chat("你是谁")); } @Autowired private Assistant3 assistant3; /** * 系统提示词2:使用注解 @SystemMessage */ @Test public void testSystemPrompt2() { /* 请求 { "model" : "gpt-4o-mini", "messages" : [ { "role" : "system", "content" : "你是股票专家。今天是2025-05-13" }, { "role" : "user", "content" : "你是谁,今天是几号?" } ], "stream" : false } */ /* 响应 我是一个股票专家,今天是2025年5月13日。请问有什么关于股票投资的问题我可以帮助你解答吗? */ System.out.println(assistant3.chatBySysPrompt("你是谁,今天是几号?")); //从提示词模版文件加载提示词 System.out.println(assistant3.chatBySysPromptFromResource("我是谁,你是谁,今天几号?")); } /** * 用户提示词:使用注解@UserMessage */ @Test public void testUserPrompt() { /* 请求 { "model" : "gpt-4o-mini", "messages" : [ { "role" : "user", "content" : "请用四川话回答.你好" } ], "stream" : false } */ /* 响应 你好嘛!你过得好伐? */ System.out.println(assistant3.chatByUserPrompt("你好")); /* 请求 { "model" : "gpt-4o-mini", "messages" : [ { "role" : "user", "content" : "你是李四,你好" } ], "stream" : false } */ /* 响应 你好!很高兴与您交流。请问有什么我可以帮助您的吗? */ System.out.println(assistant3.chatByUserPrompt2("你是{{yourName}},你好", "李四")); /* 请求 { "model" : "gpt-4o-mini", "messages" : [ { "role" : "user", "content" : "请用东北话回答,你是谁?" } ], "stream" : false } */ /* 响应 俺是AI助手,你问我啥事儿啊? */ System.out.println(assistant3.chatByUserPromptFromResource("你是谁?")); } /** * 系统+用户 */ @Test public void testPrompt() { /* 请求 { "model" : "gpt-4o-mini", "messages" : [ { "role" : "system", "content" : "你是股票领域的专家,且具有丰富的经济知识和10年炒股经验。" }, { "role" : "user", "content" : "请用东北话回答.请介绍下你自己" } ], "stream" : false } */ /* 响应 哈哈,哎呀,我就是一个对股票特别感兴趣的专家呀!在这个股市里整整混了十年,见过不少风浪。经济知识也有点底子,老太太常说,了解经济才能懂得股票嘛。要是你有啥问题,尽管问,我给你讲讲! */ System.out.println(assistant3.chatByPrompt("请介绍下你自己", "10")); } }
五、聊天记忆
- 记忆与历史记录:历史记录是用户在 UI 中看到的内容,它代表了实际说过的内容。记忆会保存一些信息,这些信息会被呈现给LLM,使其表现得像是“记住”了对话一样。记忆与历史记录截然不同
1、实现记忆-耦合式
-
编写测试类
Test04_Memorypackage cn.freyfang; import cn.freyfang.demo.Assistant; import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.service.AiServices; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class Test04_Memory { @Autowired private OpenAiChatModel openAiChatModel; /** * 实现记忆方式1:通过使用chat(ChatMessage... messages)携带之前的历史问答记录实现 */ @Test public void testMemory1() { UserMessage userMessage1 = UserMessage.from("我是张三"); ChatResponse chatResponse1 = openAiChatModel.chat(userMessage1); AiMessage aiMessage1 = chatResponse1.aiMessage(); System.out.println(aiMessage1); // 此时无记忆 UserMessage userMessage2 = UserMessage.from("我是谁"); ChatResponse chatResponse2 = openAiChatModel.chat(userMessage2); AiMessage aiMessage2 = chatResponse2.aiMessage(); System.out.println(aiMessage2); // 通过携带之前的记录实现记忆 AiMessage aiMessage3 = openAiChatModel.chat(userMessage1, aiMessage1, userMessage2).aiMessage(); /* 请求 { "model" : "gpt-4o-mini", "messages" : [ { "role" : "user", "content" : "我是张三" }, { "role" : "assistant", "content" : "你好,张三!有什么我可以帮助你的吗?" }, { "role" : "user", "content" : "我是谁" } ], "stream" : false } */ /* 响应 AiMessage { text = "你是张三!如果你有其他想讨论的内容,或者需要帮助,随时告诉我!" toolExecutionRequests = null } */ System.out.println(aiMessage3); } /** * 实现记忆方式2:使用AISerive + 耦合式创建chatMemory * 对话时会自动把历史记录一起发送给LLM */ @Test public void testMemory2() { ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(100); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(openAiChatModel) .chatMemory(chatMemory) .build(); System.out.println(assistant.chat("我是张三")); /* 请求 { "model" : "gpt-4o-mini", "messages" : [ { "role" : "user", "content" : "我是张三" }, { "role" : "assistant", "content" : "你好,张三!有什么我可以帮助你的吗?" }, { "role" : "user", "content" : "我是谁" } ], "stream" : false } */ /* 响应 你是张三。如果你有其他想分享的信息或者有任何问题,欢迎告诉我! */ System.out.println(assistant.chat("我是谁")); } }
2、实现记忆-声明式
-
创建配置类
ChatMemoryConfigpackage cn.freyfang.demo; import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @Component public class ChatMemoryConfig { /** * 基于内存的记忆 * @return */ @Bean public ChatMemory chatMemoryInMemory() { return MessageWindowChatMemory.withMaxMessages(100); } } -
在
Assistant2中指定chatMemory实例@AiService( //... //注入chatMemory实例 chatMemory = "chatMemoryInMemory" ) public interface Assistant2 { String chat(String userMessage); } -
在
Test04_Memory中编写测试方法@Autowired private Assistant2 assistant2; /** * 实现记忆方式3:使用AiService + 声明式创建chatMemory * 配置ChatMemory实例,并自动注入给AiService */ @Test public void testMemory3() { System.out.println(assistant2.chat("我是李四"));//你好,李四!有什么我可以帮你的吗?😊 System.out.println(assistant2.chat("我是谁"));//你是李四。😊 }
3、实现记忆隔离
-
不同用户的会话或者同一用户的不同会话需要有不同的记忆,解决方法是使用
ChatMemoryProvider -
为了避免内存泄漏,清除不再需要的对话内存也很重要。为了使 AI Service内部使用的聊天内存可访问,定义Service的接口只需扩展
ChatMemoryAccess接口即可 -
在配置类
ChatMemoryConfig中增加方法/** * 隔离:通过 memoryId 区分不同用户或同一用户不同会话的记忆 */ @Bean public ChatMemoryProvider chatMemoryInMemoryProvider() { return memoryId -> MessageWindowChatMemory .builder() .id(memoryId) .maxMessages(100) .build(); } -
创建AI Service接口:
Assistant4package cn.freyfang.demo; import dev.langchain4j.service.ChatMemoryAccess; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.spring.AiService; import dev.langchain4j.service.spring.AiServiceWiringMode; @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "qwenChatModel", chatMemoryProvider = "chatMemoryInMemoryProvider" ) public interface Assistant4 extends ChatMemoryAccess { String chatByMemoryId(@MemoryId String MemoryId, @UserMessage String userMessage); } -
在
Test04_Memory中编写测试方法@Autowired private Assistant4 assistant4; /** * 实现记忆隔离: * 通过声明式创建chatMemoryProvider + @MemoryId 实现记忆及隔离 */ @Test public void testMemoryById() { System.out.println(assistant4.chatByMemoryId("1", "我是李四"));//你好,李四!有什么我可以帮你的吗?😊 System.out.println(assistant4.chatByMemoryId("1", "我是谁"));//你刚刚告诉我你是李四,所以我认为你是李四。如果你有其他身份或问题需要确认,请告诉我更多信息哦!😊 System.out.println(assistant4.chatByMemoryId("2", "我是谁"));//你是一个正在使用这个设备的人,正在与我进行对话。不过如果你是在问一个更深层次的哲学问题,那可能需要你自己去探索和思考了。我可以帮助你思考这个问题,但答案最终还是取决于你自己的认知和理解。你想要从哪个角度来探讨这个问题呢? // 查询记忆 ChatMemory chatMemory = assistant4.getChatMemory("1"); List<ChatMessage> messages = chatMemory.messages(); /* [ UserMessage { name = null contents = [TextContent { text = "我是李四" }] }, AiMessage { text = "你好,李四!有什么我可以帮你的吗?😊" toolExecutionRequests = null }, UserMessage { name = null contents = [TextContent { text = "我是谁" }] }, AiMessage { text = "你刚刚告诉我你是李四,所以我认为你是李四。如果你有其他身份或问题需要确认,请告诉我更多信息哦!😊" toolExecutionRequests = null } ] */ System.out.println(messages); // 清除记忆 boolean b = assistant4.evictChatMemory("1"); System.out.println(b);//true // 再查询则为null System.out.println(assistant4.getChatMemory("1") == null);//true }
4、实现记忆持久化
-
默认记忆存储在内存中,可以持久化到数据库中。同一个会话的所有消息可以单独存储(例如,每条消息一个记录/行/对象),也可以一起存储(所有消息一个记录/行/对象)。
-
这里选择mongodb(需要启动mongo容器),并且将一个会话的所有消息整体存储为一行
-
在
pom.xml中引入依赖<!-- 5、引入mongodb持久化记忆 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> -
在
application.yml中添加配置# mongodb 存储聊天记忆 spring: data: mongodb: uri: mongodb://localhost:27017/ff_ai_memory -
创建实体映射类
MongoChatMessagepackage cn.freyfang.demo; 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 //映射到chat_message Collection(表) @Document("chat_message") public class MongoChatMessage { // 表id @Id private ObjectId id; // 对话ID,用于隔离记忆 private String memoryId; // 聊天内容 json str private String message; public MongoChatMessage(String message) { this.message = message; } } -
创建
MongoChatMemoryStore实现ChatMemoryStore接口及方法package cn.freyfang.demo; 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.List; @Component public class MongoChatMemoryStore implements ChatMemoryStore { @Autowired private MongoTemplate mongoTemplate; @Override public List<ChatMessage> getMessages(Object memoryId) { MongoChatMessage mongoChatMessage = mongoTemplate.findOne(new Query(Criteria.where("memoryId").is(memoryId)), MongoChatMessage.class); if (mongoChatMessage == null) return List.of(); return ChatMessageDeserializer.messagesFromJson(mongoChatMessage.getMessage()); } @Override public void updateMessages(Object memoryId, List<ChatMessage> chatMessages) { //更新或新增 mongoTemplate.upsert(new Query(Criteria.where("memoryId").is(memoryId)), new Update().set("message", ChatMessageSerializer.messagesToJson(chatMessages)), MongoChatMessage.class); } @Override public void deleteMessages(Object memoryId) { mongoTemplate.remove(new Query(Criteria.where("memoryId").is(memoryId)), MongoChatMessage.class); } } -
在配置类
ChatMemoryConfig中增加方法@Autowired private MongoChatMemoryStore mongoChatMemoryStore; /** * 隔离+持久化 */ @Bean public ChatMemoryProvider chatMemoryMongoProvider() { return memoryId -> MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(100) .chatMemoryStore(mongoChatMemoryStore) .build(); } -
在
Assistant4中替换chatMemoryProvider@AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "qwenChatModel", // chatMemoryProvider = "chatMemoryInMemoryProvider" chatMemoryProvider = "chatMemoryMongoProvider" ) public interface Assistant4 extends ChatMemoryAccess { String chatByMemoryId(@MemoryId String MemoryId, @UserMessage String userMessage); } -
在
Test04_Memory中编写测试方法/** * 实现记忆持久化 * 通过声明式创建chatMemoryProvider + chatMemoryStore(mongodb) */ @Test public void testMemoryMongo() { System.out.println(assistant4.chatByMemoryId("1", "我是李四"));//你好,李四!有什么我可以帮你的吗?😊 System.out.println(assistant4.chatByMemoryId("1", "我是谁"));//你刚刚告诉我你是李四,所以我认为你是李四。如果你有其他身份或者需要帮助的地方,请告诉我!😊 } -
使用mongodb官方软件Compass查看数据表记录,发现已生成了一条memoryId=1的记录,message字段值如下
[ { "contents": [ { "text": "我是李四", "type": "TEXT" } ], "type": "USER" }, { "text": "你好,李四!有什么我可以帮你的吗?😊", "type": "AI" }, { "contents": [ { "text": "我是谁", "type": "TEXT" } ], "type": "USER" }, { "text": "你刚刚告诉我你是李四,所以我认为你是李四。如果你有其他身份或者需要帮助的地方,请告诉我!😊", "type": "AI" } ]
六、Tool (Function Calling)
-
有一个概念叫做“工具”或“函数调用”。它允许 LLM 在必要时调用一个或多个可用工具,这些工具通常由开发人员定义。工具可以是任何东西:网页搜索、外部 API 调用、特定代码的执行等等。LLM 实际上无法自行调用该工具;相反,它们会在响应中表达调用特定工具的意图(而不是以纯文本形式响应)
-
LLM 在调用工具时可能会出现幻觉,为了增加 LLM 使用正确参数调用正确工具的机会,我们应该提供清晰明确的:
- 工具名称、描述该工具的功能以及何时使用:通过
@Tool指定 - 每个工具参数的描述:通过
@P指定
- 工具名称、描述该工具的功能以及何时使用:通过
-
由于LLM不擅长数学计算,我们来为 LLM 提供一个“数学工具”
-
创建工具类
MathToolpackage cn.freyfang.demo; import dev.langchain4j.agent.tool.P; import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agent.tool.ToolMemoryId; import org.springframework.stereotype.Component; @Component public class MathTool { /** * `@Tool`修饰的方法: * 可以是静态的,也可以是非静态的. * 可以具有任何可见性(公共、私人等 */ @Tool("计算两个数的和") double sum(double a, double b) { System.out.println("sum..." + a + "," + b); return a + b; } @Tool("计算给定值的平方根") double squareRoot(double x) { System.out.println("squareRoot..." + x); return Math.sqrt(x); } /** * `@Tool`注解有 2 个可选字段: * - `name`:工具的名称。如果未提供,则方法名称将作为工具的名称。 * - `value`:工具的描述。 * `@ToolMemoryId`: * AI Service方法的值将自动传递给该方法。如果您有多个用户或每个用户有多个聊天/回忆,并且希望在方法内部区分它们 * `@P`注释有 2 个字段 * - `value`:参数描述。必填字段。 * - `required`:是否为必填项,默认为`true`,可选字段。 */ @Tool(name = "判断用户是否存在") public String checkUserExists(@ToolMemoryId String memoryId, @P(value = "用户名", required = true) String username, @P(value = "年龄", required = false) String age) { System.out.println("checkUserExists..." + memoryId + "," + username + "," + age); if (username.equals("张三")) return "用户已存在"; else return "用户不存在"; } } -
创建AI Service接口:
Assistant5package cn.freyfang.demo; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.spring.AiService; import dev.langchain4j.service.spring.AiServiceWiringMode; @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "qwenChatModel", chatMemoryProvider = "chatMemoryMongoProvider", tools = {"mathTool"} //指定工具类 ) public interface Assistant5 { String chat(@MemoryId String memoryId, @UserMessage String message); } -
创建测试类
Test05_Toolpackage cn.freyfang; import cn.freyfang.demo.Assistant; import cn.freyfang.demo.Assistant5; import cn.freyfang.demo.MathTool; import dev.langchain4j.community.model.dashscope.QwenChatModel; import dev.langchain4j.service.AiServices; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class Test05_Tool { @Autowired private QwenChatModel qwenChatModel; @Autowired private MathTool mathTool; /** * 编程式 */ @Test public void testTool() { Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(qwenChatModel) .tools(mathTool) .build(); //查看日志发现,调用了sum方法 System.out.println(assistant.chat("1.333加上2.00001等于几?")); } @Autowired private Assistant5 assistant5; /** * Ai Service 方式 */ @Test public void testTool2() { //查看日志发现,调用了squareRoot方法 System.out.println(assistant5.chat("2","对100开平方根等于几?"));//100 的平方根是 10.0。 //张三和李四分别调用了checkUserExists方法 System.out.println(assistant5.chat("2", "张三是否存在?李四是否存在?"));//张三存在,而李四不存在。 } } -
查看mongodb中持久化记录
[ { "contents": [ { "text": "对100开平方根等于几?", "type": "TEXT" } ], "type": "USER" }, { "toolExecutionRequests": [ { "id": "call_a8bfa3900b9944d1bfa6b5", "name": "squareRoot", "arguments": "{\"arg0\": 100}" } ], "type": "AI" }, { "id": "call_a8bfa3900b9944d1bfa6b5", "toolName": "squareRoot", "text": "10.0", "type": "TOOL_EXECUTION_RESULT" }, { "text": "100 的平方根是 10.0。", "type": "AI" }, { "contents": [ { "text": "张三是否存在?李四是否存在?", "type": "TEXT" } ], "type": "USER" }, { "toolExecutionRequests": [ { "id": "call_a938367ee9d449d3964e16", "name": "判断用户是否存在", "arguments": "{\"arg1\": \"张三\"}" } ], "type": "AI" }, { "id": "call_a938367ee9d449d3964e16", "toolName": "判断用户是否存在", "text": "用户已存在", "type": "TOOL_EXECUTION_RESULT" }, { "toolExecutionRequests": [ { "id": "call_6953a044fe9a496eaa35ff", "name": "判断用户是否存在", "arguments": "{\"arg1\": \"李四\"}" } ], "type": "AI" }, { "id": "call_6953a044fe9a496eaa35ff", "toolName": "判断用户是否存在", "text": "用户不存在", "type": "TOOL_EXECUTION_RESULT" }, { "text": "张三存在,而李四不存在。", "type": "AI" } ]
七、RAG
-
LLM的知识仅限于它所训练的数据。 如果您想使LLM了解特定领域的知识或专有数据,您可以:
使用RAGRetrieval-Augmented Generation检索增强生成,即先将非结构化特定数据分片向量化后作为知识库,用户每次查询问题时,先查询知识库,再将查询结果和问题一起发送给大模型,大模型根据这些信息进行回复
- 用您的数据微调LLM
- 即使用特定数据再次训练大模型
- 将 RAG 和微调相结合
-
常用的检索方法有:
- 全文(关键词)搜索。此方法使用 TF-IDF 和 BM25 等技术,通过将查询中的关键词(例如,用户正在询问的内容)与文档数据库进行匹配来搜索文档。它根据这些关键词在每个文档中出现的频率和相关性对结果进行排序。
向量搜索,也称为“语义搜索”。使用嵌入模型将文本文档转换为数字向量。然后,它根据查询向量和文档向量之间的余弦相似度或其他相似度/距离度量来查找和排序文档,从而捕捉更深层的语义。- 混合。结合多种搜索方法(例如,全文+向量)通常可以提高搜索的有效性。
-
LangChain4j 提供三种 RAG:
Easy RAG:开始使用 RAG 的最简单方法- Naive RAG:使用向量搜索的 RAG 基本实现
- Advanced RAG:模块化 RAG 框架,允许执行其他步骤,例如查询转换、从多个来源检索和重新排序
-
RAG 流程分为两个不同的阶段:索引和检索
1、索引阶段
-
加载并解析文档(加载器和解析器)=》将文档分片(分割器)=》将片段向量化(嵌入模型)=》再将向量和原始片段一起存入向量数据库
-
设置分片大小和重叠大小的原则
- 考虑大模型支持的上下文大小,即每片大小(token数)要远小于大模型的限制
- 考虑数据特点和检索需求,如每个文本段落的大小,检索的粒度大小
- 考虑每个片段之间的重叠大小,保持片段连贯性的同时也要考虑冗余度,避免浪费
-
嵌入(向量)模型
- 向量:指具有大小和方向的量。它可以形象化地表示为带箭头的线段。箭头所指:代表向量的方向;线段长度:代表向量的大小
- 维度:如二维向量(1,3),三维向量(1,3,-3)等,嵌入模型支持的维度越多,对事务的描述越清晰
- Token:是模型用来表示自然语言文本的的最小单位,可以是一个词、一个数字或一个标点符号等
2、检索阶段
-
将查询文本向量化(嵌入模型)=》根据查询向量检索向量数据库,使用相似度算法查询到相关文本片段=》将原始问题和查询到的文档片段交给大模型
-
向量检索
- 相似度:如用户输入”运动“,向量搜索返回”足球“而不是”天气“,这就是相似度搜索
- 得分:根据某种算法计算查询文本的向量和知识库存储的文档片段向量之间的相似程度,越相似则得分越高。设置得分阈值可对结果进行过滤,即小于阈值的结果将被舍弃
-
设置得分阈值的原则:
- 考虑召回率和准确率
- 如果得分阈值设置越高,返回结果会变少,召回率会变低,但准确率会提高
- 如果得分阈值设置越低,返回结果会变多,召回率会变高,但准确率会减小
3、测试
-
需要启动pgvector容器
-
在
applicaiton.yml中添加嵌入模型langchain4j: open-ai: #... community: dashscope: chat-model: api-key: sk-xxx model-name: qwen-plus-latest # 指定向量模型 embedding-model: api-key: sk-xxx model-name: text-embedding-v3 -
在
pom.xml中引入相关依赖<!-- 6、引入RAG依赖:easy-rag --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-easy-rag</artifactId> </dependency> <!-- 7、引入文档解析器 --> <!-- 可以解析 PDF 文件--> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId> </dependency> <!-- 可以解析 MS Office 文件格式(例如 DOC、DOCX、PPT、PPTX、XLS、XLSX 等)--> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-document-parser-apache-poi</artifactId> </dependency> <!-- 8、引入pgvector向量存储--> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-pgvector</artifactId> </dependency> -
创建配置类
RagConfigpackage cn.freyfang.demo; import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.rag.content.retriever.ContentRetriever; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RagConfig { //注入阿里text-embedding-v3向量模型 @Autowired private QwenEmbeddingModel qwenEmbeddingModel; /** * 创建PgVector向量存储 */ @Bean public EmbeddingStore<TextSegment> embeddingStore() { return PgVectorEmbeddingStore.builder() .host("localhost") .port(5432) .database("ff_ai") .user("root") .password("123456") .table("test") .dimension(qwenEmbeddingModel.dimension()) //指定索引阶段的向量维度与 embeddedModel 生成的向量维度相同。 .build(); } /** * 创建一个基于pgvector向量存储的检索对象, 用于从嵌入存储中检索内容 */ @Bean public ContentRetriever contentRetrieverPgVector(EmbeddingStore embeddingStore) { //构造注入,解决依赖关系 return EmbeddingStoreContentRetriever .builder() .embeddingModel(qwenEmbeddingModel) //向量模型 .embeddingStore(embeddingStore) //向量存储 .maxResults(10) //设置最大检索结果数量, 这里表示最多返回 1 条匹配结果 .minScore(0.75) // 设置最小得分阈值, 只有得分大于等于 0.75 的结果才会被返回 .build(); } } -
创建AI Service接口:
Assistant6package cn.freyfang.demo; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.spring.AiService; import dev.langchain4j.service.spring.AiServiceWiringMode; @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "qwenChatModel", chatMemoryProvider = "chatMemoryMongoProvider", contentRetriever = "contentRetrieverPgVector" ) public interface Assistant6 { String chat(@MemoryId String memoryId, @UserMessage String message); } -
创建测试类
Test06_RAGpackage cn.freyfang; import dev.langchain4j.community.model.dashscope.QwenModelName; import dev.langchain4j.community.model.dashscope.QwenTokenizer; import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.loader.FileSystemDocumentLoader; import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser; import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.embedding.onnx.HuggingFaceTokenizer; import dev.langchain4j.model.openai.OpenAiTokenizer; import dev.langchain4j.store.embedding.EmbeddingMatch; import dev.langchain4j.store.embedding.EmbeddingSearchRequest; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import java.nio.file.FileSystems; import java.nio.file.PathMatcher; import java.util.List; @SpringBootTest public class Test06_RAG { /** * 测试 文档加载器 * FileSystemDocumentLoader 来自langchain4j模块 * ClassPathDocumentLoader 来自langchain4j模块 * UrlDocumentLoader 来自langchain4j模块 * ... */ @Test public void testFileSystemDocumentLoader() { // 1、加载单个文档 Document document = FileSystemDocumentLoader.loadDocument("D:/docs/readme.txt"); System.out.println(document.text()); // 2、加载目录下所有文档(注意:默认使用的是TextDocumentParser,所以如果目录下有非文本类型文件,则会报错) // List<Document> documents = FileSystemDocumentLoader.loadDocuments("D:/docs"); // 包括子目录 // List<Document> documents = FileSystemDocumentLoader.loadDocumentsRecursively("D:/docs"); // 限定文件格式 PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:*.txt"); List<Document> documents = FileSystemDocumentLoader.loadDocuments("D:/docs", pathMatcher); System.out.println(documents); } /** * 测试 文档解析器,默认使用TextDocumentParser * TextDocumentParser来自langchain4j模块,可以解析纯文本格式的文件(例如TXT,HTML,MD等) * ApachePdfBoxDocumentParser来自langchain4j-document-parser-apache-pdfbox可以解析 PDF 文件的模块 * ApachePoiDocumentParser来自langchain4j-document-parser-apache-poi模块,可以解析 MS Office 文件格式(例如 DOC、DOCX、PPT、PPTX、XLS、XLSX 等) * ApacheTikaDocumentParser来自langchain4j-document-parser-apache-tika模块,它可以自动检测和解析几乎所有现有的文件格式 */ @Test public void testDocumentParser() { ApachePdfBoxDocumentParser apachePdfBoxDocumentParser = new ApachePdfBoxDocumentParser(); Document document = FileSystemDocumentLoader.loadDocument("D:/docs/编程指南.pdf", apachePdfBoxDocumentParser); System.out.println(document.text()); } /** * 测试 完整索引阶段: * 1、加载(文档加载器) * 2、解析pdf(文档解析器) * 3、分片(文档分割器) * 4、文本向量化(基于默认向量模型) * 5、向量存储(基于内存) */ @Test public void testDocumentSplitter() { // 1、加载+解析 ApachePdfBoxDocumentParser apachePdfBoxDocumentParser = new ApachePdfBoxDocumentParser(); Document document = FileSystemDocumentLoader.loadDocument("D:/docs/编程指南.pdf", apachePdfBoxDocumentParser); // System.out.println(document.text()); InMemoryEmbeddingStore<TextSegment> inMemoryEmbeddingStore = new InMemoryEmbeddingStore<>(); // 2、分割(默认使用递归分割器)+向量化(默认使用内置的简单嵌入模型),并将文本和向量存入向量存储库 EmbeddingStoreIngestor .builder() //基于内存的向量存储 .embeddingStore(inMemoryEmbeddingStore) //自定义分割器:按段落分割文档: 每个片段包含不超过 500个token, 并且有 30个token的重叠部分保证连贯性 .documentSplitter(new DocumentByParagraphSplitter( 500, 30, new HuggingFaceTokenizer())) .build() .ingest(document); // 3、查看结果 /* { "entries": [ { "id": "f7fd34a7-26d8-4d58-b327-0446ea7fea3d", "embedding": { "vector": [ -0.019628847, 0.0308202, // ... ] }, "embedded": { "text": "这里是pdf的内容,大约500个字符。。。", "metadata": { "metadata": { "absolute_directory_path": "D:\\docs", "index": "0", "file_name": "编程指南.pdf" } } } }, { "id": "0dde10fa-d5dd-4e83-a04a-37e398b16961", "embedding": { "vector": [ -0.017716374, // ... ] }, "embedded": { "text": "这里是pdf的内容,大约500个字符。。。", "metadata": { "metadata": { "absolute_directory_path": "D:\\docs", "index": "1", "file_name": "编程指南.pdf" } } } }, // ... ] } */ System.out.println(inMemoryEmbeddingStore.serializeToJson()); } @Value("${langchain4j.community.dashscope.chat-model.api-key}") private String dashscopeKey; /** * 测试: 不同向量模型对Token数量的计算 */ @Test public void testCountToken() { String text = "testCountToken你好啊"; OpenAiTokenizer openAiTokenizer = new OpenAiTokenizer("gpt-4o-mini"); System.out.println(openAiTokenizer.estimateTokenCountInText(text));//5 QwenTokenizer qwenTokenizer = new QwenTokenizer(dashscopeKey, QwenModelName.QWEN_MAX); System.out.println(qwenTokenizer.estimateTokenCountInText(text));//5 } //因为整个容器中我们只初始化了qwenEmbeddingModel,所以这里使用EmbeddingModel不会冲突 @Autowired private EmbeddingModel embeddingModel; @Autowired private EmbeddingStore embeddingStore; /** * 测试:RAG * 索引阶段:文本向量化+向量存储(基于PgVectorEmbeddingStore) * 检索阶段: 查询文本向量化+向量搜索 */ @Test public void testRAG() { // 1、文本向量化 TextSegment segment1 = TextSegment.from("我喜欢足球"); Embedding embedding1 = embeddingModel.embed(segment1).content(); // 2、存储向量 embeddingStore.add(embedding1, segment1); TextSegment segment2 = TextSegment.from("今天天气真好"); Embedding embedding2 = embeddingModel.embed(segment2).content(); embeddingStore.add(embedding2, segment2); // 3、查询文本向量化 Embedding queryEmbedding = embeddingModel.embed("你最喜欢的运动是什么?").content(); // 4、向量搜索 EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .maxResults(1) //匹配最相似的一条 .minScore(0.75) //相似度得分,用于过滤。低于这个阈值的结果将被舍弃 .build(); // 相似度搜索, 获取搜索结果中的匹配项列表 List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(embeddingSearchRequest).matches(); EmbeddingMatch<TextSegment> embeddingMatch = relevant.get(0); // 相似度得分 System.out.println(embeddingMatch.score()); // 0.8440947830677032 System.out.println(embeddingMatch.embedded().text()); // 我喜欢足球 // 查看pgvector数据库test表,发现已将两个文本向量化 } @Autowired private Assistant6 assistant6; /** * 测试聊天中使用RAG */ @Test public void testRAG2() { /* 查看memoryId=3的记忆,“Answer using the following information”即RAG的结果 [ { "contents": [ { "text": "你喜欢什么运动?\n\nAnswer using the following information:\n我喜欢足球", "type": "TEXT" } ], "type": "USER" }, { "text": "我喜欢足球。", "type": "AI" } ] */ System.out.println(assistant6.chat("3", "你喜欢什么运动?")); //我喜欢足球 } }
八、底层关键组件
1、接口 ChatLanguageModel
-
实现类 OpenAiChatModel、QwenChatModel
-
查看4个常用默认方法
- 发现底层都是将用户消息封装成了ChatRequest对象,通过该对象可以设置参数,如温度、模型名等
- 都是将AI回答封装成了ChatResponse对象,其中AiMessage属性是回答的内容
default String chat(String userMessage) { ChatRequest chatRequest = ChatRequest.builder().messages(new ChatMessage[]{UserMessage.from(userMessage)}).build(); ChatResponse chatResponse = this.chat(chatRequest); return chatResponse.aiMessage().text(); } default ChatResponse chat(ChatMessage... messages) { ChatRequest chatRequest = ChatRequest.builder().messages(messages).build(); return this.chat(chatRequest); } default ChatResponse chat(List<ChatMessage> messages) { ChatRequest chatRequest = ChatRequest.builder().messages(messages).build(); return this.chat(chatRequest); } default ChatResponse chat(ChatRequest chatRequest) { ChatRequest finalChatRequest = ChatRequest.builder().messages(chatRequest.messages()).parameters(this.defaultRequestParameters().overrideWith(chatRequest.parameters())).build(); List<ChatModelListener> listeners = this.listeners(); Map<Object, Object> attributes = new ConcurrentHashMap(); ListenersUtil.onRequest(finalChatRequest, this.provider(), attributes, listeners); try { ChatResponse chatResponse = this.doChat(finalChatRequest); ListenersUtil.onResponse(chatResponse, finalChatRequest, this.provider(), attributes, listeners); return chatResponse; } catch (Exception error) { ListenersUtil.onError(error, finalChatRequest, this.provider(), attributes, listeners); throw error; } }
2、接口 ChatMessage
- 实现类 UserMessage:用于封装用户发送的消息
- 实现类 SystemMessage:用于封装系统提示词
- 实现类 AiMessage:大模型的回答
- 实现类 ToolExecutionResultMessage:函数调用的结果
3、接口 ChatMemory
public interface ChatMemory {
Object id();
void add(ChatMessage var1);
List<ChatMessage> messages();
void clear();
}
- 实现类 MessageWindowChatMemory
- 保留N条最新消息,并丢弃不再合适的旧消息,每条消息可以包含不同数量的Token
- 实现类 TokenWindowChatMemory
- 保留N最新的token,并根据需要逐出较旧的消息。消息是不可分割的。如果一条消息不适合,它将被完全逐出
4、接口 ChatMemoryStore
public interface ChatMemoryStore {
List<ChatMessage> getMessages(Object var1);
void updateMessages(Object var1, List<ChatMessage> var2);
void deleteMessages(Object var1);
}
九、项目实战
1、项目介绍
- 创建健康助手智能体,支持上传体检报告到知识库,智能体基于知识库回答用户的健康问题并提供建议。智能体具有用户健康问题管理功能(查询、保存、删除)
2、环境准备
-
在
pom.xml中引入依赖<properties> <!-- 增加版本 --> <mybatis-plus.version>3.5.11</mybatis-plus.version> <knife4j.version>4.3.0</knife4j.version> </properties> <dependencies> <!-- 9、引入mybatis-plus和mysql --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <!-- 10、引入流式输出--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-reactor</artifactId> </dependency> <!-- 11、引入接口测试--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> <version>${knife4j.version}</version> </dependency> </dependencies> -
在
application.yml中设置流式模型和mysql数据源langchain4j: #。。。 community: dashscope: #。。。 # 流式模型 streaming-chat-model: api-key: sk-xxx model-name: qwen-max spring: # 。。。 # 设置mysql数据源 datasource: url: jdbc:mysql://localhost:3306/ff_ai?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver -
创建mysql数据库及业务表
- 需要启动mysql容器
create database ff_ai default CHARACTER set utf8; use ff_ai; CREATE TABLE health ( id BIGINT NOT NULL primary key auto_increment, username VARCHAR(100) NOT NULL, date VARCHAR(10) NOT NULL, problem VARCHAR(100) NOT NULL, detail VARCHAR(1000) NOT NULL ); -
创建实体类
Healthpackage cn.freyfang.health.bean; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; @Data @ToString @AllArgsConstructor @NoArgsConstructor public class Health { @TableId(type = IdType.AUTO) private Long id; private String username; private String date; private String problem; private String detail; } -
在
resources/knowledge目录下存放”体检报告.pdf“,后续通过用户发送http请求来解析该文档,模拟创建知识库及上传文档
3、搭建智能体
-
在resources下创建系统提示词模版
system-prompt-health.txt# 角色 你叫苏大强,是健康领域的专家。 你已经充分掌握了用户上传的体检报告知识库,可以回复用户的关于这方面的问题,并且可以根据自身经验提供一些专业健康建议。今天是 {{current_date}} ## 回答主题简介 分析用户的体检报告,协助用户记录用户的健康问题,并给出健康建议。 ## 工作流程 ### 步骤一:问题理解与回复分析 1. 认真理解从知识库中召回的内容和用户输入的问题,判断召回的内容是否是用户问题的答案。 2. 如果你不能理解用户的问题,例如用户的问题太简单、不包含必要信息,此时你需要追问用户,直到你确定已理解了用户的问题和需求。 ### 步骤二:回答用户问题 1. 经过你认真的判断后,确定用户的问题和回答主题完全无关,你应该拒绝回答。 2. 如果知识库中没有召回任何内容,你的话术可以参考“对不起,我已经学习的知识中不包含问题相关内容,暂时无法提供答案。如果你有回答主题相关的其他问题,我会尝试帮助你解答。 3. 如果召回的内容与用户问题有关,你应该只提取知识库中和问题提问相关的部分,整理并总结、整合并优化从知识库中召回的内容。你提供给用户的答案必须是精确且简洁的,无需注明答案的数据来源。 ### 步骤三:协助用户查询、保存、删除用户提出的健康问题,请不要提供默认参数 在查询用户健康问题前,你必须确保自己知晓用户的姓名(必选)、出现健康问题的日期(可选,格式举例: 2025-04-28),请不要提供默认日期。 在保存或删除用户的健康问题前, 你必须确保自己知晓用户的姓名(必选)、出现健康问题的日期(必选,格式举例: 2025-04-28)、健康问题描述(必选)、健康问题详细描述(可选)。 ## 限制 1. 禁止回答的问题 对于这些禁止回答的问题,你可以根据用户问题想一个合适的话术。 - 非主题相关问题:比如股票、旅游等与你需要聚焦回答的主题无关的问题。 - 违法、违规内容:包括但不限于政治敏感话题、色情、暴力、赌博、侵权等违反法律法规和道德伦理的内容 3. 风格:你必须确保你的回答准确无误、并且言简意赅、容易理解。你必须进行专业和确定性的回复。 4. 语言:你应该用与用户输入相同的语言回答。 ## 问答示例 ### 示例1 正常问答 用户问题:我的体检报告中有哪些异常? 你的答案:您好,查询到您在某年某月上传的体检报告,发现您的矫正视力偏低。建议您平时科学用眼,合理休息,并及时更换合适度数的眼镜。 ### 示例2 用户意图不明确 用户问题:最近身体不舒服 你的答案:你想了解关于身体健康的哪些信息呢?请详细描述你的问题,以便于我可以更好的帮助你。 -
创建配置类
HealthAgentConfig- RagConfig中contentRetrieverPgVector(EmbeddingStore embeddingStore)方法参数需要添加@Qualifier(“embeddingStore”)
package cn.freyfang.health.config; import cn.freyfang.demo.MongoChatMemoryStore; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.memory.chat.ChatMemoryProvider; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.rag.content.retriever.ContentRetriever; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class HealthAgentConfig { @Autowired private MongoChatMemoryStore mongoChatMemoryStore; /** * 基于mongodb的记忆持久化 * * @return */ @Bean public ChatMemoryProvider chatMemoryProviderHealth() { return memoryId -> MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(100) .chatMemoryStore(mongoChatMemoryStore) .build(); } @Autowired private EmbeddingModel embeddingModel; /** * 创建PgVector向量存储对象 */ @Bean public EmbeddingStore<TextSegment> embeddingStoreHealth() { return PgVectorEmbeddingStore.builder() .host("localhost") .port(5432) .database("ff_ai") .user("root") .password("123456") .table("health") .dimension(embeddingModel.dimension()) //指定索引的向量维度, 该维度与 embeddedModel 生成的向量维度相同。 .build(); } /** * 创建一个基于pgvector向量存储的检索对象, 用于从嵌入存储中检索内容 * 注意:由于之前测试RAG功能时已经在RagConfig类中注册了一个embeddingStore,该类中又注册了一个。所以注入时要使用@Qualifier明确。 * 同理,RagConfig中contentRetrieverPgVector(EmbeddingStore embeddingStore)方法参数需要添加@Qualifier("embeddingStore"); Test06_RAG测试类中注入EmbeddingStore时也要使用@Qualifier("embeddingStore") */ @Bean public ContentRetriever contentRetrieverPgVectorHealth(@Qualifier("embeddingStoreHealth") EmbeddingStore embeddingStore) { //构造注入,解决依赖关系 return EmbeddingStoreContentRetriever .builder() .embeddingModel(embeddingModel) //向量模型 .embeddingStore(embeddingStore) //向量存储 .maxResults(1) //设置最大检索结果数量, 这里表示最多返回 1 条匹配结果 .minScore(0.7) // 设置最小得分阈值, 只有得分大于等于 0.7 的结果才会被返回 .build(); } } -
创建工具
HealthToolpackage cn.freyfang.health.tool; import cn.freyfang.health.bean.Health; import cn.freyfang.health.service.HealthService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import dev.langchain4j.agent.tool.P; import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agent.tool.ToolMemoryId; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.List; @Component public class HealthTool { @Autowired private HealthService healthService; @Tool(name = "查询用户所有的健康记录", value = "根据参数用户名和日期查询用户的健康记录,并以列表形式返回给用户") public List<Health> findAllHealthRecord(@ToolMemoryId String memoryId, @P("用户名") String username, @P(value = "日期", required = false) String date) { System.out.println("---------------------findAllHealthRecord()" + memoryId + username + date); // 查询用户在某天的记录 List<Health> healthList = healthService.list(new LambdaQueryWrapper<Health>() .eq(Health::getUsername, username) .eq(date != null && !date.isEmpty(), Health::getDate, date) ); System.out.println(healthList); return healthList; } @Tool(name = "保存用户的健康问题", value = "根据参数,先让用户确认所有信息,用户确认后再进行保存。") public String saveHealthProblem(@ToolMemoryId String memoryId, Health health) { System.out.println("---------------------saveHealthProblem()" + memoryId + health); if (healthService.save(health)) { return "保存成功,并返回详情"; } else { return "保存失败"; } } @Tool(name = "删除用户的健康记录", value = "根据参数,先让用户确认所有信息,用户确认后再进行删除。") public String removeHealthRecords(@ToolMemoryId String memoryId, @P("用户名") String username, @P(value = "日期", required = false) String date) { System.out.println("---------------------removeHealthRecords()" + memoryId + username + date); boolean result = healthService.remove(new LambdaQueryWrapper<Health>() .eq(Health::getUsername, username) .eq(date != null && !date.isEmpty(), Health::getDate, date) ); if (result) { return "删除成功"; } else { return "删除失败"; } } } -
创建智能体
HealthAgentpackage cn.freyfang.health.assistant; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.spring.AiService; import dev.langchain4j.service.spring.AiServiceWiringMode; import reactor.core.publisher.Flux; //智能体:记忆隔离+持久化、提示词、Function Calling、RAG @AiService(wiringMode = AiServiceWiringMode.EXPLICIT, // 流式输出 streamingChatModel = "qwenStreamingChatModel", // 记忆隔离和持久化 chatMemoryProvider = "chatMemoryProviderHealth", // Function Calling tools = "healthTool", // RAG contentRetriever = "contentRetrieverPgVectorHealth" ) public interface HealthAgent { @SystemMessage(fromResource = "/system-prompt-health.txt") Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage); }
4、搭建MVC
-
创建Service层
package cn.freyfang.health.service; import cn.freyfang.health.bean.Health; import com.baomidou.mybatisplus.extension.service.IService; public interface HealthService extends IService<Health> { }package cn.freyfang.health.service.impl; import cn.freyfang.health.bean.Health; import cn.freyfang.health.mapper.HealthMapper; import cn.freyfang.health.service.HealthService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; @Service public class HealthServiceImpl extends ServiceImpl<HealthMapper, Health> implements HealthService { } -
创建Mapper层
package cn.freyfang.health.mapper; import cn.freyfang.health.bean.Health; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface HealthMapper extends BaseMapper<Health> { }- 在
resources/mapper目录下创建HealthMapper.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="cn.freyfang.health.mapper.HealthMapper"> </mapper> - 在
-
创建Controller
package cn.freyfang.health.controller; import cn.freyfang.health.assistant.HealthAgent; import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.loader.FileSystemDocumentLoader; import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; import io.swagger.v3.oas.annotations.Operation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.io.ResourceLoader; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import java.io.IOException; import java.util.Arrays; import java.util.List; @RestController @RequestMapping("/health") public class HealthController { @Autowired private HealthAgent healthAgent; @Operation(summary = "聊天", description = "实现体检报告检索(RAG)+ 健康问题记录及取消(Tool)") //这里用Get是为了浏览器方便测试,实际生产中请用Post @GetMapping(value = "/chat/{memoryId}/{message}", produces = "text/stream;charset=utf-8") public Flux<String> chat(@PathVariable("memoryId") String memoryId, @PathVariable("message") String message) { return healthAgent.chat(memoryId, message); } @Autowired @Qualifier("embeddingStoreHealth") private EmbeddingStore embeddingStore; @Autowired private EmbeddingModel embeddingModel; @Autowired private ResourceLoader resourceLoader; @Operation(summary = "模拟上传体检报告文档") @PostMapping("/upload") public String uploadDoc() throws IOException { //1、文档加载、解析 Document document1 = FileSystemDocumentLoader.loadDocument(resourceLoader.getResource("classpath:knowledge/体检报告.pdf").getFile().getPath(), new ApachePdfBoxDocumentParser()); List<Document> documents = Arrays.asList(document1); //2、分片、向量化、存储 EmbeddingStoreIngestor.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .build() .ingest(documents); return "OK"; } }
5、测试
-
启动
AiLangChain4jApp -
访问接口测试页面:http://localhost:8080/doc.html
-
直接调用"模拟上传体检报告"接口,返回"OK"说明"体检报告.pdf"已上传到知识库,连接pgvector,查看库ff_ai、表health,验证文档索引阶段已成功
-
调用"聊天"接口
- 因为knife4j无法显示流式打字效果,所以直接通过浏览器地址栏访问
http://localhost:8080/health/chat/{memoryId}/{message}
- 因为knife4j无法显示流式打字效果,所以直接通过浏览器地址栏访问
-
测试RAG:连问3个问题,功能正常。聊天记忆如下
[ { "text": "这里是设置的系统提示词。。。", "type": "SYSTEM" }, { "contents": [ { "text": "你是谁", "type": "TEXT" } ], "type": "USER" }, { "text": "我是苏大强,健康领域的专家。如果您有关于体检报告或健康方面的问题,欢迎向我咨询。", "type": "AI" }, { "contents": [ { "text": "我的身体有什么问题吗\n\nAnswer using the following information: 这里是RAG的检索结果,和用户问题一起发送给LLM", "type": "TEXT" } ], "type": "USER" }, { "text": "根据您提供的体检报告信息,。。。省略。。。。也是正常的。\n\n从这些信息来看,您的身体状况良好,并没有发现健康问题。当然,为了保持良好的健康状态,请继续保持健康的生活方式,定期进行体检。如果您有任何不适或疑虑,建议及时就医咨询。", "type": "AI" }, { "contents": [ { "text": "我的体检报告有什么问题\n\nAnswer using the following information: 这里是RAG的检索结果,和用户问题一起发送给LLM", "type": "TEXT" } ], "type": "USER" }, { "text": "根据您提供的体检报告信息,您的。。。。省略。。。", "type": "AI" } ] -
测试Tool:可能出现幻觉,需要不断的调整"工具描述和方法参数"以及提示词

7000

被折叠的 条评论
为什么被折叠?



