Spring AI 入门(持续更新)

介绍

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

依赖

xml 复制代码
<!-- 基于 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 模型类型

bash 复制代码
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/

plain 复制代码
# 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。

java 复制代码
spring:
  ai:
    chat:
      client:
        enabled: false

配置类

java 复制代码
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 {

    @Resource
    private OpenAiChatModel openAiChatModel;

    @Resource
    private ZhiPuAiChatModel zhiPuAiChatModel;

    @Resource
    private OllamaChatModel ollamaChatModel;

    @Resource
    private 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 的名称注入

java 复制代码
@Resource
private ChatClient openAiChatClient;

@Resource
private ChatClient ollamaChatClient;

@Resource
private ChatClient zhiPuAiChatClient;

基础对话

普通响应

使用 call 方法来调用大模型

java 复制代码
private ChatClient openAiChatModel;

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

流式响应

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

java 复制代码
@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 注解实现:

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

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

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

3 种组织提示词的方式

Prompt

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

java 复制代码
Prompt prompt = new Prompt("介绍下你自己");
PromptTemplate

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

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

java 复制代码
@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 进一步处理
java 复制代码
@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

java 复制代码
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();
    }
}
java 复制代码
PromptTemplate promptTemplate = new PromptTemplate(
    fileContentReader.readFileContent("prompt/formatPrompt.txt")
);

解析模型输出(结构化)

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

实体类 Json 格式

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

java 复制代码
@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 传递进去即能做到结构化输出

java 复制代码
@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())

java 复制代码
@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<>() {}

java 复制代码
@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/

依赖与配置

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

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

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

java 复制代码
@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 秒
java 复制代码
@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"
                              );
}
java 复制代码
@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

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

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

引入 ollama 依赖

java 复制代码
<dependency>
	<groupId>org.springframework.ai</groupId>
	<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>

配置

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

实际调用

java 复制代码
/**
 * 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

java 复制代码
/**
 * 内存记忆/短期记忆
 * @param input
 * @return
 */
@GetMapping("/memory/chat")
public String chat(@RequestParam(value = "input") String input) {
    Prompt prompt = new Prompt(input);
    // 内存记忆的 ChatMemory
    InMemoryChatMemory 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 条对话历史
java 复制代码
@Bean("openAiChatClient")
public ChatClient openAiChatClient() {
    return ChatClient.builder(openAiChatModel)
    // 默认加载所有的工具,避免重复 new
    .defaultTools(toolCallbackProvider.getToolCallbacks())
    // 设置记忆
    .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
    .build();
}
java 复制代码
/**
* 短期记忆,按用户 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 依赖
java 复制代码
<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yml 配置
java 复制代码
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: xxxxx
Redis 配置类

创建了一个 RedisTemplate 实例

java 复制代码
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 {

    @Bean
    public 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 中存储

java 复制代码
/**
 * @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表示清除聊天记录
java 复制代码
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
     */
    @Override
    public 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>
     */
    @Override
    public 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
     */
    @Override
    public void clear(String conversationId) {
        String key = KEY_PREFIX + conversationId;
        redisTemplate.delete(key);
    }
}
更改 ChatClient 配置

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

java 复制代码
@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();
}
测试
java 复制代码
/**
 * 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 表示对工具的描述,大模型会根据这个描述来理解该工具的作用

java 复制代码
@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 中

java 复制代码
@Configuration
public class ChatClientConfig {

    @Resource
    private OpenAiChatModel openAiChatModel;

    @Resource
    private ToolCallbackProvider toolCallbackProvider;

    @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();
    }
}

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

java 复制代码
@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

java 复制代码
@Component
public class DateTimeTool {

    private static final Logger LOGGER = LoggerFactory.getLogger(DateTimeTool.class);

    // no annotation
    public String getCurrentDateTime() {
        LOGGER.info("---------- getCurrentDateTime 工具被调用 ----------");

        return LocalDateTime.now()
        .atZone(LocaleContextHolder.getTimeZone().toZoneId())
        .toString();
    }
}

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

java 复制代码
@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();
    
    // 创建一个 MethodToolCallback
    MethodToolCallback 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 为工具参数的描述,模型通过描述可以更好的理解参数的作用

java 复制代码
/**
 * 天气查询请求参数
 */
@Data
public class WeatherRequest {
    /**
     * 坐标
     */
    @ToolParam(description = "经纬度,精确到小数点后4位,格式为:经度,纬度")
    String location;

}
java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WeatherResponse {
    /**
     * 温度
     */
    private double temp;
    /**
     * 单位
     */
    private Unit unit;
}
java 复制代码
/**
 * 温度单位
 */
public enum Unit {
    C, F
}

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

java 复制代码
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;

    @Override
    public 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解析JSON
                ObjectMapper 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 中的方法

java 复制代码
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 方法中

java 复制代码
@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

相关推荐
智驱力人工智能2 分钟前
高密爆炸警钟长鸣:AI为化工安全戴上“智能护盾”
人工智能·算法·安全·重构·边缘计算·高密爆炸·高密化工厂
元闰子13 分钟前
AI Agent需要什么样的数据库?
数据库·人工智能·后端
蚂蚁数据AntData14 分钟前
⼤模型驱动的DeepInsight Copilot在蚂蚁的技术实践
大数据·人工智能·数据分析·copilot·数据库架构
LeonDL16816 分钟前
HALCON 深度学习训练 3D 图像的几种方式优缺点
人工智能·python·深度学习·3d·halcon·halcon训练3d图像·深度学习训练3d图像
jmsail17 分钟前
Dynamics 365 Business Central AI Sales Order Agent Copilot
人工智能·microsoft·copilot·dynamics 365·d365 bc erp
知初~18 分钟前
SpringCloud
后端·spring·spring cloud
果壳~22 分钟前
【Java】mybatis-plus乐观锁与Spring重试机制
java·spring·mybatis
要养家的程序猿33 分钟前
RagFlow优化&代码解析(一)
人工智能·ai
MARSERERER39 分钟前
Starrocks Full GC日志分析
java