Spring AI 入门(持续更新)

介绍

Spring AI 是 Spring 项目中一个面向 AI 应用的模块,旨在通过集成开源框架、提供标准化的工具和便捷的开发体验,加速 AI 应用程序的构建和部署。

依赖

<!-- 基于 WebFlux 的响应式 SSE 传输 -->
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
</dependency>
<!-- mcp -->
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
</dependency>
<!-- spring-ai -->
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- spring-web -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

配置文件

配置大模型的 API Key 模型类型

spring:ai:openai:base-url: ${AI_BASE_URL}api-key: ${AI_API_KEY} # 通过环境变量文件 .env 获取chat:options:model: ${AI_MODEL}temperature: 0.8

我这里使用的是 DeepSeek 的 API,可以去官网查看:https://platform.deepseek.com/

# AI URL
AI_BASE_URL=https://api.deepseek.com
# AI 密钥
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxx
# AI 模型
AI_MODEL=deepseek-chat

配置类

概念

首先,简单介绍一些概念

  1. ChatClient

ChatClient 提供了与 AI 模型通信的 Fluent API,它支持同步和反应式(Reactive)编程模型。

ChatClient 类似于应用程序开发中的服务层,它为应用程序直接提供 AI 服务,开发者可以使用 ChatClient Fluent API 快速完成一整套 AI 交互流程的组装

  1. ChatModel

