文章目录
一、前言
1、本文介绍
- 本文介绍了
SpringBoot + SpringAI实现大模型应用开发,通过一系列测试用例演示了核心功能(提示词、聊天记忆、函数调用、RAG等)的实现 - 你只需要一步步按照本文实操,即可快速熟悉所有知识点
- 本文使用的开发工具:
IntelliJ IDEA Community Edition 2025.1 - 本文使用的SpringAI版本:
1.0.0 - 本文同时使用
硅基流动平台中的deepseek-v3和阿里百炼平台中的通义系列模型来演示,因为这两个平台有免费额度
2、SpringAI介绍
-
2025 年 5 月 20 日,Spring AI 官方团队宣布 1.0 GA 版本正式发布
-
官网:https://spring.io/projects/spring-ai
-
参考文档:https://docs.spring.io/spring-ai/reference/index.html
-
SpringAI 旨在简化包含人工智能功能的应用程序的开发
-
该项目的灵感源自一些著名的 Python 项目,例如 LangChain 和 LlamaIndex,但 Spring AI 并非这些项目的直接移植
-
Spring AI 提供以下功能
- 支持所有主流AI 模型提供商,例如 Anthropic、OpenAI、Microsoft、Amazon、Google 和 Ollama
- 支持的模型类型包括:Chat Completion、Embedding、Text to Image、Audio Transcription、Text to Speech、Moderation
- 结构化输出- AI 模型输出到 POJO 的映射
- 支持所有主要的矢量数据库提供商,例如 Apache Cassandra、Azure Cosmos DB、Azure Vector Search、Chroma、Elasticsearch、GemFire、MariaDB、Milvus、MongoDB Atlas、Neo4j、OpenSearch、Oracle、PostgreSQL/PGVector、PineCone、Qdrant、Redis、SAP Hana、Typesense 和 Weaviate
- Tools/Function Calling
- Spring Boot 自动配置和 AI 模型和矢量存储的启动器
- ChatClient API - 用于与 AI 聊天模型通信的流畅 API,惯用语类似于 WebClient 和 RestClient API
- Advisors API - 封装重复的生成式 AI 模式,转换发送到和来自语言模型 (LLM) 的数据,并提供跨各种模型和用例的可移植性
- 支持聊天对话记忆和检索增强生成(RAG)
-
核心概念
- 模型Models
- 人工智能模型是用于处理和生成信息的算法,通常模仿人类的认知功能。通过从大型数据集中学习模式和洞察,这些模型可以进行预测、文本、图像或其他输出,从而增强各行各业的各种应用
- 分类:语言模型、图像视频模型、文本-音频模型、嵌入模型
- 提示词Prompts
- 用于引导 AI 模型生成特定的输出
- 人们必须像与人交谈一样与 AI 模型进行交流。这种交互方式如此重要,以至于“提示工程”一词已发展成为一门独立的学科。目前,有越来越多的技术可以提高提示的有效性。投入时间精心设计提示可以显著提升最终效果
- 提示最初只是一些简单的字符串,后来演变为包含多条消息,每条消息中的每个字符串都代表着模型的不同角色
- 嵌入Embeddings
- 嵌入的工作原理是将文本、图像和视频转换为浮点数数组(称为向量)。这些向量旨在捕捉文本、图像和视频的含义
- 嵌入数组的长度称为向量的维数
- 通过计算两段文本的向量表示之间的数值距离,应用程序可以确定用于生成嵌入向量的对象之间的相似性
- Tokens
- 输入时,模型将单词转换为Token。输出时,模型将Token转换回单词
- 在英语中,一个Token大致对应一个单词的 75%
- 在托管AI模型的背景下,你的费用取决于使用的Token数量。输入和输出都会计入Token总量
- 模型还受到Token限制的影响,这会限制单个 API 调用中处理的文本量。此阈值通常称为“上下文窗口”。模型不会处理任何超出此限制的文本
- 结构化输出
- 即使你要求回复为 JSON 格式, AI 模型的输出通常也会以String的形式出现。在提示中要求“需要 JSON”并非 100% 准确。
- 这种复杂性导致了一个专门领域的出现,该领域涉及创建提示以产生预期的输出,然后将生成的简单字符串转换为可用于应用程序集成的数据结构
- 结构化输出转换采用精心设计的提示,通常需要与模型进行多次交互才能实现所需的格式。
- 将您的数据和API引入AI模型
- 如何为人工智能模型配备尚未训练过的信息?有三种技术可以定制 AI 模型来整合您的数据
- 微调:这项传统的机器学习技术涉及调整模型并更改其内部权重。然而,对于机器学习专家来说,这是一个极具挑战性的过程,而且由于 GPT 等模型的规模庞大,会极其耗费资源。此外,某些模型可能不提供此选项。
- 提示填充:一种更实用的替代方案是将数据嵌入到提供给模型的提示中。考虑到模型的令牌限制,需要采用一些技术在模型的上下文窗口中呈现相关数据。这种方法通俗地称为“填充提示”。Spring AI 库可以帮助您基于“填充提示”技术(也称为检索增强生成 (RAG))实现解决方案。
- 工具调用:此技术允许注册工具(用户定义的服务),将大型语言模型连接到外部系统的 API。Spring AI 大大简化了支持工具调用所需的代码。
- 如何为人工智能模型配备尚未训练过的信息?有三种技术可以定制 AI 模型来整合您的数据
- 模型Models
二、快速开始
-
创建项目
ff-ai-springai
-
在
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>cn.freyfang</groupId> <artifactId>ff-ai-springai</artifactId> <version>1.0-SNAPSHOT</version> <!-- 继承父项目 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.12</version> </parent> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- 指定版本 --> <spring-ai.version>1.0.0</spring-ai.version> </properties> <dependencies> <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> <!-- 1、引入openai --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <!-- 引入spring-ai 版本管理清单 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project> -
创建启动类
SpringAIApplicationpackage cn.freyfang; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringAIApplication { public static void main(String[] args) { SpringApplication.run(SpringAIApplication.class, args); } } -
创建配置文件
application.ymlspring: ai: # 使用openai的标准来调用硅基流动平台中的deepseek-v3,注意有些模型(如R1)可能会报timeout或404等错误 openai: api-key: sk-xxx base-url: https://api.siliconflow.cn chat: options: model: deepseek-ai/DeepSeek-V3 -
创建测试类
Test01_GettingStartedpackage cn.freyfang; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.client.ChatClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class Test01_GettingStarted { // 使用自动配置的 ChatClient.Builder @Autowired private ChatClient.Builder chatClientBuilder ; @Test public void test01(){ ChatClient chatClient = chatClientBuilder.build(); String content = chatClient.prompt() .user("你是谁?") //用户消息 .call()//向 AI 模型发送请求 .content();//以String的形式返回 System.out.println(content);//我是DeepSeek Chat,由深度求索公司(DeepSeek)创造的智能AI助手... } }
三、ChatClient
1、创建ChatClient
1)使用自动配置的 ChatClient.Builder
- 默认情况下,Spring AI 会自动配置单个
ChatClient.BuilderBean,如在快速开始中的使用方式
2)创建多个聊天模型
-
使用一个模型创建多个ChatClients,这些实例使用相同的底层模型,但具有不同的配置
-
使用多个模型创建不同的ChatClients,需要禁用自动配置ChatClient.Builder
spring.ai.chat.client.enabled=false -
使用OpenAI兼容方式派生ChatClients
3)demo
-
在
pom.xml中引入依赖<!-- 2、引入阿里云百炼 --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> </dependency> -
在
pom.xml中引入依赖版本管理<!-- 引入阿里百炼 版本管理清单 --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-bom</artifactId> <version>1.0.0.2</version> <type>pom</type> <scope>import</scope> </dependency> -
在
application.yml中配置千问模型spring: ai: openai: #... chat: client: enabled: false #当使用了多个model时,需要禁用自动配置ChatClient.Builder dashscope: api-key: sk-xxx base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 chat: options: model: qwen-max -
创检测试类
Test02_ChatClientpackage cn.freyfang; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class Test02_ChatClient { @Autowired private OpenAiChatModel openAiChatModel; /** * 1、同一模型的不同实例 */ @Test public void test01() { // 方式1 ChatClient chatClient1 = ChatClient.create(openAiChatModel); System.out.println(chatClient1.prompt() .user("你是谁?") .call() .content());//我是DeepSeek Chat,由... //方式2 可以设置参数 ChatClient chatClient2 = ChatClient.builder(openAiChatModel) .defaultSystem("你是一个股票专家") .build(); System.out.println(chatClient2.prompt() .user("你是谁") .call() .content());//我是您的股票投资助手... } @Autowired private DashScopeChatModel dashScopeChatModel; /** * 2、不同模型的实例 */ @Test public void test02() { System.out.println(ChatClient.create(openAiChatModel).prompt().user("你是谁").call().content());//我是DeepSeek Chat,... System.out.println(ChatClient.create(dashScopeChatModel).prompt().user("你是谁").call().content());//我是Qwen,... } /** * 3、基于OpenAI兼容API派生实例 */ @Test public void test03() { OpenAiApi baseOpenAiApi = OpenAiApi.builder().apiKey("").build(); // 先派生 OpenAiApi OpenAiApi guiji = baseOpenAiApi.mutate() .baseUrl("https://api.siliconflow.cn") //硅基流动 .apiKey("sk-xxx") .build(); OpenAiApi openai = baseOpenAiApi.mutate() .baseUrl("https://api.gptsapi.net") //openAI .apiKey("sk-xxx") .build(); // 再派生 ChatModel OpenAiChatModel guijiModel = OpenAiChatModel .builder() .openAiApi(guiji) .defaultOptions(OpenAiChatOptions.builder().model("THUDM/glm-4-9b-chat").temperature(0.5).build()) .build(); OpenAiChatModel openAiModel = OpenAiChatModel .builder() .openAiApi(openai) .defaultOptions(OpenAiChatOptions.builder().model("gpt-4o-2024-08-06").temperature(0.7).build()) .build(); System.out.println(ChatClient.builder(guijiModel).build().prompt().user("你是谁").call().content());//我是一个人工智能助手,名为 ChatGLM... System.out.println(ChatClient.builder(openAiModel).build().prompt().user("你是谁").call().content());//我是一个人工智能助手,旨在... } }
2、ChatClient响应
-
call() 返回值:返回 ChatResponse、返回实体
-
String content():返回响应的字符串内容,如上文的测试方法 -
ChatResponse chatResponse():返回ChatResponse包含多个Token以及有关响应的元数据的对象 -
ChatClientResponse chatClientResponse():返回一个ChatClientResponse包含ChatResponse对象和 ChatClient 执行上下文的对象,使您能够访问assistant执行期间使用的附加数据(例如,在 RAG 流中检索到的相关文档) -
entity()返回 Java 类型 -
stream() 返回值:流式响应
-
Flux<String> content():返回Flux由AI模型生成的字符串 -
Flux<ChatResponse> chatResponse():返回Flux对象ChatResponse,其中包含有关响应的附加元数据。 -
Flux<ChatClientResponse> chatClientResponse() -
在测试类
Test02_ChatClient中创建测试方法/** * 4、返回ChatResponse,包含有关响应生成方式的元数据 */ @Test public void test04() { ChatResponse chatResponse = ChatClient.create(openAiChatModel) .prompt() .user("你是谁?") .call() .chatResponse(); System.out.println(Objects.requireNonNull(chatResponse).getResult().getOutput().getText());//我是DeepSeek Chat,由... ChatResponseMetadata metadata = Objects.requireNonNull(chatResponse).getMetadata(); System.out.println(metadata.getUsage().getPromptTokens());//提示Tokens 5 System.out.println(metadata.getUsage().getCompletionTokens());//响应Tokens 68 System.out.println(metadata.getUsage().getTotalTokens());//73 } record ActorFilms(String actor, List<String> movies) { } /** * 5、返回实体 */ @Test public void test05() { ActorFilms actorFilms = ChatClient.create(openAiChatModel) .prompt() .user("生成一个中国大陆随机演员的电影记录") .call() .entity(ActorFilms.class); System.out.println(actorFilms);//ActorFilms[actor=黄渤, movies=[疯狂的石头, 人在囧途, 一出好戏, 心花路放, 无人区]] } /** * 6、流式响应 */ @Test public void test06() throws InterruptedException { Flux<String> output = ChatClient.create(openAiChatModel) .prompt() .user("你是谁?") .stream() .content(); output.subscribe(System.out::println); // 每次收到一段响应时打印 //subscribe 是非阻塞的,所以先不让本线程结束 Thread.sleep(10000); }
4、Advisors
- Advisors API提供了一种灵活而强大的方法来拦截、修改和增强 Spring 应用程序中的 AI 驱动的交互
- 使用用户文本调用 AI 模型时的一个常见模式是使用上下文数据附加或扩充提示
- 这些上下文数据可以属于不同类型。常见类型包括
- 您自己的数据:这是 AI 模型尚未训练过的数据。即使模型已经见过类似的数据,附加的上下文数据在生成响应时也会优先使用
- 对话历史记录:聊天模型的 API 是无状态的。如果您告诉 AI 模型您的姓名,它不会在后续交互中记住它。每次请求都必须发送对话历史记录,以确保在生成响应时考虑到之前的交互
三、提示词
-
最初,提示符只是简单的字符串。随着时间的推移,它们逐渐包含特定输入的占位符,例如“USER:”,而人工智能模型可以识别这些占位符。OpenAI 通过在人工智能模型处理多个消息字符串之前,将它们分类到不同的角色中,为提示符引入了更丰富的结构
-
主要角色
- 系统角色system:引导AI的行为和响应方式,设置AI如何解释和回复输入的参数或规则。这类似于在发起对话之前向AI提供指令
- 用户角色user:代表用户的输入——他们向AI提出的问题、命令或语句。这个角色至关重要,因为它构成了AI响应的基础
- 助手角色assistant:AI 对用户输入的响应。它不仅仅是一个答案或反应,对于维持对话的流畅性至关重要。通过追踪 AI 之前的响应(其“助手角色”消息),系统可以确保交互的连贯性以及与上下文的相关性。助手消息也可能包含功能工具调用请求信息。它就像 AI 中的一项特殊功能,在需要执行特定功能(例如计算、获取数据或其他不仅仅是对话的任务)时使用。
- 工具/功能角色tool:该角色专注于响应工具调用助手消息返回附加信息
-
默认情况下,模板变量由
{}语法标 -
在
resources/prompts下创建提示词模版system-message.txt你是一个健身教练,曾获得过健身世界赛冠军 -
创建测试类
Test03_Promptpackage cn.freyfang; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.io.Resource; @SpringBootTest public class Test03_Prompt { @Autowired private OpenAiChatModel openAiChatModel; /** * 提示词及模版字符串 */ @Test public void test01() { ChatClient chatClient = ChatClient.builder(openAiChatModel) .defaultSystem("你是一个股票专家,你的名字是{name}") //带参数的默认系统提示词 .defaultUser("请用{area}话回答问题,{userMessage}") //带参数的默认用户提示词 .build(); System.out.println(chatClient.prompt() .system(promptSystemSpec -> promptSystemSpec.param("name", "张三")) .user(promptUserSpec -> promptUserSpec.param("area", "东北") .param("userMessage", "你是谁?")) .call() .content());//哎哟我滴老天爷啊!你可算问对人啦!俺是张三, } @Value("classpath:/prompts/system-message.txt") private Resource systemResource; /** * 提示词及模版文件 */ @Test public void test02() { ChatClient chatClient = ChatClient.builder(openAiChatModel) .defaultSystem(systemResource) .build(); System.out.println(chatClient.prompt() .user("你是谁?") .call() .content());//嘿,兄弟!我是你的健身教练,前世界健身冠军!💪 ... } }
四、聊天记忆
-
大型语言模型 (LLM) 是无状态的,这意味着它们不会保留先前交互的信息。当您希望在多个交互之间维护上下文或状态时,这可能会造成限制。为了解决这个问题,Spring AI 提供了聊天记忆功能,允许您使用 LLM 跨多个交互存储和检索信息
-
聊天记忆和聊天历史之间的区别
- 聊天记忆。大模型保留并用于在整个对话过程中保持语境感知的信息
- 聊天记录。整个对话历史记录,包括用户和模型之间交换的所有消息
-
Spring AI 会自动配置一个
ChatMemoryBean,供您在应用程序中直接使用。默认情况下,它使用内存存储库(InMemoryChatMemoryRepository)来存储消息,并使用一个MessageWindowChatMemory实现来管理对话历史记录。如果已配置其他存储库(例如 Cassandra、JDBC 或 Neo4j),Spring AI 将改用该存储库
1、基于内存
-
创建测试类
Test04_Memorypackage cn.freyfang; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class Test04_Memory { @Autowired private DashScopeChatModel dashScopeChatModel; /** * 1、默认没有记忆 */ @Test public void test01() { ChatClient chatClient = ChatClient.create(dashScopeChatModel); System.out.println(chatClient.prompt() .user("我是张三") .call() .content());//你好,张三!很高兴见到你。有什么我可以帮助你的吗? System.out.println(chatClient.prompt() .user("我是谁?") .call() .content());//您是这次对话的发起者,但我无法直接知道您的具体身份信息... } /** * 2、基于内存的记忆 * 使用 MessageWindowChatMemory + InMemoryChatMemoryRepository(内存存储) */ @Test public void test02() { // MessageWindowChatMemory 维护一个消息窗口,使其不超过指定的最大大小。当消息数量超过最大值时,较旧的消息将被删除,同时保留系统消息。 MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(100) .build(); //默认会 new InMemoryChatMemoryRepository() ChatClient chatClient = ChatClient.builder(dashScopeChatModel) .defaultAdvisors( //每次交互时,此Advisor都会从内存中检索对话历史记录,并将其作为消息集合包含在提示中 MessageChatMemoryAdvisor .builder(chatMemory) .build() ) .build(); System.out.println(chatClient.prompt() .user("我是李四") .call() .content());//你好,李四!很高兴认识你。有什么我可以帮助你的吗? System.out.println(chatClient.prompt() .user("我是谁") .call() .content());//你刚才告诉我你是李四。如果你是在问其他身份相关的信息,或者有其他问题需要帮助,请告诉我更多的细节。 } }
2、基于mysql
-
Spring AI 通过方言抽象支持多种关系数据库。以下数据库是开箱即用的:PostgreSQL、MySQL / MariaDB、SQL Server、HSQLDB。也可以通过实现
JdbcChatMemoryRepositoryDialect接口来扩展对其他数据库的支持 -
本文使用mysql来实现记忆存储
- 使用wsl+docker安装mysql
- 创建时数据库
ff_ai
-
在
pom.xml中引入依赖<!-- 3、引入聊天记忆存储 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> -
在
application.yml中添加配置spring: ai: openai: #。。。 chat: #。。。 # 添加 memory: repository: jdbc: initialize-schema: always # 控制何时初始化存储表,默认值 embedded platform: mariadb # 使用脚本的名称:@@platform@@.sql,默认值:自动检测。但因为classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-*.sql没有提供mysql的脚本,只有mariadb脚本,所以这里设置为mariadb。当然也可以创建一个schema-mysql.sql文件并通过spring.ai.chat.memory.repository.jdbc.schema参数设置脚本位置 dashscope: #。。。 # 添加 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 -
查看
schema.sql文件的内容CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( conversation_id VARCHAR(36) NOT NULL, content TEXT NOT NULL, type VARCHAR(10) NOT NULL, `timestamp` TIMESTAMP NOT NULL, CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')) ); CREATE INDEX IF NOT EXISTS SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX ON SPRING_AI_CHAT_MEMORY(conversation_id, `timestamp`); -
在测试类
Test04_Memory中创建测试方法@Autowired private JdbcTemplate jdbcTemplate; /** * 2、基于mysql的记忆持久化 + 记忆隔离 * 使用 MessageWindowChatMemory + JdbcChatMemoryRepository + mysql 存储记忆 */ @Test public void test03() { // 1、JdbcChatMemoryRepository ChatMemoryRepository chatMemoryRepository = JdbcChatMemoryRepository.builder() .jdbcTemplate(jdbcTemplate) .dialect(new MysqlChatMemoryRepositoryDialect()) .build(); //2、MessageWindowChatMemory MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) // 使用JdbcChatMemoryRepository存储对话历史记录 .maxMessages(100) .build(); //3、MessageChatMemoryAdvisor ChatClient chatClient = ChatClient.builder(dashScopeChatModel) .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) .build(); System.out.println(chatClient.prompt() .user("我是王五") .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "1")) .call() .content());//你好,王五!很高兴认识你。有什么我可以帮助你的吗? System.out.println(chatClient.prompt() .user("我是谁") .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "1")) .call() .content());//你刚刚告诉我,你是王五。如果你是在问其他方面的身份信息或有其他问题需要帮助,可以再具体说明一下哦! /* 查看表: SPRING_AI_CHAT_MEMORY 1 我是王五 USER 2025-06-02 15:50:56 1 你好,王五!很高兴认识你。有什么我可以帮助你的吗? ASSISTANT 2025-06-02 15:50:56 1 我是谁 USER 2025-06-02 15:50:56 1 你刚刚告诉我,你是王五。如果你是在问其他方面的身份信息或有其他问题需要帮助,可以再具体说明一下哦! ASSISTANT 2025-06-02 15:50:56 */ }
五、Tool
- 工具调用(也称为函数调用)是 AI 应用程序中的一种常见模式,允许模型与一组 API 或工具进行交互,从而增强其功能
- 工具主要用于
- 信息检索。此类工具可用于从外部来源(例如数据库、Web 服务、文件系统或 Web 搜索引擎)检索信息。其目标是增强模型的知识,使其能够回答原本无法回答的问题。因此,它们可用于检索增强生成 (RAG) 场景。例如,可以使用工具检索给定位置的当前天气、检索最新新闻文章或查询数据库中的特定记录
- 执行操作。此类工具可用于在软件系统中采取行动,例如发送电子邮件、在数据库中创建新记录、提交表单或触发工作流。其目标是自动化原本需要人工干预或明确编程的任务。例如,可以使用工具为与聊天机器人交互的客户预订航班、在网页上填写表单,或在代码生成场景中基于自动化测试 (TDD) 实现 Java 类
- 尽管我们通常将工具调用称为模型功能,但实际上工具调用逻辑是由客户端应用程序提供的。模型只能请求工具调用并提供输入参数,而应用程序负责根据输入参数执行工具调用并返回结果。模型永远无法访问任何作为工具提供的 API,这是一个至关重要的安全考虑因素
1、方法作为工具
-
声明式地使用
@Tool注解修饰方法,方法可以是静态的,也可以是实例的,并且可以具有任意可见性name:工具的名称。如果未提供,则使用方法名称description:工具的描述,模型可以通过该描述了解何时以及如何调用该工具。如果未提供,则方法名称将用作工具描述。如果描述不充分,可能会导致模型在应该使用工具时不使用它,或者错误地使用它returnDirect:工具结果应直接返回给客户端还是传递回模型resultConverter:ToolCallResultConverter用于将工具调用的结果转换
-
注解
@ToolParam修饰工具参数description:参数的描述,模型可以通过它来更好地理解如何使用参数。例如,参数应采用什么格式、允许使用哪些值等等required:参数是必需的还是可选的。默认情况下,所有参数都被视为必需的
-
创建工具类
DateTimeToolspackage cn.freyfang.demo; import org.springframework.ai.tool.annotation.Tool; import org.springframework.context.i18n.LocaleContextHolder; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class DateTimeTools { @Tool(description = "获取用户所在时区中的当前日期和时间") String getCurrentDateTime() { String now = LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); System.out.println("getCurrentDateTime:" + now); return now; } @Tool(description = "设置给定时间的用户闹钟,以ISO-8601格式提供") void setAlarm(String time) { LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME); System.out.println("setAlarm:" + time + alarmTime); } } -
创建测试类
Test05_Toolpackage cn.freyfang; import cn.freyfang.demo.DateTimeTools; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.client.ChatClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class Test05_Tool { @Autowired private DashScopeChatModel dashScopeChatModel; /** * 1、信息检索: 获取当前日期 * 由于AI 模型无法访问实时信息,我们可以提供一个工具来检索这些信息 */ @Test public void test01() { ChatClient chatClient = ChatClient.create(dashScopeChatModel); System.out.println(chatClient.prompt() .user("明天是几号?") .call() .content());//我目前无法直接查看实时日期... System.out.println(chatClient.prompt() .user("明天是几号?") //将工具提供给模型。当模型需要获取当前日期和时间时,它会请求调用该工具。在内部,ChatClient将调用该工具并将结果返回给模型,然后模型将使用工具调用结果生成对原始问题的最终响应。 .tools(new DateTimeTools()) .call() .content());//明天是2025年6月3日。 } /** * 2、执行操作:在特定时间设置闹钟 */ @Test public void test02() { ChatClient chatClient = ChatClient.create(dashScopeChatModel); System.out.println(chatClient.prompt() .user("你能把闹钟定在10分钟后吗?") //模型首先需要知道当前日期和时间。然后,它将使用当前日期和时间计算闹钟时间。最后,它将使用闹钟工具设置闹钟。在内部,ChatClient将处理来自模型的任何工具调用请求,并将任何工具调用执行结果发送回模型,以便模型可以生成最终响应。 .tools(new DateTimeTools()) .call() .content());//已经帮您设置了10分钟后的闹钟,现在的时刻是2025-06-02 18:50:25,闹钟将在19:00:25响起。 } }
2、函数作为工具
-
Spring AI 提供对从函数指定工具的内置支持,可以通过编程方式使用低级
FunctionToolCallback实现,也可以@Bean在运行时动态解析 -
创建工具类
WeatherServicepackage cn.freyfang.demo; import java.util.function.Function; public class WeatherService implements Function<WeatherRequest, WeatherResponse> { public WeatherResponse apply(WeatherRequest request) { System.out.println("WeatherService"); return new WeatherResponse(30.0, Unit.C); } } enum Unit {C, F} -
创建请求响应类
WeatherResponse、WeatherRequestpublic record WeatherResponse(double temp, Unit unit) { } public record WeatherRequest(String location, Unit unit) { } -
在测试类中创建测试方法
/** * 3、使用 FunctionToolCallback 实现 */ @Test public void test03() { ToolCallback toolCallback = FunctionToolCallback .builder("currentWeather", new WeatherService()) .description("获取天气") .inputType(WeatherRequest.class) .build(); System.out.println(ChatClient.create(dashScopeChatModel) .prompt() .user("北京的天气如何") .toolCallbacks(toolCallback) .call() .content());//北京现在的温度是30.0摄氏度。 }
六、RAG

1、ETL管道
- 提取、转换和加载 (ETL) 框架是检索增强生成 (RAG) 用例中数据处理的支柱
- ETL 管道协调从原始数据源到结构化向量存储的流程,确保数据采用 AI 模型检索的最佳格式
- ETL 管道有三个主要组件
DocumentReader实现Supplier<List<Document>>加载文档- TextReader 读取txt文档
- PagePdfDocumentReader 读取pdf
DocumentTransformer实现Function<List<Document>, List<Document>>转换文档- TokenTextSplitter 文档分割
- KeywordMetadataEnricher 从文档内容中提取关键字并将其添加为元数据
- SummaryMetadataEnricher 为文档创建摘要并将其添加为元数据
DocumentWriter实现Consumer<List<Document>>存储文档- FileDocumentWriter 将Document写入文件
- VectorStore 将文档内容向量化并存储向量数据库
2、向量数据库
-
在向量数据库中,查询与传统的关系型数据库不同。它们执行相似性搜索,而不是精确匹配。当给定一个向量作为查询时,向量数据库会返回与查询向量“相似”的向量
-
向量数据库用于将您的数据与 AI 模型集成。使用向量数据库的第一步是将您的数据加载到向量数据库中。然后,当用户查询要发送到 AI 模型时,系统会首先检索一组类似的文档。这些文档随后作为用户问题的上下文,并与用户的查询一起发送到 AI 模型。这项技术称为检索增强生成 (RAG)
-
向量数据库的作用是存储这些embeddings,并方便进行相似性搜索。它本身并不生成embeddings。要创建向量embeddings,应该使用
EmbeddingModel -
批处理策略
- 使用向量存储时,通常需要嵌入大量文档。虽然一次调用即可嵌入所有文档看似简单,但这种方法可能会导致问题。嵌入模型将文本作为标记处理,并且具有最大标记限制,通常称为上下文窗口大小。此限制限制了单个嵌入请求中可处理的文本量。尝试在一次调用中嵌入过多的标记可能会导致错误或嵌入被截断
- 为了解决此令牌限制问题,Spring AI 实现了批处理策略。此方法将大量文档分解为适合嵌入模型最大上下文窗口的较小批次。批处理不仅解决了令牌限制问题,还可以提高性能并更有效地利用 API 速率限制
-
VectorStore 的一些实现
-
PgVector Store 本文使用该存储来演示
-
PGvector是 PostgreSQL 的一个开源扩展,支持存储和搜索机器学习生成的嵌入。它提供了多种功能,让用户能够识别精确和近似的最近邻。它旨在与其他 PostgreSQL 功能(包括索引和查询)无缝协作
-
使用wsl+docker安装
docker pull pgvector/pgvector:pg16
-
3、检索
-
Spring AI 使用
Advisor API为常见的 RAG 流提供开箱即用的支持 -
QuestionAnswerAdvisor
- 向量数据库存储的是 AI 模型无法感知的数据。当用户问题发送给 AI 模型时,
QuestionAnswerAdvisor会查询向量数据库,查找与用户问题相关的文档。来自向量数据库的响应被附加到用户文本中,为 AI 模型生成响应提供上下文 - 假设您已经将数据加载到
VectorStore中,则可以通过向ChatClient提供QuestionAnswerAdvisor的实例来执行检索增强生成 (RAG)
- 向量数据库存储的是 AI 模型无法感知的数据。当用户问题发送给 AI 模型时,
-
RetrievalAugmentationAdvisor
-
Spring AI包含一个RAG模块库,您可以使用它来构建自己的RAG流。RetrievalAugmentationAdvisor是一个Advisor,它基于模块化体系结构,为最常见的RAG流提供了开箱即用的实现
-
在
pom.xml中引入依赖<!-- 4、引入rag --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-advisors-vector-store</artifactId> </dependency> <!-- 引入pdf读取 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pdf-document-reader</artifactId> </dependency> <!-- 引入pgvector --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-pgvector</artifactId> </dependency> -
修改
application.ymlspring: ai: openai: # ... chat: #... dashscope: embedding: enabled: true base-url: https://dashscope.aliyuncs.com # 必须指定,否则路径会拼接错误 options: model: text-embedding-v3 dimensions: 1024 api-key: sk-d2d9421f043e425090697b88c626a639 base-url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions chat: options: model: qwen-max vectorstore: pgvector: initialize-schema: true # 会初始化表 # remove-existing-vector-store-table: true index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 1024 # 模型维度 要和模型保持一致 max-document-batch-size: 10000 # Optional: Maximum number of documents per batch # 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 datasource: url: jdbc:postgresql://localhost:5432/postgres username: root password: 123456 -
创建测试类
Test06_RAGpackage cn.freyfang; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.api.Advisor; import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor; import org.springframework.ai.document.Document; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor; import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter; import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever; import org.springframework.ai.reader.TextReader; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest public class Test06_RAG { @Autowired private OpenAiChatModel openAiChatModel; @Autowired VectorStore vectorStore; /** * 测试ETL: 加载-》分段-》向量化并存储 */ @Test public void test01() { /* 1、XxxReader 加载文档 txt/pdf/md/doc... */ // 1.1 读取 Txt 文档内容 到 Document 对象 TextReader textReader = new TextReader("classpath:rag-text.txt"); //setCharset(Charset charset):设置读取文本文件的字符集。默认为 UTF-8。 // textReader.setCharset("UTF-8"); // getCustomMetadata 返回一个可变映射,您可以在其中为文档添加自定义元数据。 textReader.getCustomMetadata().put("filename", "rag-text.txt"); List<Document> documents = textReader.read(); //[Document{id='f9e9b605-a84a-42c7-a767-a7c8727ad14b', text='hi,你好,我是张三,今天天气晴,温度25度。', // media='null', metadata={charset=UTF-8, filename=rag-text.txt, source=rag-text.txt}, score=null}] System.out.println(documents); /* 1.2 读取pdf PagePdfDocumentReader pdfReader = new PagePdfDocumentReader("classpath:/编程指南.pdf", PdfDocumentReaderConfig.builder() .withPageTopMargin(0) .withPageExtractedTextFormatter(ExtractedTextFormatter.builder() .withNumberOfTopTextLinesToDelete(0) .build()) .withPagesPerDocument(1) .build()); System.out.println(pdfReader.read()); */ /* 2、Transformer 文档处理 将TextReader整个文件内容读入内存,因此可能不适合非常大的文件。 如果需要将文本拆分成更小的块,则可以TokenTextSplitter在阅读文档后使用文本拆分器: */ List<Document> docs = textReader.get(); // 2.1 文档分割器 TokenTextSplitter //使用默认设置创建拆分器 List<Document> splitDocuments = new TokenTextSplitter().apply(docs); /* 自定义分割参数 TokenTextSplitter(int defaultChunkSize, int minChunkSizeChars, int minChunkLengthToEmbed, int maxNumChunks, boolean keepSeparator) defaultChunkSize:每个文本块的目标大小(以标记为单位)(默认值:800)。 minChunkSizeChars:每个文本块的最小字符数(默认值:350)。 minChunkLengthToEmbed:要包含的块的最小长度(默认值:5)。 maxNumChunks:从文本生成的最大块数(默认值:10000)。 keepSeparator:是否在块中保留分隔符(如换行符)(默认值:true)。 */ System.out.println(splitDocuments); /* 3、Writer 存储 */ vectorStore.add(splitDocuments); vectorStore.add(List.of(new Document("我喜欢足球"))); // 检索 List<Document> results = this.vectorStore.similaritySearch(SearchRequest.builder().query("今天天气怎么样?").build()); System.out.println(results); } /** * 测试RAG : QuestionAnswerAdvisor */ @Test public void test02() { QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore) //设置相似度阈值0.8并返回最佳6个结果 .searchRequest(SearchRequest.builder().similarityThreshold(0.5d).topK(6).build()) .build(); System.out.println(ChatClient.builder(openAiChatModel) .build() .prompt() .advisors(qaAdvisor) .user("今天天气怎么样") .call() .content()); //根据提供的上下文信息,今天天气晴,温度25度。 } /** * 测试RAG : retrievalAugmentationAdvisor */ @Test public void test03() { Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder() .documentRetriever(VectorStoreDocumentRetriever.builder() .similarityThreshold(0.50) .vectorStore(vectorStore) .build()) //默认情况下,RetrievalAugmentationAdvisor不允许检索到的上下文为空。如果出现这种情况,它会指示模型不回答用户查询。您可以按如下方式允许空上下文。 .queryAugmenter(ContextualQueryAugmenter.builder() .allowEmptyContext(true) .build()) .build(); System.out.println(ChatClient.builder(openAiChatModel) .build() .prompt() .advisors(retrievalAugmentationAdvisor) .user("今天天气怎么样") .call() .content()); //今天天气晴。 } }
2169

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