ChatModel 即对话模型,它接收一系列消息(Message)作为输入,与模型 LLM 服务进行交互,并接收返回的聊天消息(ChatMessage)作为输出。目前,它有 3 种类型:

  • ChatModel:文本聊天交互模型,支持纯文本格式作为输入,并将模型的输出以格式化文本形式返回

  • ImageModel:接收用户文本输入,并将模型生成的图片作为输出返回(文生图

  • AudioModel:接收用户文本输入,并将模型合成的语音作为输出返回

    ChatModel 的工作原理是接收 Prompt 或部分对话作为输入,将输入发送给后端大模型,模型根据其训练数据和对自然语言的理解生成对话响应,应用程序可以将响应呈现给用户或用于进一步处理。

问题

一个项目中可能会存在多个大模型的调用实例,例如 ZhiPuAiChatModel(智谱)、OllamaChatModel(Ollama本地模型)、OpenAiChatModel(OpenAi),这些实例都实现了ChatModel 接口,当然,我们可以直接使用这些模型实例来实现需求,但我们通常通过 ChatModel 来构建 ChatClient,因为这更通用

可以通过在 yml 配置文件中设置 spring.ai.chat.client.enabled=false 来禁用 ChatClient bean 的自动配置,然后为每个聊天模型 build 出一个 ChatClient。

spring:ai:chat:client:enabled: false

配置类

package cn.onism.mcp.config;import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** ChatClient 配置** @author wxw* @date 2025-03-25*/
@Configuration
public class ChatClientConfig {@Resourceprivate OpenAiChatModel openAiChatModel;@Resourceprivate ZhiPuAiChatModel zhiPuAiChatModel;@Resourceprivate OllamaChatModel ollamaChatModel;@Resourceprivate ToolCallbackProvider toolCallbackProvider;@Bean("openAiChatClient")public ChatClient openAiChatClient() {return ChatClient.builder(openAiChatModel)// 默认加载所有的工具,避免重复 new.defaultTools(toolCallbackProvider.getToolCallbacks()).defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory())).build();}@Bean("zhiPuAiChatClient")public ChatClient zhiPuAiChatClient() {return ChatClient.builder(zhiPuAiChatModel)// 默认加载所有的工具,避免重复 new.defaultTools(toolCallbackProvider.getToolCallbacks()).defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory())).build();}@Bean("ollamaChatClient")public ChatClient ollamaChatClient() {return ChatClient.builder(ollamaChatModel)// 默认加载所有的工具,避免重复 new.defaultTools(toolCallbackProvider.getToolCallbacks()).defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory())).build();}
}

使用 ChatClient 的时候,@Resource 注解会按 Bean 的名称注入

@Resource
private ChatClient openAiChatClient;@Resource
private ChatClient ollamaChatClient;@Resource
private ChatClient zhiPuAiChatClient;

基础对话

普通响应

使用 call 方法来调用大模型

private ChatClient openAiChatModel;@GetMapping("/chat")
public String chat(){Prompt prompt = new Prompt("你好,请介绍下你自己");String response = openAiChatModel.prompt(prompt).call().content();return response;
}

流式响应

call 方法修改为 stream,最终返回一个 Flux 对象

@GetMapping(value = "/chat/stream", produces = "text/html;charset=UTF-8")
public Flux<String> stream() {Prompt prompt = new Prompt("你好,请介绍下你自己");String response = openAiChatModel.prompt(prompt).stream().content();return response;
}

tips:我们可以通过缓存减少重复请求,提高性能。可以使用 Spring Cache 的 @Cacheable 注解实现:

@Cacheable("getChatResponse")
public String getChatResponse(String message){String response = openAiChatModel.prompt().user(message).call().content();return response;
}

tips: 适用于批量处理场景。可以使用 Spring 的 @Async 注解实现:

@Async
public CompletableFuture<String> getAsyncChatResponse(String message) {return CompletableFuture.supplyAsync(() -> openAiChatModel.prompt().user(message).call().content());
}

3 种组织提示词的方式

Prompt

通过 Prompt 来封装提示词实体,适用于简单场景

Prompt prompt = new Prompt("介绍下你自己");
PromptTemplate

使用提示词模板 PromptTemplate 来复用提示词,即将提示词的大体框架构建好,用户仅输入关键信息完善提示词

其中,{ } 作为占位符,promptTemplate.render 方法来填充

@GetMapping("/chat/formatPrompt")
public String formatPrompt(@RequestParam(value = "money") String money,@RequestParam(value = "number") String number,@RequestParam(value = "brand") String brand
) {PromptTemplate promptTemplate = new PromptTemplate("""根据我目前的经济情况{money},只推荐{number}部{brand}品牌的手机。""");Prompt prompt = new Prompt(promptTemplate.render(Map.of("money",money,"number", number, "brand", brand)));return openAiChatModel.prompt(prompt).call().content();
}
Message

使用 Message ,提前约定好大模型的功能或角色

消息类型:

系统消息(SystemMessage):设定对话的背景、规则或指令,引导 AI 的行为
用户消息(UserMessage):表示用户的输入,即用户向 AI 提出的问题或请求
助手消息(AssistantMessage):表示 AI 的回复,即模型生成的回答
工具响应消息(ToolResponseMessage):当 AI 调用外部工具(如 API)后,返回 工具的执行结果,供 AI 进一步处理
@GetMapping("/chat/messagePrompt")
public String messagePrompt(@RequestParam(value = "book", defaultValue = "《白夜行》") String book) {// 用户输入UserMessage userMessage = new UserMessage(book);log.info("userMessage: {}", userMessage);// 对系统的指令SystemMessage systemMessage = new SystemMessage("你是一个专业的评书人,给出你的评价吧!");log.info("systemMessage: {}", systemMessage);// 组合成完整的提示词,注意,只能是系统指令在前,用户消息在后,否则会报错Prompt prompt = new Prompt(List.of(systemMessage, userMessage));return openAiChatModel.prompt(prompt).call().content();
}
保存 prompt

prompt 不宜嵌入到代码中,可以将作为一个 .txt 文件 其保存到 src/main/resources/prompt 目录下,使用读取文件的工具类就可以读取到 prompt

package cn.onism.mcp.utils;import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;/*** @description: 读取文件内容的工具类* @date: 2025/5/8*/
@Component
public class FileContentReader {public String readFileContent(String filePath) {StringBuilder content = new StringBuilder();try {// 创建 ClassPathResource 对象以获取类路径下的资源ClassPathResource resource = new ClassPathResource(filePath);// 打开文件输入流InputStream inputStream = resource.getInputStream();// 创建 BufferedReader 用于读取文件内容BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));String line;// 逐行读取文件内容while ((line = reader.readLine()) != null) {content.append(line).append("\n");}// 关闭输入流reader.close();} catch (IOException e) {// 若读取文件时出现异常,打印异常信息e.printStackTrace();}return content.toString();}
}
PromptTemplate promptTemplate = new PromptTemplate(fileContentReader.readFileContent("prompt/formatPrompt.txt")
);

解析模型输出(结构化)

模型输出的格式是不固定的,无法直接解析或映射到 Java 对象,因此,Spring AI 通过在提示词中添加格式化指令要求大模型按特定格式返回内容,在拿到大模型输出数据后通过转换器做结构化输出。

实体类 Json 格式

首先我们定义一个实体类 ActorInfo

@Data
@Description("演员信息")
public class ActorInfo {@JsonPropertyDescription("演员姓名")private String name;@JsonPropertyDescription("演员年龄")private Integer age;@JsonPropertyDescription("演员性别")private String gender;@JsonPropertyDescription("演员出生日期")private String birthday;@JsonPropertyDescription("演员国籍")private String nationality;
}

在 call 方法后面调用 entity 方法,把对应实体类的 class 传递进去即能做到结构化输出

@GetMapping("/chat/actor")
public ActorInfo queryActorInfo(@RequestParam(value = "actorName") String actorName) {PromptTemplate promptTemplate = new PromptTemplate("查询{actorName}演员的详细信息");Prompt prompt = new Prompt(promptTemplate.render(Map.of("actorName", actorName)));ActorInfo response = openAiChatModel.prompt(prompt).call().entity(ActorInfo.class);return response;
}

结果符合要求

List 列表格式

在 entity 方法中传入 new ListOutputConverter(new DefaultConversionService())

@GetMapping("/chat/actorMovieList")
public List<String> queryActorMovieList(@RequestParam(value = "actorName") String actorName) {PromptTemplate promptTemplate = new PromptTemplate("查询{actorName}主演的电影");Prompt prompt = new Prompt(promptTemplate.render(Map.of("actorName", actorName)));List<String> response = openAiChatModel.prompt(prompt).call().entity(new ListOutputConverter(new DefaultConversionService()));return response;
}
Map 格式

tips: 目前在 Map 中暂不支持嵌套复杂类型,因此 Map 中不能返回实体类,而只能是 Object。

在 entity 方法中传入 new ParameterizedTypeReference<>() {}

@GetMapping("/chat/actor")
public Map<String, Object> queryActorInfo(@RequestParam(value = "actorName") String actorName) {PromptTemplate promptTemplate = new PromptTemplate("查询{actorName}演员及另外4名相关演员的详细信息");Prompt prompt = new Prompt(promptTemplate.render(Map.of("actorName", actorName)));Map<String, Object> response = openAiChatModel.prompt(prompt).call().entity(new ParameterizedTypeReference<>() {});return response;
}

多模态

deepseek 暂时不支持多模态,因此这里选用 智谱:https://bigmodel.cn/

依赖与配置

<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-zhipuai</artifactId><version>1.0.0-M6</version>
</dependency>
spring:ai:zhipuai:api-key: ${ZHIPUAI_API_KEY}chat:options:model: ${ZHIPUAI_MODEL}temperature: 0.8
# api-key
ZHIPUAI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxx
# 所选模型
ZHIPUAI_MODEL=glm-4v-plus-0111
理解图片

在 src/main/resources/images 目录下保存图片

创建一个 ZhiPuAiChatModel,将用户输入和图片封装成一个 UserMessage,然后使用 call 方法传入一个 prompt,最后获得输出

@Resource
private ZhiPuAiChatModel zhiPuAiChatModel;@GetMapping("/chat/pic")
public String pic() {Resource imageResource = new ClassPathResource("images/mcp.png");// 构造用户消息var userMessage = new UserMessage("解释一下你在这幅图中看到了什么?",new Media(MimeTypeUtils.IMAGE_PNG, imageResource));ChatResponse chatResponse = zhiPuAiChatModel.call(new Prompt(userMessage));return chatResponse.getResult().getOutput().getText();
}

文生图

这里需要使用 zhiPuAiImageModel,我们调用它的 call 方法,传入一个 ImagePrompt,ImagePrompt 由**用户图片描述输入 ImageMessage **和 **图片描述信息 OpenAiImageOptions **所构成,

其中,

  • model 要选择适用于图像生成任务的模型,这里我们选择了 cogview-4-250304
  • quality 为图像生成图像的质量,默认为 standard
    • hd : 生成更精细、细节更丰富的图像,整体一致性更高,耗时约20 秒
    • standard :快速生成图像,适合对生成速度有较高要求的场景,耗时约 5-10 秒
@Autowired
ZhiPuAiImageModel ziPuAiImageModel;@Autowired
private FileUtils fileUtils;@GetMapping("/outputImg")
public void outputImg() throws IOException {ImageMessage userMessage = new ImageMessage("一个仙人掌大象");OpenAiImageOptions chatOptions = OpenAiImageOptions.builder().model("cogview-4-250304").quality("hd").N(1).height(1024).width(1024).build();ImagePrompt prompt = new ImagePrompt(userMessage, chatOptions);// 调用ImageResponse imageResponse = ziPuAiImageModel.call(prompt);// 输出的图片Image image = imageResponse.getResult().getOutput();// 保存到本地InputStream in = new URL(image.getUrl()).openStream();fileUtils.saveStreamToFile(in,"src/main/resources/images", "pic"+RandomUtils.insecure().randomInt(0, 100)+".png");
}
@Component
public class FileUtils {public String saveStreamToFile(InputStream inputStream, String filePath, String fileName) throws IOException {// 创建目录(如果不存在)Path dirPath = Paths.get(filePath);if (!Files.exists(dirPath)) {Files.createDirectories(dirPath);}// 构建完整路径Path targetPath = dirPath.resolve(fileName);// 使用 try-with-resources 确保流关闭try (inputStream) {Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);}return targetPath.toAbsolutePath().toString();}
}

调用本地模型

_**tips: **_若想零成本调用大模型,并且保障隐私,可以阅读本章节

下载安装 Ollama

下载安装 Ollama:https://ollama.com/ [Ollama 是一个开源的大型语言模型服务工具,旨在帮助用户快速在本地运行大模型。通过简单的安装指令,用户可以通过一条命令轻松启动和运行开源的大型语言模型。Ollama 是 LLM 领域的 Docker],安装完成后执行 ollama 得到如下输出则表明安装成功

选择一个模型下载到本地:https://ollama.com/search,我这里选择了 qwen3:8b

引入 ollama 依赖

<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>

配置

spring:ai:ollama:chat:model: ${OLLAMA_MODEL}base-url: ${OLLAMA_BASE_URL}
# 本地模型
OLLAMA_MODEL=qwen3:8b
# URL
OLLAMA_BASE_URL=http://localhost:11434

实际调用

/*** ollama本地模型测试* @param input* @return*/
@GetMapping("/ollama/chat")
public String ollamaChat(@RequestParam(value = "input") String input) {Prompt prompt = new Prompt(input);return ollamaChatModel.call(prompt).getResult().getOutput().getText();
}

结果

对话记忆

内存记忆(短期)

MessageChatMemoryAdvisor 可以用来提供聊天记忆功能,这需要传递一个基于内存记忆的 ChatMemory

/*** 内存记忆/短期记忆* @param input* @return*/
@GetMapping("/memory/chat")
public String chat(@RequestParam(value = "input") String input) {Prompt prompt = new Prompt(input);// 内存记忆的 ChatMemoryInMemoryChatMemory inMemoryChatMemory = new InMemoryChatMemory();return openAiChatClient.prompt(prompt).advisors(new MessageChatMemoryAdvisor(inMemoryChatMemory)).call().content();
}

测试

隔离

多用户参与 AI 对话,每个用户的对话记录要做到互不干扰,因此要对不同用户的记忆按一定规则做好隔离。

由于在配置类中已经设置好了默认的 Advisors 为基于内存的聊天记忆 InMemoryChatMemory,因此,我们只需调用 openAiChatClient 的 advisors 方法,并设置好相应的参数即可

其中,

chat_memory_conversation_id 表示 会话 ID,用于区分不同用户的对话历史
chat_memory_response_size 表示每次最多检索 x 条对话历史
@Bean("openAiChatClient")
public ChatClient openAiChatClient() {return ChatClient.builder(openAiChatModel)// 默认加载所有的工具,避免重复 new.defaultTools(toolCallbackProvider.getToolCallbacks())// 设置记忆.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory())).build();
}
/**
* 短期记忆,按用户 ID 隔离
* @param input 
* @param userId
* @return
*/
@GetMapping("/memory/chat/user")
public String chatByUser(@RequestParam(value = "input") String input, @RequestParam(value = "userId") String userId) {Prompt prompt = new Prompt(input);return openAiChatClient.prompt(prompt)// 设置记忆的参数.advisors(advisor -> advisor.param("chat_memory_conversation_id", userId).param("chat_memory_response_size", 100)).call().content();
}

测试

集成 Redis

基于内存的聊天记忆可能无法满足开发者的需求,因此,我们可以构建一个基于 Redis 的长期记忆 RedisChatMemory

引入 Redis 依赖
<!-- redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yml 配置
spring:data:redis:host: localhostport: 6379password: xxxxx
Redis 配置类

创建了一个 RedisTemplate 实例

package cn.onism.mcp.config;import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;/*** @description: Redis配置类* @date: 2025/5/9*/
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(factory);redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));redisTemplate.afterPropertiesSet();return redisTemplate;}
}
定义消息实体类

用于存储对话的 ID、类型和内容,同时实现了序列化接口以便在 Redis 中存储

/*** @description: 聊天消息实体类* @date: 2025/5/9*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class ChatEntity implements Serializable {private static final long serialVersionUID = 1555L;/*** 聊天ID*/private String chatId;/*** 聊天类型*/private String type;/*** 聊天内容*/private String content;
}
构造 RedisChatMemory

创建一个 RedisChatMemory 并实现 ChatMemory 接口,重写该接口的 3 个方法

其中,

add表示添加聊天记录,conversationId 为会话 ID,messages 为消息列表
get表示获取聊天记录,lastN 表示获取最后 lastN 条聊天记录
clear表示清除聊天记录
package cn.onism.mcp.memory;import cn.onism.mcp.model.entity.ChatEntity;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.*;
import org.springframework.data.redis.core.RedisTemplate;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;/*** @description: 基于Redis的聊天记忆* @date: 2025/5/9*/
public class ChatRedisMemory implements ChatMemory {/*** 聊天记录的Redis key前缀*/private static final String KEY_PREFIX = "chat:history:";private final RedisTemplate<String, Object> redisTemplate;public ChatRedisMemory(RedisTemplate<String, Object> redisTemplate) {this.redisTemplate = redisTemplate;}/*** 添加聊天记录* @param conversationId* @param messages*/@Overridepublic void add(String conversationId, List<Message> messages) {String key = KEY_PREFIX + conversationId;List<ChatEntity> chatEntityList = new ArrayList<>();for (Message message : messages) {// 解析消息内容String[] strings = message.getText().split("</think>");String text = strings.length == 2 ? strings[1] : strings[0];// 构造聊天记录实体ChatEntity chatEntity = new ChatEntity();chatEntity.setChatId(conversationId);chatEntity.setType(message.getMessageType().getValue());chatEntity.setContent(text);chatEntityList.add(chatEntity);}// 保存聊天记录到Redis, 并设置过期时间为60分钟redisTemplate.opsForList().rightPushAll(key, chatEntityList.toArray());redisTemplate.expire(key, 60, TimeUnit.MINUTES);}/*** 获取聊天记录* @param conversationId* @param lastN* @return List<Message>*/@Overridepublic List<Message> get(String conversationId, int lastN) {String key = KEY_PREFIX + conversationId;Long size = redisTemplate.opsForList().size(key);if (size == null || size == 0) {return Collections.emptyList();}// 取最后lastN条聊天记录,如果聊天记录数量少于lastN,则取全部int start = Math.max(0, size.intValue() - lastN);List<Object> objectList = redisTemplate.opsForList().range(key, start, -1);List<Message> outputList = new ArrayList<>();// 解析聊天记录实体,并构造消息对象ObjectMapper mapper = new ObjectMapper();for (Object object : objectList){ChatEntity chatEntity = mapper.convertValue(object, ChatEntity.class);if(MessageType.USER.getValue().equals(chatEntity.getType())){outputList.add(new UserMessage(chatEntity.getContent()));}else if (MessageType.SYSTEM.getValue().equals(chatEntity.getType())){outputList.add(new SystemMessage(chatEntity.getContent()));}else if (MessageType.ASSISTANT.getValue().equals(chatEntity.getType())){outputList.add(new AssistantMessage(chatEntity.getContent()));}}return outputList;}/*** 清除聊天记录* @param conversationId*/@Overridepublic void clear(String conversationId) {String key = KEY_PREFIX + conversationId;redisTemplate.delete(key);}
}
更改 ChatClient 配置

将 MessageChatMemoryAdvisor 中的参数替换为我们实现的 ChatRedisMemory

@Resource
private RedisTemplate<String, Object> redisTemplate;@Bean("openAiChatClient")
public ChatClient openAiChatClient() {return ChatClient.builder(openAiChatModel)// 默认加载所有的工具,避免重复 new.defaultTools(toolCallbackProvider.getToolCallbacks()).defaultAdvisors(new MessageChatMemoryAdvisor(new ChatRedisMemory(redisTemplate))).build();
}
测试
/*** RedisChatMemory* @param input* @param userId* @return String*/
@GetMapping("/memory/chat/user")
public String chatByUser(@RequestParam(value = "input") String input, @RequestParam(value = "userId") String userId) {Prompt prompt = new Prompt(input);return openAiChatClient.prompt(prompt).advisors(advisor -> advisor.param("chat_memory_conversation_id", userId).param("chat_memory_response_size", 100)).call().content();
}

执行结果

可以看到,Redis 中有对应的记录,我们可以通过 lrange key start end 命令获取列表中的数据,其中 content 为 UTF-8 编码

Tool/Function Calling

工具(Tool)或功能调用(Function Calling)允许大型语言模型在必要时调用一个或多个可用的工具,这些工具通常由开发者定义。工具可以是任何东西:网页搜索、对外部 API 的调用,或特定代码的执行等。

下面是工具调用的流程图:

更加简洁的流程图:

  1. 工具注册阶段,当需要为模型提供工具支持时,需在聊天请求中声明工具定义。每个工具定义包含三个核心要素:工具名称(唯一标识符)、功能描述(自然语言说明)、输入参数结构(JSON Schema格式)
  2. 模型决策阶段,模型分析请求后,若决定调用工具,将返回结构化响应,包含:目标工具名称、符合预定义Schema的格式化参数
  3. 工具执行阶段,客户端应用负责根据工具名称定位具体实现,使用验证后的参数执行目标工具
  4. 工具响应阶段,工具执行结果返回给应用程序
  5. 重新组装阶段,应用将标准化处理后的执行结果返回给模型再次处理
  6. 结果响应阶段,模型结合用户初始输入以及工具执行结果再次加工返回给用户

工具定义与使用

Methods as Tools
  1. 注解式定义

创建一个 DateTimeTool 工具类,在 getCurrentDateTime 方法上使用 @Tool 注解,表示将该方法标记为一个 Tool,description 表示对工具的描述,大模型会根据这个描述来理解该工具的作用

@Component
public class DateTimeTool {private static final Logger LOGGER = LoggerFactory.getLogger(DateTimeTool.class);@Tool(description = "获取当前用户的日期和时间")public String getCurrentDateTime() {LOGGER.info("---------- getCurrentDateTime 工具被调用 ----------");return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();}
}

在使用时,可以在 ChatClient 配置类中将所有工具都提前加载到 ChatClient 中

@Configuration
public class ChatClientConfig {@Resourceprivate OpenAiChatModel openAiChatModel;@Resourceprivate ToolCallbackProvider toolCallbackProvider;@Resourceprivate RedisTemplate<String, Object> redisTemplate;@Bean("openAiChatClient")public ChatClient openAiChatClient() {return ChatClient.builder(openAiChatModel)// 默认加载所有的工具,避免重复 new.defaultTools(toolCallbackProvider.getToolCallbacks()).defaultAdvisors(new MessageChatMemoryAdvisor(new ChatRedisMemory(redisTemplate))).build();}
}

或者是不在配置类中加载所有工具,而是在调用 ChatClient 时将需要用到的工具传递进去,即使用 tools 方法,传入工具类

@GetMapping("/tool/chat")
public String toolChat(@RequestParam(value = "input") String input) {Prompt prompt = new Prompt(input);return openAiChatClient.prompt(prompt).tools(new DateTimeTool()).call().content();
}

测试后发现大模型的确调用了 DateTimeTool

  1. 编程式定义

我们可以不使用 @Tool 注解,而是采用编程式的方式构造一个 Tool

@Component
public class DateTimeTool {private static final Logger LOGGER = LoggerFactory.getLogger(DateTimeTool.class);// no annotationpublic String getCurrentDateTime() {LOGGER.info("---------- getCurrentDateTime 工具被调用 ----------");return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();}
}

首先通过反射获取方法,然后定义一个 ToolDefinition,最后创建一个 MethodToolCallback,将其传入到 tools 方法中即可

@GetMapping("/tool/chat")
public String toolChat(@RequestParam(value = "input") String input) {Prompt prompt = new Prompt(input);// 通过反射获取方法Method method = ReflectionUtils.findMethod(DateTimeTool.class, "getCurrentDateTime");// 工具定义ToolDefinition toolDefinition = ToolDefinition.builder(method).description("获取当前用户的日期和时间").build();// 创建一个 MethodToolCallbackMethodToolCallback methodToolCallback = MethodToolCallback.builder().toolDefinition(toolDefinition).toolMethod(method).toolObject(new DateTimeTool()).build();return openAiChatClient.prompt(prompt).tools(methodToolCallback).call().content();
}
Fuctions as Tools

除方法外,Function、Supplier、Consumer 等函数式接口也可以定义为 Tool

下面**模拟一个查询天气的服务,首先定义 WeatherRequestWeatherResponse**

其中,@ToolParam 注解用于定义工具所需参数, description 为工具参数的描述,模型通过描述可以更好的理解参数的作用

/*** 天气查询请求参数*/
@Data
public class WeatherRequest {/*** 坐标*/@ToolParam(description = "经纬度,精确到小数点后4位,格式为:经度,纬度")String location;}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WeatherResponse {/*** 温度*/private double temp;/*** 单位*/private Unit unit;
}
/*** 温度单位*/
public enum Unit {C, F
}

接下来创建一个 WeatherService,实现 Function 接口,编写具体逻辑。这里获取天气使用的是彩云科技开放平台提供的免费的 API 接口:https://docs.caiyunapp.com/weather-api/,构造好请求后使用 HttpURLConnection 发送请求,读取响应后使用 Jackson 解析 JSON,获取天气数据。

package cn.onism.mcp.tool.service;import cn.onism.mcp.tool.Unit;
import cn.onism.mcp.tool.WeatherRequest;
import cn.onism.mcp.tool.WeatherResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;/*** @description: 天气服务* @date: 2025/5/9*/
@Slf4j
@Component
public class WeatherService implements Function<WeatherRequest, WeatherResponse> {private static final Logger LOGGER = LoggerFactory.getLogger(WeatherService.class);private static final String TOEKN = "xxxxxxxxxxxxxxxxxx";/*** 实时天气接口*/private static final String API_URL = "https://api.caiyunapp.com/v2.6/%s/%s/realtime";private double temp;private String skycon;@Overridepublic WeatherResponse apply(WeatherRequest weatherRequest) {LOGGER.info("Using caiyun api, getting weather information...");try {// 构造API请求String location = weatherRequest.getLocation();String encodedLocation = URLEncoder.encode(location, StandardCharsets.UTF_8);String apiUrl = String.format(API_URL,TOEKN,encodedLocation);URL url = new URL(apiUrl);// 发送请求HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("GET");// 读取响应int responseCode = connection.getResponseCode();if (responseCode == HttpURLConnection.HTTP_OK) {BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));String inputLine;StringBuilder response = new StringBuilder();while ((inputLine = in.readLine()) != null) {response.append(inputLine);}in.close();// 使用Jackson解析JSONObjectMapper objectMapper = new ObjectMapper();JsonNode rootNode = objectMapper.readTree(response.toString());// 获取天气数据JsonNode resultNode = rootNode.get("result");LOGGER.info("获取到天气信息: " + resultNode.toString());temp = resultNode.get("realtime").get("temperature").asDouble();skycon = resultNode.get("realtime").get("skycon").asText();} else {System.out.println("请求失败,响应码为: " + responseCode);}} catch (IOException e) {e.printStackTrace();}return new WeatherResponse(temp, skycon, Unit.C);}
}

创建一个 WeatherTool 工具类,定义一个 Bean,Bean 名称为工具名称,@Description 中描述工具作用,该 Bean 调用了 WeatherService 中的方法

package cn.onism.mcp.tool;import cn.onism.mcp.tool.service.WeatherService;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Description;
import org.springframework.stereotype.Component;import java.util.function.Function;/*** @description: 天气工具类* @date: 2025/5/9*/@Slf4j
@Component
public class WeatherTool {private final WeatherService weatherService = new WeatherService();@Bean(name = "currentWeather")@Description("依据位置获取天气信息")public Function<WeatherRequest, WeatherResponse> currentWeather() {return weatherService::apply;}
}

将天气工具和日期工具传入 tools 方法中

@GetMapping("/tool/weather")
public String toolFunctionAnnotation(@RequestParam(value = "input") String input) {Prompt prompt = new Prompt(input);return openAiChatClient.prompt(prompt).tools("currentWeather","getCurrentDateTime").call().content();
}

测试

可以看到,大模型首先会调用日期工具获取时间,同时,我们向大模型询问的地点会被自动解析为 location 经纬度参数,接着调用天气工具获取天气信息

在之前的流程图中,工具的调用会与大模型进行 2 次交互,第一次为发起请求,第二次在工具执行完成后带着工具执行的结果再次调用大模型,而某些情况下,我们想在工具执行完成后直接返回,而不去调用大模型。在 @Tool 注解中令 returnDirect = true 即可

MCP

首先来看这张经典的图,MCP(Model Context Protocol 模型上下文协议可以被视为 AI 应用的 USB-C 端口,它为 AI 模型/应用不同数据源/工具建立了统一对接规范,旨在标准化应用程序向大语言模型提供上下文的交互方式。

MCP 采用客户端-服务器架构,

其中,

- **<font style="color:rgb(25, 27, 31);">MCP Hosts(宿主程序):</font>**<font style="color:rgb(25, 27, 31);">如 Claude Desktop、IDE 等,通过 MCP 访问数据</font>
- **<font style="color:rgb(25, 27, 31);">MCP Clients(客户端):</font>**<font style="color:rgb(25, 27, 31);">与服务器建立 1:1 连接,处理通信</font>
- **<font style="color:rgb(25, 27, 31);">MCP Servers(服务端):</font>**<font style="color:rgb(25, 27, 31);">轻量级程序,提供标准化的数据或工具访问能力</font>
- **<font style="color:rgb(25, 27, 31);">Local Data Sources(本地数据源):</font>**<font style="color:rgb(25, 27, 31);">如文件、数据库等,由 MCP 服务端安全访问</font>
- **<font style="color:rgb(25, 27, 31);">Remote Services(远程服务):</font>**<font style="color:rgb(25, 27, 31);">如 API、云服务等,MCP 服务端可代理访问</font>

RAG

Aegnt

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/80297.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c/c++日志库初识

C/C日志库&#xff1a;从入门到实践的深度指南 在软件开发的世界里&#xff0c;日志&#xff08;Logging&#xff09;扮演着一个沉默却至关重要的角色。它像是飞行记录仪的“黑匣子”&#xff0c;记录着应用程序运行时的关键信息&#xff0c;帮助开发者在问题发生时追溯根源&a…

C 语言图形编程 | 界面 / 动画 / 字符特效

注&#xff1a;本文为 “C 语言图形编程” 相关文章合辑。 略作重排&#xff0c;如有内容异常&#xff0c;请看原文。 C 语言图形化界面——含图形、按钮、鼠标、进度条等部件制作&#xff08;带详细代码、讲解及注释&#xff09; 非线性光学元件于 2020-02-15 09:42:37 发布…

开发狂飙VS稳定刹车:Utility Tree如何让架构决策“快而不失控”

大家好&#xff0c;我是沛哥儿。 在软件技术架构的世界里&#xff0c;架构师们常常面临灵魂拷问&#xff1a;高并发和低成本哪个优先级更高&#xff1f; 功能迭代速度和系统稳定性该如何平衡&#xff1f; 当多个质量属性相互冲突时&#xff0c;该如何做出科学决策&#xff1f; …

SCI论文图数据提取软件——GetData Graph Digitizer

在写综述或者毕业论文的时候一般会引用前人的文献数据图&#xff0c;但是直接截图获取来的数据图通常质量都不太高。因此我们需要从新画一张图&#xff0c;可以通过origin绘图来实现&#xff0c;今天介绍一个新的软件GetData Graph Digitizer 感谢下面博主分享的破解安装教程 …

深入探索 Apache Spark:从初识到集群运行原理

深入探索 Apache Spark&#xff1a;从初识到集群运行原理 在当今大数据时代&#xff0c;数据如同奔涌的河流&#xff0c;蕴藏着巨大的价值。如何高效地处理和分析这些海量数据&#xff0c;成为各行各业关注的焦点。Apache Spark 正是为此而生的强大引擎&#xff0c;它以其卓越…

场景可视化与数据编辑器:构建数据应用情境​

场景可视化是将数据与特定的应用场景相结合&#xff0c;借助数据编辑器对数据进行灵活处理和调整&#xff0c;通过模拟和展示真实场景&#xff0c;使企业能够更直观地理解数据在实际业务中的应用和影响&#xff0c;为企业的决策和运营提供有力支持。它能够将抽象的数据转化为具…

攻防世界-php伪协议和文件包含

fileinclude 可以看到正常回显里面显示lan参数有cookie值表示为language 然后进行一个判断&#xff0c;如果参数不是等于英语&#xff0c;就加上.php&#xff0c;那我们就可以在前面进行注入一个参数&#xff0c;即flag&#xff0c; payload&#xff1a;COOKIE:languageflag …

手撕LFU

博主介绍&#xff1a;程序喵大人 35- 资深C/C/Rust/Android/iOS客户端开发10年大厂工作经验嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手《C20高级编程》《C23高级编程》等多本书籍著译者更多原创精品文章&#xff0c;首发gzh&#xff0c;见文末&#x1f447;&#x1f…

火影bug,未保证短时间数据一致性,拿这个例子讲一下Redis

本文只拿这个游戏的bug来举例Redis&#xff0c;如果有不妥的地方&#xff0c;联系我进行删除 描述&#xff1a;今天在高速上打火影&#xff08;有隧道&#xff0c;有时候会卡&#xff09;&#xff0c;发现了个bug&#xff0c;我点了两次-1000的忍玉&#xff08;大概用了1千七百…

KRaft (Kafka 4.0) 集群配置指南(超简单,脱离 ZooKeeper 集群)还包含了简化测试指令的脚本!!!

docker-compose方式部署kafka集群 Kafka 4.0 引入了 KRaft 模式&#xff08;Kafka Raft Metadata Mode&#xff09;&#xff0c;它使 Kafka 集群不再依赖 ZooKeeper 进行元数据管理。KRaft 模式简化了 Kafka 部署和管理&#xff0c;不需要额外配置 ZooKeeper 服务&#xff0c;…

Admyral - 可扩展的GRC工程自动化平台

文章目录 一、关于 Admyral相关链接资源关键特性 二、安装系统要求 三、快速开始1、启动服务 四、核心功能1、自动化即代码2、AI增强工作流3、双向同步编辑器4、工作流监控5、企业级基础设施 五、示例应用六、其他信息许可证遥测说明 一、关于 Admyral Admyral 是一个基于 Pyt…

DDR在PCB布局布线时的注意事项及设计要点

一、布局注意事项 控制器与DDR颗粒的布局 靠近原则&#xff1a;控制器与DDR颗粒应尽量靠近&#xff0c;缩短时钟&#xff08;CLK&#xff09;、地址/控制线&#xff08;CA&#xff09;、数据线&#xff08;DQ/DQS&#xff09;的走线长度&#xff0c;减少信号延迟差异。 分组隔…

计算机网络-LDP工作过程详解

前面我们已经学习了LDP的基础概念&#xff0c;了解了LDP会话的建立、LDP的标签控制等知识&#xff0c;今天来整体过一遍LDP的一个工作过程&#xff0c;后面我们再通过实验深入学习。 一、LDP标签分发 标签分发需要基于基础的路由协议建立LDP会话&#xff0c;激活MPLS和LDP。以…

解构与重构:自动化测试框架的进阶认知之旅

目录 一、自动化测试的介绍 &#xff08;一&#xff09;自动化测试的起源与发展 &#xff08;二&#xff09;自动化测试的定义与目标 &#xff08;三&#xff09;自动化测试的适用场景 二、什么是自动化测试框架 &#xff08;一&#xff09;自动化测试框架的定义 &#x…

跑不出的循环 | LoveySelf 系列定位

最近开始陷入一轮一轮的循环状态&#xff0c;无奈&#xff0c;只能自我整理一下。23年暑假&#xff0c;在计算机系折腾了一年后&#xff0c;重新打开博客&#xff0c;回想在数学系摸索博客写作的日子&#xff0c;思绪涌上心头&#xff0c;我们决定拾起这份力量。当时觉得 hexo …

Redis最新入门教程

文章目录 Redis最新入门教程1.安装Redis2.连接Redis3.Redis环境变量配置4.入门Redis4.1 Redis的数据结构4.2 Redis的Key4.3 Redis-String4.4 Redis-Hash4.5 Redis-List4.6 Redis-Set4.7 Redis-Zset 5.在Java中使用Redis6.缓存雪崩、击穿、穿透6.1 缓存雪崩6.2 缓冲击穿6.3 缓冲…

一文读懂Python之requests模块(36)

一、requests模块简介 requests模块是python中原生的一款基于网络请求的模块&#xff0c;功能强大&#xff0c;简单便捷且高效 &#xff0c;该模块可以模拟浏览器发送请求&#xff0c;主要包括指定url、发起请求、获取响应数据和持久化存储&#xff0c;包括 GET、POST、PUT、…

WPF之布局流程

文章目录 1. 概述2. 布局元素的边界框3. 布局系统原理3.1 布局流程时序图 4. 测量阶段(Measure Phase)4.1 测量过程4.2 MeasureOverride方法 5. 排列阶段(Arrange Phase)5.1 排列过程5.2 ArrangeOverride方法 6. 渲染阶段(Render Phase)7. 布局事件7.1 主要布局事件7.2 布局事件…

uniapp|获取当前用户定位、与系统设定位置计算相隔米数、实现打卡签到(可自定义设定位置、位置有效范围米数)

基于UniApp阐述移动应用开发中定位功能的实现全流程,涵盖实时定位获取、动态距离计算与自定义位置、有效范围设定等功能。文章提供完整的代码示例与适配方案,适用于社交签到、课堂教室打卡等场景。 目录 引言定位功能在移动应用中的价值(社交、导航、O2O等场景)UniApp跨平台…

Yii2.0 模型规则(rules)详解

一、基本语法结构 public function rules() {return [// 规则1[[attribute1, attribute2], validator, options > value, ...],// 规则2[attribute, validator, options > value, ...],// 规则3...]; }二、规则类型分类 1、核心验证器&#xff08;内置验证器&#xff0…