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>

配置文件

在 yml 中配置大模型的 API Key 模型类型

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

在 yml 配置文件的同目录下创建一个 .env 文件,配置以下内容。这里使用的是 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 {

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

@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

下载安装 Ollamahttps://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);
    // 内存记忆的 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 条对话历史
@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: localhost
      port: 6379
      password: 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 {

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

/**
 * @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
     */
    @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

@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 {

    @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 方法,传入工具类

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

/**
 * 天气查询请求参数
 */
@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;

    @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 中的方法

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 采用客户端-服务器架构,

其中,

  1. MCP Hosts(宿主程序):如 Claude Desktop、IDE 等,通过 MCP 访问数据
  2. MCP Clients(客户端):与服务器建立 1:1 连接,处理通信
  3. MCP Servers(服务端):轻量级程序,提供标准化的数据或工具访问能力
  4. Local Data Sources(本地数据源):如文件、数据库等,由 MCP 服务端安全访问
  5. Remote Services(远程服务):如API、云服务等,MCP 服务端可代理访问


好的,你已经知道了 MCP Servers 就是展示和调用工具的地方,而 MCP Clients 就是用户与各种大模型/AI应用对话的地方

假设,我们的 MCP Servers 提供了 2 个简单的工具,分别是获取日期、获取天气。那么 MCP Servers 启动成功之后,MCP Clients 就可以获取到这两个工具。

我们在 MCP Clients 向 AI 询问 今天是星期几? 接下来, MCP Clients 会首先使用模型的 Function call 能力,由大模型决定是否使用工具,以及使用哪个工具。随后,MCP Clients 把确定要使用的工具和参数发送回 MCP Servers,由 MCP Servers 实现工具调用并返回结果。最后,MCP Clients 根据返回结果,再次调用大模型,由大模型进行回答。

你可能会问,这和工具调用有什么区别?这个流程和 Function/Tool call 不是一样嘛……
在这里插入图片描述
确实,MCP 只是一个协议,就像后端开发中的 RESTful API ,基本上就是一组接口约定,通过固定的模式来理解每个工具的作用和参数,实际上并没有新的技术。 而且实际使用过程中,会产生巨量的token 消耗。

并且,在真实的生产环境中,工具数量可能会非常多,一些工具之间的界限可能并没有那么清晰,模型也会出现调用错误的情况。此外,若一个工具需要传入多个参数,那么模型提取参数可能不是那么准确。

但是,作为一个协议,它的出发点是好的。

了解更多,请阅读 MCP 中文文档:https://mcplab.cc/zh/docs/getstarted


MCP Client/Server

从下面这幅图可以看到,通过 Spring AI,我们可以自己实现一个 MCP Client 和 MCP Server,并通过 MCP 来连接 MCP Server。

MCP 有两种通信模式:stdio、sse。

stdio(标准输入/输出)通过本地进程间通信实现,客户端以子进程形式启动服务器,双方通过stdin/stdout 交换 JSON-RPC 消息,每条消息以换行符分隔。适用场景:本地工具集成、隐私数据处理、快速原型开发。

SSE 又分为 Spring MVC (Server-Sent Events) 和 Spring WebFlux (Reactive SSE) 。

1. <font style="color:rgb(77, 77, 77);">Spring MVC 的 SSE </font><font style="color:rgb(51, 51, 51);">适合传统的基于 Servlet 的 Web 应用程序,能够与现有的 Spring M</font>VC 项目无缝集成,支持**同步模式**,适合传统的请求-响应模式。
2. <font style="color:rgb(77, 77, 77);">Spring WebFlux 的 SSE </font><font style="color:rgb(51, 51, 51);">适合需要高性能、非阻塞 I/O 的现代响应式应用,特别是在处理大量并发连接时表现出色</font>,支持异步模式,适合需要高并发和低延迟的应用。

MCP Server 开发
基于 stdio 形式是将 MCP Server 当做一个本地的子进程,基于 sse 可将 MCP Server 部署在远端,各有千秋
stdio

引入 **mcp-server **依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server</artifactId>
    <version>1.0.0-M7</version>
</dependency>

定义一个工具,这里模拟获取某地区新闻头条

/**
 * @description:
 * @date: 2025/5/10
 */
@Service
public class NewsService {

    @Tool(description = "获取某地区新闻头条")
    public String getNewsTop(String location) {
        return "今日" + location + "地区的新闻头条是: " + location + "申奥成功";
    }
}

在配置类中将工具加载到 ToolCallbackProvider 中

/**
 * @description:
 * @date: 2025/5/10
 */
@Configuration
public class ToolConfiguration {

    @Bean
    public ToolCallbackProvider weatherTools(NewsService newsService) {
        return MethodToolCallbackProvider
        .builder()
        .toolObjects(newsService)
        .build();
    }
}

接下来在 yml 文件中配置如下内容,含义见注释:

spring:
  main:
    # 禁用 web 应用类型
    web-application-type: none
    # 禁用 banner
    banner-mode: off
  ai:
    mcp:
      server:
        # mcp 服务名称
        name: mcp-stdio-news
        # mcp 服务版本
        version: 1.0.0
        # mcp 通信模式为 stdio
        stdio: true
  application:
    name: mcp-stdio-demo
# 日志
logging:
  file:
    name: mcp-stdio-demo.log

接下来打包,在 target 目录中获取 jar 包路径

编写 mcp-servers-config.json 文件,该文件可以在任何 MCP Client 平台中导入

其中:

mcpServers” 中可以有多个 MCP Server 配置,这里只写了一个

mcp-stdio-news” 表示 MCP Server 的名称,随便取

command” 表示命令

args” 表示命令中的参数

“E:/Java-Projects/mcp-server-demo/target/mcp-server-demo-0.0.1-SNAPSHOT.jar” 表示 jar 包的路径

{
  "mcpServers": {
    "mcp-stdio-news": {
      "command": "java",
      "args": [
        "-Dspring.ai.mcp.server.stdio=true",
        "-Dspring.main.web-application-type=none",
        "-Dlogging.pattern.console=",
        "-jar",
        "E:/Java-Projects/mcp-server-demo/target/mcp-server-demo-0.0.1-SNAPSHOT.jar"
      ]
    }
  }
}

为了测试,我们使用 Spring AI 来实现一个 MCP Client,具体见 MCP Client 开发的 stdio 章节

sse

相较于 stdio 方式,sse 更适用于远程部署的 MCP 服务器,客户端可以通过标准 HTTP 协议与服务器建立连接,实现单向的实时数据推送。

引入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
  <version>1.0.0-M7</version>
</dependency>

接下来在 yml 文件中配置如下内容

spring:
  ai:
    mcp:
      server:
        name: mcp-sse-server
        version: 1.0.0
  application:
    name: mcp-server
server:
  port: 8090

增加配置类

@Configuration
public class McpConfig implements WebMvcConfigurer {
    @Bean
    public WebMvcSseServerTransportProvider transportProvider(ObjectMapper mapper) {
        return new WebMvcSseServerTransportProvider(mapper, "/mcp"); // 基础路径设为/mcp
    }

    @Bean
    public RouterFunction<ServerResponse> mcpRouterFunction(
        WebMvcSseServerTransportProvider transportProvider) {
        return transportProvider.getRouterFunction();
    }

}

下面的步骤和 stdio 一样

定义一个工具,这里模拟获取某地区新闻头条

/**
 * @description:
 * @date: 2025/5/10
 */
@Service
public class NewsService {

    @Tool(description = "获取某地区新闻头条")
    public String getNewsTop(String location) {
        return "今日" + location + "地区的新闻头条是: " + location + "申奥成功";
    }
}

在配置类中将工具加载到 ToolCallbackProvider 中

/**
 * @description:
 * @date: 2025/5/10
 */
@Configuration
public class ToolConfiguration {

    @Bean
    public ToolCallbackProvider weatherTools(NewsService newsService) {
        return MethodToolCallbackProvider
        .builder()
        .toolObjects(newsService)
        .build();
    }
}

最后直接运行,不用打包和编写 json 文件,为了测试,我们使用 Spring AI 来实现一个 MCP Client,具体见 MCP Client 开发的 sse 章节

MCP Client 开发
stdio

引入以下依赖

<!--  mcp-client -->
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-mcp-client</artifactId>
  <version>1.0.0-M7</version>
</dependency>
<!--  spring-ai-openai -->
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
  <version>1.0.0-M6</version>
</dependency>
<!--  web -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

编写 yml 配置文件,这里还是使用 deepseek 的 API,其中的 servers-configuration 为 MCP Server 提供的 json 文件,我们将其放在 resources 目录下即可,toolcallback 设置为 true 表示启用工具回调功能

spring:
  ai:
    openai:
      base-url: xxxxxxxxxxxx
      api-key: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxx
      chat:
        options:
          model: deepseek-chat
          temperature: 0.8
    mcp:
      client:
        stdio:
          servers-configuration: classpath:/mcp-servers-config.json
        toolcallback:
          enabled: true
        request-timeout: 60000
server:
  port: 8085

编写配置类

/**
 * @description:
 * @date: 2025/5/10
 */
@Configuration
public class ChatClientConfig {


    @Resource
    private OpenAiChatModel openAiChatModel;

    @Bean("openAiChatClient")
    public ChatClient openAiChatClient(ToolCallbackProvider toolCallbackProvider) {
        return ChatClient.builder(openAiChatModel)
        .defaultTools(toolCallbackProvider)
        .build();
    }
}

进行测试,可以看到,Client 端的确调用到了 Server 端的工具

/**
 * @description:
 * @date: 2025/5/10
 */
@RestController
public class MCPController {

    @Resource
    private ChatClient openAiChatClient;

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

sse

引入依赖

<dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
   <version>1.0.0-M6</version>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
  <version>1.0.0-M6</version>
</dependency>

在 yml 文件中配置 MCP 服务器,其中,sse.connections.server1.url 填写 MCP 服务器的地址;news-server 为自定义的服务名

spring:
  ai:
    openai:
      base-url: https://api.deepseek.com
      api-key: sk-0e48a66aef34434489ce9710ba45f8e2
      chat:
        options:
          model: deepseek-chat
          temperature: 0.8
    mcp:
      client:
        toolcallback:
          enabled: true
        name: mcp-sse-client
        version: 1.0.0
        request-timeout: 20s
        type: ASYNC
        sse:
          connections:
            news-server:
              url: http://localhost:8090
server:
  port: 8087

编写配置类

/**
 * @description:
 * @date: 2025/5/10
 */
@Configuration
public class ChatClientConfig {


    @Resource
    private OpenAiChatModel openAiChatModel;

    @Bean("openAiChatClient")
    public ChatClient openAiChatClient(ToolCallbackProvider toolCallbackProvider) {
        return ChatClient.builder(openAiChatModel)
        .defaultTools(toolCallbackProvider)
        .build();
    }
}

进行测试,可以看到,Client 端的确调用到了 Server 端的工具

/**
 * @description:
 * @date: 2025/5/10
 */
@RestController
public class MCPController {

    @Resource
    private ChatClient openAiChatClient;

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

MCP 操作文件系统

MCP 操作数据库

Dify 结合 MCP

进入 Dify 工作室:https://cloud.dify.ai/apps,创建一个 Chatflow

添加 Agent 节点

选择支持 MCP工具的 Agent 策略,若没有该选项,请前往 https://marketplace.dify.ai/ 中安装 MCP 插件

选择模型,进行 MCP 服务配置,注意,本地启动的 MCP Server 无法进行被访问到,需要在云服务器上部署好 MCP Server,详情请见 简易部署 MCP Server 章节

将开始节点和直接回复节点与 Agent 节点相连

为了提高 Agent 调用工具的准确性,可以编写指令,当然 1. 若包含地区,则提取地区,调用工具”getNewsTop“ 删掉也能成功地调用相应工具。

查询中,添加开始节点中的 sys.query,最大迭代次数可以设置为 2

点击预览,进行聊天,发现的确调用了相关工具

简易部署 MCP Server

RAG

检索增强生成(Retrieval-Augmented Generation, RAG),是一种结合了语言模型和信息检索的技术,其大致流程如下:

  1. 文档预处理与向量化存储:将生产数据处理为文档 Documents,用嵌入模型(如 OpenAI text-embedding、BERT 等)转为向量并存入向量数据库(如 Milvus、Faiss),保留语义。
  2. 用户查询向量化:用与文档处理相同的嵌入模型,将用户自然语言查询转为向量,确保向量空间一致。
  3. 语义检索:计算用户查询向量与库中文档向量的余弦相似度,返回相似度最高的 Top K 文档。
  4. 生成 LLM 输入:将用户查询和检索到的文档按模板合并,形成 LLM 的输入提示。
  5. 大模型输出与后处理:LLM 基于输入生成回答,减少幻觉 API 对输出格式化后返回结构化响应给用户。

下面我们按流程进行演示

文档预处理与向量化存储

文档提取

首先,我们要将生产数据(文本TXT、JSON、PDF、DOCX、Markdown、HTML、数据库等)处理为 Document

Spring AI 提供了 DocumentReader 来将生产数据转换为 Document,它的实现类有:

- **<font style="color:rgb(77, 77, 77);">JsonReader</font>**<font style="color:rgb(77, 77, 77);">:读取 JSON 格式的文件</font>
- **<font style="color:rgb(77, 77, 77);">TextReader</font>**<font style="color:rgb(77, 77, 77);">:读取 txt 文件</font>
- **<font style="color:rgb(77, 77, 77);">PagePdfDocumentReader</font>**<font style="color:rgb(77, 77, 77);">:使用 Apache PdfBox 读取 PDF 文件</font>
- **<font style="color:rgb(77, 77, 77);">TikaDocumentReader</font>**<font style="color:rgb(77, 77, 77);">:使用 Apache Tika 来读取 PDF、DOC/DOCX、PPT/PPTX、HTML等文件</font>

我们进行逐个演示:

txt

使用 TextReader 来读取违规行为管理规范.txt 文件内容:

@Component
public class DocumentService {

    @Value("classpath:违规行为管理规范.txt")
    private Resource txtResource;

    public List<Document> loadText() {
        TextReader textReader = new TextReader(txtResource);
        textReader.getCustomMetadata().put("title", "违规行为管理规范.txt");
        List<Document> documentList = textReader.get();
        return documentList;
    }
}
@RestController
public class RAGController {
    @Resource
    private DocumentService documentService;

    @GetMapping("/document/text")
    public List<Document> txtDocument() {
        return documentService.loadText();
    }

}

可以看到,Document 的格式由 id 文档的唯一标识符、text 文档主要内容、media 与文档相关的媒体内容、metadata 元数据和 score 用于排序和过滤的分数组成

JSON

使用 JsonReader 来读取 weather.json 文件内容:

@Component
public class DocumentService {

    @Value("classpath:weather.json")
    private Resource jsonResource;

    public List<Document> loadJson() {
        JsonReader jsonReader = new JsonReader(jsonResource);
        List<Document> documents = jsonReader.get();
        return documents;
    }
}
@RestController
public class RAGController {
    @Resource
    private DocumentService documentService;

    @GetMapping("/document/json")
    public List<Document> jsonDocument() {
        return documentService.loadJson();
    }

}

PDF

引入 spring-ai-pdf-document-reader 依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
    <version>1.0.0-M6</version>
</dependency>

使用 PagePdfDocumentReader 来读取 raft.pdf 文件内容:

@Component
public class DocumentService {


    @Value("classpath:raft.pdf")
    private Resource pdfResource;


    public List<Document> loadPdf() {
        PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(pdfResource, 
                                PdfDocumentReaderConfig.builder()
                                            .withPageTopMargin(0)
                                            .withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
                                            .withNumberOfTopTextLinesToDelete(0)
                                            .build())
                                            .withPagesPerDocument(1)
                                            .build());
        List<Document> read = pdfReader.read();
        return read;
    }
}
@RestController
public class RAGController {
    @Resource
    private DocumentService documentService;

    @GetMapping("/document/pdf")
    public List<Document> pdfDocument() {
        return documentService.loadPdf();
    }

}

DOCX

引入 spring-ai-tika-document-reader 依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-tika-document-reader</artifactId>
    <version>1.0.0-M6</version>
</dependency>

使用 TikaDocumentReader 来读取 test.docx 文件内容:

@Component
public class DocumentService {

    @Value("classpath:test.docx")
    private Resource docxResource;

    public List<Document> loadDocx() {
        TikaDocumentReader docxReader = new TikaDocumentReader(docxResource);
        List<Document> documents = docxReader.read();
        return documents;
    }
}
@RestController
public class RAGController {
    @Resource
    private DocumentService documentService;

    @GetMapping("/document/docx")
    public List<Document> docxDocument() {
        return documentService.loadDocx();
    }
}

HTML

使用 TikaDocumentReader 来读取 =email.html 文件内容:

@Component
public class DocumentService {

    @Value("classpath:email.html")
    private Resource htmlResource;
    
    public List<Document> loadHtml(){
        TikaDocumentReader htmlReader = new TikaDocumentReader(htmlResource);
        List<Document> documents = htmlReader.read();
        return documents;
    }
}
@RestController
public class RAGController {
    @Resource
    private DocumentService documentService;

    @GetMapping("/document/html")
    public List<Document> htmlDocument() {
        return documentService.loadHtml();
    }
}

文档转换

接下来,我们要将文档 Document 分割成适合 AI 模型上下文窗口的片段,这里要使用的是

TokenTextSplitter,它基于 CL100K_BASE 编码,按 Token 数量分割文本,默认按 800Tokens/块 来分块

我们创建一个 TokenTextSplitter 对象

其中:
- defaultChunkSize 表示目标 Token 数,即每个块的 Token 数
- minChunkSizeChars 表示最小字符数,当某块因分隔符导致不足 defaultChunkSize 时,会至少保留minChunkSize 的内容
- minChunkLengthToEmbed 表示最小有效分块长度,避免过滤短句
- maxNumChunks 表示最大分块数
- keepSeparator 表示是否保留分隔符(如换行符)在文本块中,中文无需保留

调用 TokenTextSplitter 的 split 方法就可以对 Document 进行分块了

@Component
public class DocumentService {

    @Value("classpath:test.docx")
    private Resource docxResource;
    
    public List<Document> loadDocx() {
        TikaDocumentReader docxReader = new TikaDocumentReader(docxResource);
        List<Document> documents = docxReader.read();
        TokenTextSplitter splitter = new TokenTextSplitter(
                30, // defaultChunkSize 目标Token数,即每个块的Token数
                20,  // minChunkSizeChars 最小字符数,当某块因分隔符导致不足chunkSize时,会至少保留minChunkSize的内容
                1, // minChunkLengthToEmbed 最小有效分块长度,避免过滤短句
                20, // maxNumChunks 最大分块数
                false // keepSeparator 是否保留分隔符(如换行符)在文本块中,中文无需保留
        );
        List<Document> documentList = splitter.split(documents);
        LOGGER.info("分块后文档数: {}", documentList.size());
        documentList.stream()
                .forEach(document -> LOGGER.info("Processing document: {}", document));
        return documents;
    }
}

当然,我们还可以对 Document 的格式进行处理,使用 ContentFormatTransformer 并结合特定规则处理文档内容

其中,DefaultContentFormatter 定义了格式规则

public class ContentFormatTransformerTest {

    public static void main(String[] args) {

        Document doc = new Document(
            "北京今日天气:晴朗,气温 25°C,空气质量优",
            Map.of(
                "city", "北京",
                "date", "2024-03-20",
                "temperature", "25°C",
                "weather", "晴朗",
                "air_quality", "优",
                "humidity", "45%",
                "wind_speed", "3级"
            )
        );

        DefaultContentFormatter formatter = DefaultContentFormatter.builder()
                .withMetadataTemplate("{key}---{value}")  // 元数据显示格式
                .withMetadataSeparator("\n")             // 元数据分隔符
                .withTextTemplate("METADATA:\n{metadata_string}\nCONTENT:\n{content}") // 内容模板
                .withExcludedInferenceMetadataKeys("air_quality")  // 推理时排除的元数据
                .withExcludedEmbedMetadataKeys("wind_speed")       // 嵌入时排除的元数据
                .build();

        String content = formatter.format(doc, MetadataMode.EMBED);
        System.out.println(content);

        ContentFormatTransformer transformer = new ContentFormatTransformer(formatter, false);

        List<Document> documentList = transformer.apply(List.of(doc));

        documentList.forEach(System.out::println);

    }
}

此外,我们还可以使用 KeywordMetadataEnricher,它会使用大模型提取文档关键词,并添加在元数据中

其中,KeywordMetadataEnricher 中的第一个参数为 ChatModel 模型,第二个参数为所提取的最大关键词数量

@Service
public class KeywordMetadataEnricherTest {

    @Resource
    private OpenAiChatModel openAiChatModel;

    public String getKeywords() {

        Document doc = new Document("""
                    今日北京,碧空如洗,澄澈的蓝天如同被反复擦拭过的蓝宝石,不见一丝云彩。
                    温暖的阳光毫无保留地倾洒在这座古老又现代的城市,气温稳定维持在 25°C,
                    体感舒适宜人,微风轻拂,带着些许春日的惬意。
                    空气质量达到了优的级别,清新的空气沁人心脾,无论是漫步在故宫的红墙黄瓦间,
                    还是穿梭于国贸的高楼大厦中,都能尽情享受每一口呼吸。
                    这样的好天气,引得市民纷纷走出家门,公园里孩童嬉笑玩耍,护城河旁老人悠闲垂钓,
                    街头巷尾满是活力与生机,处处洋溢着幸福的气息。
                    """,
                    Map.of(
                        "city", "北京",
                        "date", "2024-03-20",
                        "temperature", "25°C",
                        "weather", "晴朗",
                        "air_quality", "优",
                        "humidity", "45%",
                        "wind_speed", "3级"
                    ));
        // 构造 KeywordMetadataEnricher,传入 OpenAiChatModel,并设置最大关键词数量为 5
        KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(openAiChatModel,5);
        List<Document> documentList = enricher.apply(List.of(doc));
        // 输出生成的关键词
        String keywords = (String) documentList.get(0).getMetadata().get("excerpt_keywords");
        return keywords;
    }
}

测试

@RestController
public class RAGController {
    @Resource
    private KeywordMetadataEnricherTest keywordMetadataEnricherTest;

    @GetMapping("/document/keywords")
    public String xmlDocument() {
        return keywordMetadataEnricherTest.getKeywords();
    }
}

接下来,演示 SummaryMetadataEnricher(它使用大模型提取文档摘要,在元数据中添加一个 section_summary/prev_section_summary/next_section_summary 字段),它不仅可以生成当前文档的摘要,还能生成相邻文档(前一篇和后一篇)的摘要

SummaryMetadataEnricher 中的参数如下:
* chatModel:用于生成摘要的 AI 模型
* summaryTypes:指定生成哪些类型的摘要(PREVIOUS、CURRENT、NEXT)
* summaryTemplate(可选):自定义摘要生成模板(默认是英文,可以改写成中文的)
* metadataMode(可选):控制生成摘要时如何处理文档的元数据

@Service
public class SummaryMetadataEnricherTest {

    @Resource
    private OpenAiChatModel openAiChatModel;

    public List<String> getSummary() {

        Document doc1 = new Document("""
                    今日北京,碧空如洗,澄澈的蓝天如同被反复擦拭过的蓝宝石,不见一丝云彩。
                    温暖的阳光毫无保留地倾洒在这座古老又现代的城市,气温稳定维持在 25°C,
                    体感舒适宜人,微风轻拂,带着些许春日的惬意。
                    空气质量达到了优的级别,清新的空气沁人心脾,无论是漫步在故宫的红墙黄瓦间,
                    还是穿梭于国贸的高楼大厦中,都能尽情享受每一口呼吸。
                    这样的好天气,引得市民纷纷走出家门,公园里孩童嬉笑玩耍,护城河旁老人悠闲垂钓,
                    街头巷尾满是活力与生机,处处洋溢着幸福的气息。
                    """,
                Map.of(
                        "city", "北京",
                        "date", "2024-03-20",
                        "temperature", "25°C",
                        "weather", "晴朗",
                        "air_quality", "优",
                        "humidity", "45%",
                        "wind_speed", "3级"
                )
        );

        Document doc2 = new Document("""
                    今日上海,全球瞩目的2024世界人工智能大会在浦东世博中心盛大开幕。
                    来自40多个国家的2000余位科技精英齐聚一堂,展示了最新的AI研究成果。
                    开幕式上,由中国科学院研发的"启明-9000"超级计算机首次公开亮相,
                    其算力达到每秒10^18次浮点运算,较前代提升300%,能耗却降低40%。
                    现场还展示了AI驱动的医疗诊断系统,能在10秒内完成肺部CT影像分析,
                    准确率高达99.7%。各大科技巨头纷纷发布智能机器人、自动驾驶等前沿产品,
                    彰显了AI技术在赋能实体经济、改善民生等领域的巨大潜力。
                    """,
                Map.of(
                        "city", "上海",
                        "date", "2024-05-15",
                        "event", "世界人工智能大会",
                        "location", "浦东世博中心",
                        "key_technology", "超级计算机、AI医疗、智能机器人",
                        "organizer", "中国科学院、科技部"
                )
        );

        Document doc3 = new Document("""
                    西安城墙下,第十四届"长安国际艺术节"在春日的暖阳中拉开帷幕。
                    来自全球15个国家的80余个艺术团体带来了为期20天的文化盛宴。
                    开幕式上,敦煌研究院与法国卢浮宫联合打造的"丝路文明对话"光影秀惊艳全场,
                    3D投影技术将莫高窟壁画与卢浮宫馆藏完美融合,呈现出跨越时空的艺术对话。
                    日本能剧、西班牙弗拉门戈等世界非遗表演轮番登场,
                    更有陕派秦腔与现代交响乐的创新融合演出,吸引了3万余名观众到场欣赏。
                    艺术节期间,还将举办20余场学术研讨会和50多场艺术工作坊,
                    为不同文化背景的艺术家和观众搭建起交流互鉴的桥梁。
                    """,
                Map.of(
                        "city", "西安",
                        "date", "2024-04-08",
                        "event", "长安国际艺术节",
                        "duration", "20天",
                        "participants", "15国80余个艺术团体",
                        "highlight", "丝路文明对话光影秀、非遗表演"
                )
        );

        // 构造 SummaryMetadataEnricher,传入 OpenAiChatModel,指定要生成的摘要类型
        String template = """
                请基于以下文本提取核心信息:
                    {context_str}
                
                要求:
                     1. 使用简体中文
                     2. 包含关键实体
                     3. 不超过50字
                
                摘要:
                """;
        SummaryMetadataEnricher summaryMetadataEnricher = new SummaryMetadataEnricher(
                openAiChatModel,
                List.of(SummaryMetadataEnricher.SummaryType.PREVIOUS,
                        SummaryMetadataEnricher.SummaryType.CURRENT,
                        SummaryMetadataEnricher.SummaryType.NEXT),
                template,
                MetadataMode.ALL
        );
        List<Document> documentList = summaryMetadataEnricher.apply(List.of(doc1, doc2, doc3));
        String summary1 = (String) documentList.get(0).getMetadata().get("section_summary");
        String summary2 = (String) documentList.get(1).getMetadata().get("prev_section_summary");
        String summary3 = (String) documentList.get(2).getMetadata().get("next_section_summary");

        return Arrays.asList(summary1, summary2, summary3);
    }
}
@RestController
public class RAGController {

    @Resource
    private SummaryMetadataEnricherTest summaryMetadataEnricherTest;


    @GetMapping("/document/summary")
    public List<String> summaryDocument() {
        return summaryMetadataEnricherTest.getSummary();
    }
}

文档写入到文件

使用 FileDocumentWriter 来将 Document 写入到文件中

其中:
- fileName 表示目标文件名
- withDocumentMarkers 表示是否在输出中包含文档标记(默认false)
- metadataMode 表示指定写入文件的文档内容格式(默认MetadataMode.NONE)
- append 表示是否追加写入文件(默认false)

调用 accept 方法,传入 List

@Service
public class DocumentWriterTest {
    
    public static void main(String[] args) {

        Document doc1 = new Document("今日北京,碧空如洗,澄澈的蓝天如同被反复擦拭过的蓝宝石,不见一丝云彩。");
        Document doc2 = new Document("温暖的阳光毫无保留地倾洒在这座古老又现代的城市,气温稳定维持在 25°C");
        Document doc3 = new Document("体感舒适宜人,微风轻拂,带着些许春日的惬意。");
        
        FileDocumentWriter fileDocumentWriter = new FileDocumentWriter(
                "output.txt", 
                true, 
                MetadataMode.ALL, 
                false
        );
        fileDocumentWriter.accept(List.of(doc1, doc2, doc3));
    }
}

向量化存储

Spring AI 通过 VectorStore 接口为向量数据库交互提供了抽象化的 API,这里的向量数据库我选择了 Milvus

安装 Milvus 及 Attu

在 Docker 中安装 Milvus,输入以下命令:

curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh -o standalone_embed.sh

bash standalone_embed.sh start

可以访问 Milvus WebUI,网址是 http://your_host:9091/webui/

Attu 是 Milvus 的官方 GUI 客户端,提供更直观的操作体验

在 Docker 中安装运行 Attu,输入以下命令:

docker run -d --name attu -p 8000:3000 -e MILVUS_URL=your_host:19530 zilliz/attu:v2.5.6

访问 Attu 的地址:http://your_host:8000

使用 Milvus

引入 spring-ai-milvus-store 依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId>
    <version>1.0.0-M6</version>
</dependency>

这里我使用的文本向量模型是阿里的 text-embedding 系列,阿里云百炼:https://help.aliyun.com/zh/model-studio/get-api-key,可能是不兼容的原因,我们不再使用 spring-ai-openai-spring-boot-starter 依赖,而是使用 spring-ai-alibaba-starter 依赖

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter</artifactId>
    <version>1.0.0-M5.1</version>
</dependency>

修改 yml 文件中的配置,在 .env 中填写 api-key

spring:
  # ai 配置
  ai:
    dashscope:
      api-key: ${AI_API_KEY}

首先将数据存入到 Milvus 中,使用 vectorStore 的 add 方法,传入 List

@Service
public class MilvusService {

    @Value("classpath:违规行为管理规范.txt")
    private Resource textResource;

    @jakarta.annotation.Resource
    private MilvusVectorStore vectorStore;

    public void initMilvus() {

        TextReader textReader = new TextReader(textResource);
        textReader.getCustomMetadata().put("title", "违规行为管理规范.txt");
        TokenTextSplitter tokenTextSplitter = new TokenTextSplitter();
        List<Document> documentList = tokenTextSplitter.apply(textReader.get());
        
        vectorStore.add(documentList);
        LOGGER.info("Vector data initialized");
    }
}

LLM + Milvus

首先测试一下向量搜索,根据用户输入搜索最相似的文本

在 advisors 方法中 new 一个 QuestionAnswerAdvisor,传入 milvusVectorStore 和SearchRequest,其中,.query(userInput) 表示搜索的文本,.topK(5) 表示要搜索的数量

@RestController
public class RAGController {

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

    @Resource
    private ChatClient chatClient;
    
    @Resource
    private MilvusVectorStore milvusVectorStore;

    @GetMapping("/chat/rag")
    public String initChat(@RequestParam(value = "userInput") String userInput) {

        return chatClient.prompt()
                .user(userInput)
                .advisors(new QuestionAnswerAdvisor(milvusVectorStore, 
                                                    SearchRequest.builder()
                                                    .query(userInput)
                                                    .topK(5)
                                                    .build()
                                                   )
                )
                .call()
                .content();
    }
}

对比文档可以看到确实进行了向量搜索

当然,我们可以将用户输入和检索到的文档进行合并,形成 LLM 的输入提示

@GetMapping("/rag")
public String milvusRag(@RequestParam(value = "input") String input) {
    PromptTemplate promptTemplate = new PromptTemplate("""
        你是一个违规行为管理规范答疑助手,请结合文档提供的内容回答用户的问题,如果不知道请直接回答不知道
        用户输入的问题:
                 {input}

        文档:
                 {documents}
    """);
    
    // 构造 SearchRequest
    SearchRequest searchRequest = SearchRequest.builder()
                                               .query(input)
                                               .topK(5).build();
    // 从向量数据库中搜索到的最相似的 Document
    List<Document> documentList = milvusVectorStore.similaritySearch(searchRequest);
    // 转换为 String
    List<String> stringList = documentList.stream()
                                          .map(Document::getText)
                                          .collect(Collectors.toList());
    // 拼接
    String relevantDocs = String.join("\n", stringList);
    
    LOGGER.info("用户问题={},查询结果={}", input, relevantDocs);

    // 构造 PromptTemplate 参数
    Map<String, Object> params = new HashMap<>();
    params.put("input", input);
    params.put("documents", relevantDocs);

    return chatClient.prompt(new Prompt(promptTemplate.render(params)))
                     .call()
                     .content();
}

Aegnt

Agent 并非一个标准概念,它可以是完全自主的系统,也可以是遵循预定工作流的系统。Anthropic 将这些变体统称为 Agent 系统,并将其在实现架构上划分为两类:

  • 工作流 Workflow:通过预定义的代码路径去编排 LLM 和工具
  • 智能体 Agent:LLM 动态指导自身流程和工具使用,保持对任务完成方式的控制

Agent 框架通常以 LLM 为核心,以工具 Tool、规划 Planning、记忆 Memory 和行动 Action 等多个模块共同组成

Workflow Agent(Workflow

Workflow Agent 是通过预定义代码路径协调 LLM 和工具的系统,我们在构建 Workflow Agent 时需要针对业务进行步骤分解和流程设计

提示链(Prompt chaining)

提示链将任务分解为一系列步骤,其中每个 LLM 调用都会处理前一个步骤的输出。我们可以在任何中间步骤上添加程序化检查(Gate),以确保流程仍在正常进行。

此工作流程非常适合任务可以轻松清晰地分解为固定子任务的情况。其主要目标是通过简化每次 LLM 调用,以降低延迟并提高准确率。

为此,我们需要对业务步骤进行拆解,构建出一个 Prompt 列表,通过循环串行化执行 LLM Call

示例:使用提示链将电商平台商品信息格式化输出

首先编写一个 WorkflowService 接口,定义 startPromptChainingWorkflow 方法

public interface WorkflowService {

    String startPromptChainingWorkflow(String userInput);
}

在其实现类中,我们将该示例业务拆解成 4 个步骤,对每个步骤编写对应的 Prompt

在 chain 方法中,我们在 for 循环中链式执行 Prompt 数组

@Service
public class WorkflowServiceImpl implements WorkflowService {

    private static final String [] DEFAULT_SYSTEM_PROMPTS = {
            // 步骤1
            """
            从文本中提取商品名称、价格、库存数量、销量或其他重要信息。
            每条数据格式为 "信息类别:具体内容",各占一行。
            示例格式:
            商品名称:智能蓝牙耳机
            价格: 199 元
            库存数量: 50
            销量: 1200
            """,

            // 步骤2
            """
            将价格统一保留两位小数,并去除货币单位(元)。
            将库存数量和销量转换为整数形式(每隔3位用逗号分隔)。
            保持每行一个信息。
            示例格式:
            商品名称:智能蓝牙耳机
            价格: 199.00
            库存数量: 5,000,000
            销量: 1,200
            """,

            // 步骤3
            """
            仅保留销量大于 1000 的商品信息,并按销量降序排列。
            保持每行 "信息类别:具体内容" 的格式。
            示例:
            商品名称:智能蓝牙耳机
            价格: 199.00
            库存数量: 5,000,000
            销量: 1,200
            """,

            // 步骤4
            """
            将排序后的数据格式化为Markdown表格,包含商品名称、价格、库存数量、销量或其他信息列
            要求仅输出Markdown表格,不要包含任何其他文本:
            | 商品名称 | 价格 | ... |
            |:--|--:||--:|
            | 智能蓝牙耳机 | 199.00 || ... |
            """
    };

    @Resource(name = "openAiChatClient")
    private ChatClient openAiChatClient;

    
    public String chain(String Input) {
        int step = 0;
        String response = Input;
        // 初始用户输入
        System.out.println(String.format("\nSTEP %s:\n %s", step++, response));

        // 串行化调用 LLM
        for (String prompt : DEFAULT_SYSTEM_PROMPTS) {

            String input = String.format("{%s}\n {%s}", prompt, response);

            response = openAiChatClient.prompt(input)
                    .call()
                    .content();

            System.out.println(String.format("\nSTEP %s:\n %s", step++, response));
        }

        return response;
    }

    @Override
    public String startPromptChainingWorkflow(String userInput) {
        return chain(userInput);
    }
}

测试效果,可以看到,控制台中输出的每个步骤的执行结果,最终返回了一个 Markdown 表格,符合预期要求

@RestController
public class AgentController {

    @Resource
    private WorkflowService workflowService;

    @GetMapping("/agent/chain")
    public String chain(@RequestParam("userInput") String userInput) {
        return workflowService.startPromptChainingWorkflow(userInput);
    }

}
STEP 0:
我们新上架了一批商品,智能蓝牙耳机,价格 199.8 元,库存 50 件,销量 120000;复古机械键盘,价格 299 元,库存 30 件,销量 800;无线鼠标,价格 89.9 元,库存 100 件,销量 15000。

STEP 1:
商品名称:智能蓝牙耳机  
价格:199.8 元  
库存数量:50  
销量:120000  

商品名称:复古机械键盘  
价格:299 元  
库存数量:30  
销量:800  

商品名称:无线鼠标  
价格:89.9 元  
库存数量:100  
销量:15000

STEP 2:
商品名称:智能蓝牙耳机  
价格:199.80  
库存数量:50  
销量:120,000  

商品名称:复古机械键盘  
价格:299.00  
库存数量:30  
销量:800  

商品名称:无线鼠标  
价格:89.90  
库存数量:100  
销量:15,000

STEP 3:
商品名称:智能蓝牙耳机  
价格:199.80  
库存数量:50  
销量:120,000  

商品名称:无线鼠标  
价格:89.90  
库存数量:100  
销量:15,000

STEP 4:
| 商品名称       | 价格  | 库存数量 | 销量    |
|:---------------|------:|---------:|--------:|
| 智能蓝牙耳机   | 199.80| 50       | 120,000 |
| 无线鼠标       | 89.90 | 100      | 15,000  |

路由(Routing)

路由会对输入进行分类,并将其定向到专门的后续任务。此工作流程允许分离关注点,并构建更专业的提示。如果没有此工作流程,针对一种输入进行优化可能会损害其他输入的性能。

路由非常适合复杂任务,其中存在不同的类别,最好分别处理

示例:将不同类型的客户服务查询(一般问题、退款请求、技术支持、合作沟通)引导到不同的下游流程中

在接口中添加 startRoutingWorkflow 方法

String startRoutingWorkflow(String userInput);

我们将 Prompt 全部放到一个常量类中,方便管理

public class PromptConstant {

    public static final String [] DEFAULT_SYSTEM_PROMPTS = {
            // 步骤1
            """
            从文本中提取商品名称、价格、库存数量、销量或其他重要信息。
            每条数据格式为 "信息类别:具体内容",各占一行。
            示例格式:
            商品名称:智能蓝牙耳机
            价格: 199 元
            库存数量: 50
            销量: 1200
            """,

            // 步骤2
            """
            将价格统一保留两位小数,并去除货币单位(元)。
            将库存数量和销量转换为整数形式(每隔3位用逗号分隔)。
            保持每行一个信息。
            示例格式:
            商品名称:智能蓝牙耳机
            价格: 199.00
            库存数量: 5,000,000
            销量: 1,200
            """,

            // 步骤3
            """
            仅保留销量大于 1000 的商品信息,并按销量降序排列。
            保持每行 "信息类别:具体内容" 的格式。
            示例:
            商品名称:智能蓝牙耳机
            价格: 199.00
            库存数量: 5,000,000
            销量: 1,200
            """,

            // 步骤4
            """
            将排序后的数据格式化为Markdown表格,包含商品名称、价格、库存数量、销量或其他信息列
            要求仅输出Markdown表格,不要包含任何其他文本:
            | 商品名称 | 价格 | ... |
            |:--|--:||--:|
            | 智能蓝牙耳机 | 199.00 || ... |
            """
    };

    public static final Map<String, String> ROUTE_MAP = Map.of (
            "general",
            """
            您是一位客户服务专员。请遵循以下准则:
            1.始终以 "通用问题回复:" 开头
            2.全面理解客户提出的一般问题,先表达共情与理解
            3.用简洁易懂的语言解答问题,若涉及复杂政策,进行通俗解释
            4.对于无法直接回答的问题,告知客户后续跟进方式及预计时间
            5.保持友好、耐心的沟通语气
            输入: 
            """,

            "refund",
            """
            您是一位退款处理专员。请遵循以下准则:
            1.始终以 "退款请求回复:" 开头
            2.优先核实客户订单及退款原因,安抚客户情绪
            3.详细说明退款政策、流程及预计到账时间
            4.指导客户提交必要的退款材料,提供提交方式和地址
            5.定期告知客户退款进度查询方式
            6.保持专业且负责的态度
            输入: 
            """,

            "technical_support",
            """
            1.您是一位技术支持人员。请遵循以下准则:
            2.始终以 "技术支持回复:" 开头
            3.询问客户具体的技术问题表现,收集系统环境信息
            4.列出分步骤的排查和解决方法,附上操作截图示例
            5.提供临时替代方案以保障客户紧急使用需求
            6.若问题复杂,说明升级至高级技术团队的流程和预计响应时间
            7.使用清晰的技术术语,表述严谨
            输入: 
            """,

            "cooperation",
            """
            1.您是一位合作对接专员。请遵循以下准则:
            2.始终以 "合作沟通回复:" 开头
            3.热情回应客户的合作意向,询问合作需求和期望
            4.介绍公司合作政策、优势资源及成功案例
            5.安排专人与客户进行后续详细对接,告知对接人信息和时间
            6.提供合作相关资料的获取方式
            7.保持积极主动、开放合作的态度
            输入: 
            """
    );

}

使用到的实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoutingResponse {

    private String reason;

    private String selection;
}

在接口实现类中,**route 方法负责进行路由操作,并根据路由到的提示词调用 LLM 返回结果 **

public String route(String input, Map<String, String> routeMap){

    Set<String> stringSet = routeMap.keySet();

    // 路由操作提示词
    String routePrompt = String.format("""
                    分析输入内容并从以下选项中选择最合适的查询类型:%s
                    首先解释你的判断依据,然后按照以下JSON格式提供选择:
                        \\{
                        "reasoning": "简要说明为何该客户服务查询应选择该查询类型。
                        需考虑关键词、用户意图和紧急程度。",
                        "selection": "所选查询类型名称"
                        \\}
                    输入:%s""", stringSet, input);
    
    // 调用 LLM,获取路由结果响应
    RoutingResponse routeResponse = openAiChatClient.prompt(new Prompt(routePrompt))
                                                    .call()
                                                    .entity(RoutingResponse.class);

    LOGGER.info("RouteResponse: " + "Reason---" + routeResponse.getReason() + 
                "\n" + "Selection---" + routeResponse.getSelection());
    
    // 获取路由结果响应中的提示词
    String callPrompt = routeMap.get(routeResponse.getSelection());

    // 调用 LLM
    String content = openAiChatClient.prompt(new Prompt(callPrompt + "\n" + input))
                                     .call()
                                     .content();
    // 返回结果
    return content;
}

@Override
public String startRoutingWorkflow(String userInput) {
    return route(userInput, PromptConstant.ROUTE_MAP);
}

测试效果,从控制台中可以看到,用户输入被路由到了 refund,符合预期要求

@GetMapping("/agent/route")
public String route(@RequestParam("userInput") String userInput) {
    return workflowService.startRoutingWorkflow(userInput);
}

并行化(Parallelization)

LLM 有时可以同时处理一项任务,并以编程方式聚合其输出

何时使用:拆分后的子任务可以并行化以提高速度、需要多个视角或尝试以获得更高置信度的结果

对于涉及多个考量的复杂任务,当每个考量都由单独的 LLM 调用处理时,LLM 通常表现更好,从而能够专注于每个特定方面。

示例:分析气候变化趋势对各个行业的系统性风险和机遇

由于可以对每个行业的分析单独执行,因此可以并行化

在接口中新增 startParallelWorkflow 方法

List<String> startParallelWorkflow(String userInput);

在常量中增加一个 PARALLEL_PROMPT

public static final String PARALLEL_PROMPT =
                        """
                        分析气候变化趋势对该行业的系统性风险和机遇,
                        构建包含短期(1-3年)、中期(3-5年)、长期(5-10年)的战略应对框架。
                        使用清晰的分区和优先级进行格式化。
                        """;

用户可以输入多个行业,startParallelWorkflow 方法可以将其拆分为多个子任务并行执行,其中核心线程数为子任务数(行业数)

@Override
public List<String> startParallelWorkflow(String userInput) {
    String regex = "[,。!?;、,.!?;]+";
    String[] userInputArr = userInput.split(regex);
    List<String> userInputList = Arrays.asList(userInputArr);
    
    // 核心线程数
    int nWorkers = userInputArr.length;

    String prompt = PromptConstant.PARALLEL_PROMPT;
    ExecutorService executorService = Executors.newFixedThreadPool(nWorkers);

    try{
        List<CompletableFuture<String>> completableFutureList = userInputList.stream().map(input ->
                CompletableFuture.supplyAsync(() -> {
                    try {
                        return openAiChatClient.prompt(prompt + "\nInput: " + input)
                                .call()
                                .content();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }, executorService)
        ).collect(Collectors.toList());

        // 并行等待所有任务完成
        CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(
                completableFutureList.toArray(CompletableFuture[]::new)
        );
        voidCompletableFuture.join();

        List<String> collect = completableFutureList.stream().map(CompletableFuture::join)
                .collect(Collectors.toList());
        LOGGER.info("ParallelWorkflow: " + collect);
        return collect;

    } finally {
        executorService.shutdown();
    }
}

测试

@GetMapping("/agent/parallel")
public List<String> parallel(@RequestParam("userInput") String userInput) {
    return workflowService.startParallelWorkflow(userInput);
}

#### **一、系统性风险分析**

---

1. **物理风险**  
   - **极端天气事件**:干旱、洪水、热浪等频率增加,影响作物产量和质量。  
   - **水资源短缺**:降水模式变化导致灌溉用水不足。  
   - **土壤退化**:高温和侵蚀加剧土壤肥力下降。  

2. **转型风险**  
   - **政策压力**:碳排放税、环保法规趋严增加生产成本。  
   - **市场需求变化**:消费者偏好转向低碳农产品,传统产品需求下降。  
   - **技术滞后**:未能适应气候智能型农业技术可能被淘汰。  

3. **供应链风险**  
   - **物流中断**:极端天气破坏运输和仓储设施。  
   - **投入成本上升**:化肥、农药等因能源价格波动或短缺而涨价。  

---
### 农业行业的气候变化趋势分析及战略应对框架

#### **二、机遇分析**
1. **气候适应型农业**  
   - 耐旱/耐涝作物品种、精准灌溉技术等需求增长。  
2. **碳汇经济**  
   - 通过再生农业、森林碳汇项目获取额外收益。  
3. **政策与市场激励**  
   - 政府对绿色农业的补贴和碳交易机制支持。  
4. **技术创新**  
   - 垂直农业、AI病虫害预测等提升效率。  

---

### **三、战略应对框架**

#### **短期(1-3年):风险缓解与基础能力建设**  
**优先级:高(生存与合规)**  
1. **风险应对**  
   - 引入天气指数保险,对冲极端气候损失。  
   - 建立应急水源储备和多元化供应链。  
2. **技术试点**  
   - 试点耐候作物品种和小规模精准灌溉系统。  
3. **政策响应**  
   - 跟踪碳减排政策,优化现有生产流程以符合法规。  

---

#### **中期(3-5年):转型与效率提升**  
**优先级:中高(竞争力提升)**  
1. **技术规模化**  
   - 推广气候智能农业技术(如土壤湿度传感器、滴灌)。  
   - 投资可再生能源(太阳能水泵、生物质能)。  
2. **市场调整**  
   - 开发低碳认证农产品,抢占细分市场。  
3. **合作网络**  
   - 与科研机构合作育种,参与碳汇项目试点。  

---

#### **长期(5-10年):可持续领导力构建**  
**优先级:中(战略重塑)**  
1. **系统变革**  
   - 全面转向再生农业(轮作、免耕、有机肥料)。  
   - 布局垂直农业或气候适应性强的区域生产基地。  
2. **价值链整合**  
   - 构建从生产到销售的低碳闭环供应链。  
3. **政策与资本利用**  
   - 利用碳交易市场实现盈利,争取长期政策红利。  

---

### **四、优先级矩阵**
| 时间框架 | 关键行动领域               | 优先级 | 资源投入建议 |  
|----------|----------------------------|--------|--------------|  
| 短期     | 应急风险管理、合规         | 高     | 20%-30%      |  
| 中期     | 技术升级、市场差异化       | 中高   | 40%-50%      |  
| 长期     | 模式创新、生态整合         | 中     | 20%-30%      |  

--- 

**注**:农业企业需根据自身规模(如小农户 vs 大型农企)调整策略重点,小农户可优先依赖政策支持与合作社协作,大企业则需引领技术创新和标准制定。, 

### 气候变化对保险业的系统性风险与机遇分析及战略应对框架  

#### **一、系统性风险**  
1. **物理风险**  
   - **短期(1-3年)**:极端天气事件(如飓风、洪水、野火)频发,导致财产险赔付激增,再保险成本上升。  
   - **中期(3-5年)**:海平面上升和慢性气候灾害(如干旱)影响长期保单定价,部分高风险地区承保能力下降。  
   - **长期(5-10年)**:气候移民和基础设施损毁导致系统性风险累积,传统精算模型失效。  

2. **转型风险**  
   - **短期(1-3年)**:碳密集型行业(如化石燃料)投保需求下降,责任险面临环保诉讼风险。  
   - **中期(3-5年)**:监管趋严(如碳税、绿色金融政策)倒逼产品结构调整,资本配置压力增大。  
   - **长期(5-10年)**:低碳技术普及导致传统业务萎缩,需重构风险评估逻辑。  

3. **声誉与合规风险**  
   - **短期(1-3年)**:客户对“绿色washing”敏感,ESG评级影响融资成本。  
   - **中期(3-5年)**:强制披露气候相关财务信息(如TCFD),合规成本上升。  

---  

#### **二、机遇**  
1. **新产品开发**  
   - **短期(1-3年)**:推出 parametric insurance(参数化保险)应对极端天气,覆盖快速理赔场景。  
   - **中期(3-5年)**:定制可再生能源项目保险(如风电、光伏设备险)。  
   - **长期(5-10年)**:气候适应型保险(如生态修复保险、碳捕获技术险)。  

2. **数据与技术革新**  
   - **短期(1-3年)**:利用AI和卫星数据优化灾害预测与动态定价。  
   - **中期(3-5年)**:区块链提升气候风险共担机制(如P2P保险)。  

3. **政策与市场协同**  
   - **短期(1-3年)**:参与政府气候韧性项目(如洪水防御基金),获取补贴。  
   - **长期(5-10年)**:成为绿色债券和碳交易市场的风险中介。  

---  

### **三、战略应对框架**  

| **时间维度** | **优先级措施**                          | **关键行动**                                                                 |
|--------------|----------------------------------------|-----------------------------------------------------------------------------|
| **短期(1-3年)** | 1. 风险建模升级<br>2. 应急产品创新     | - 整合气候数据到精算模型<br>- 试点参数化保险产品<br>- 剥离高碳资产承保       |
| **中期(3-5年)** | 1. 业务结构转型<br>2. 合规能力建设     | - 设立绿色保险专项基金<br>- 开发ESG投资组合<br>- 培训气候风险评估团队        |
| **长期(5-10年)**| 1. 生态协同<br>2. 系统性风险对冲       | - 与政府合作建立巨灾风险池<br>- 投资气候适应技术(如海绵城市保险解决方案) |

**优先级排序**:短期聚焦风险对冲与监管适应,中期转向绿色产品线,长期构建气候韧性生态。  

---  
**注**:战略需动态调整,建议每年评估气候情景分析(如RCP 2.6 vs RCP 8.5)对业务的影响。, 

### 旅游业应对气候变化的战略框架  
(按风险/机遇优先级排序,★数量代表紧迫性)

---

#### **一、系统性风险分析**  
**1. 物理风险**  
- **短期(1-3年)★★★**:极端天气事件(如飓风、野火)导致景区关闭、基础设施损坏。  
- **中期(3-5年)★★☆**:海平面上升威胁沿海度假区(如马尔代夫、加勒比地区)。  
- **长期(5-10年)★☆☆**:生态系统退化(珊瑚白化、冰川消失)降低目的地吸引力。  

**2. 转型风险**  
- **短期★★☆**:碳税政策增加航空及酒店运营成本。  
- **中期★★★**:消费者偏好转向低碳旅行,高排放业务(如邮轮)面临需求下降。  
- **长期★★☆**:全球“净零”目标倒逼全产业链脱碳改革。  

---

#### **二、核心机遇分析**  
**1. 需求转型**  
- **短期★★★**:低碳旅游产品(如本地游、生态民宿)需求激增。  
- **中期★★☆**:气候适应性旅游(如极地旅游替代消失的冰川游)兴起。  
- **长期★☆☆**:虚拟旅游(VR+碳中和)成为新增长点。  

**2. 政策与投资**  
- **短期★★☆**:绿色补贴(如欧盟可持续旅游基金)支持企业转型。  
- **中期★★★**:碳交易市场为低碳景区创造额外收益。  
- **长期★★☆**:气候韧性基建(如防波堤、智能电网)提升目的地竞争力。  

---

#### **三、战略应对框架**  
**短期(1-3年)** ★★★  
- **优先级1**:建立气候风险监测系统,针对高频灾害(如洪水)制定应急方案。  
- **优先级2**:推出低碳认证(如酒店能源改造、短途旅游套餐),抢占市场先机。  
- **优先级3**:与航空公司合作开发碳抵消计划,缓解政策压力。  

**中期(3-5年)** ★★☆  
- **优先级1**:投资气候适应性项目(如人工珊瑚礁、高海拔滑雪场)。  
- **优先级2**:供应链脱碳(切换可再生能源交通、零碳供应链)。  
- **优先级3**:培训员工掌握可持续旅游服务技能(如生态导游)。  

**长期(5-10年)** ★☆☆  
- **优先级1**:参与目的地气候韧性规划(如马尔代夫“漂浮城市”)。  
- **优先级2**:开发“气候友好型”旅游IP(如碳中和主题公园)。  
- **优先级3**:布局虚拟旅游技术,减少实体资源依赖。  

---

#### **四、关键指标(KPI)**  
- 短期:应急响应时间缩短30%,低碳产品收入占比达20%。  
- 中期:供应链碳排放下降50%,10个目的地获气候韧性认证。  
- 长期:虚拟旅游收入占比超15%,全产业链实现净零排放。  

---  
**注**:框架需结合区域特性调整(如海岛vs内陆),并定期评估气候模型更新影响。, 

### 能源行业气候变化战略应对框架  
(按风险/机遇优先级排序,★为关键程度)

---

#### **一、系统性风险分析**  
**1. 政策与监管风险**  
- **短期(1-3年)**:碳税、排放标准趋严(★★★)  
- **中期(3-5年)**:化石燃料补贴取消、可再生能源配额制(★★★)  
- **长期(5-10年)**:全球碳边境税(如CBAM)扩大化(★★☆)  

**2. 物理风险**  
- **短期**:极端天气导致能源基础设施损坏(如电网、炼油厂)(★★☆)  
- **中期**:水资源短缺影响火电/核电运营(★☆☆)  
- **长期**:海平面上升威胁沿海能源设施(★★☆)  

**3. 市场与转型风险**  
- **短期**:可再生能源成本下降挤压传统能源利润(★★★)  
- **中期**:投资者撤资化石燃料资产(★★☆)  
- **长期**:能源需求结构颠覆(如电动汽车普及)(★★★)  

---

#### **二、战略机遇分析**  
**1. 清洁能源转型**  
- **短期**:分布式光伏、储能技术商业化(★★★)  
- **中期**:绿氢产业链突破(★★☆)  
- **长期**:碳捕集与封存(CCUS)规模化应用(★☆☆)  

**2. 能效与创新**  
- **短期**:智能电网与需求响应技术(★★☆)  
- **中期**:工业流程电气化(如绿电制钢)(★★★)  
- **长期**:核聚变等颠覆性能源技术(★☆☆)  

**3. 新商业模式**  
- **短期**:能源即服务(EaaS)订阅模式(★☆☆)  
- **中期**:碳信用交易与绿证金融化(★★☆)  
- **长期**:全球可再生能源电力贸易(★★★)  

---

#### **三、战略应对框架**  
**优先级排序**:政策响应 > 技术投资 > 资产重组 > 市场重塑  

| **时间维度** | **核心措施**                          | **关键行动示例**                          |  
|--------------|---------------------------------------|-------------------------------------------|  
| **短期**     | 合规与灵活性提升                      | - 加速煤电资产剥离或改造<br>- 布局储能+可再生能源混合项目 |  
| **中期**     | 技术突破与产业链重构                  | - 投资绿氢试点项目<br>- 建立碳资产管理团队           |  
| **长期**     | 系统性转型与生态构建                  | - 参与国际能源标准制定<br>- 打造零碳工业园           |  

---

#### **四、监控指标**  
- **短期**:政策变动频率、可再生能源装机增速  
- **中期**:CCUS成本下降曲线、绿氢产能占比  
- **长期**:全球温升目标进展、颠覆性技术专利数  

---  
**注**:★☆表示优先级,需结合企业具体业务板块调整权重(如油气企业需更高权重关注转型风险)。
协调者-工作者(Orchestrator-Workers)

在协调器-工作者工作流中,中央 LLM 动态分解任务,将其委托给工作者 LLM,并综合其结果。

此工作流程非常适合无法预测所需子任务的复杂任务。虽然它在拓扑结构上与并行化类似,但其与并行化的关键区别在于灵活性——子任务并非预先定义,而是由编排器根据具体输入确定。

示例:我要到访秘鲁,为我写一篇旅游规划书

首先明确一点,任务(用户输入)要先被 Orchestrator 动态分解,然后 Workers 再执行这些子任务,最后汇总结果

下面定义 OrchestratorWorkers 的 Prompt,添加到常量类中

    public static final String ORCHESTRATOR_PROMPT = """
         
    分析此任务并分解为3-4种不同的处理方式:
           
            任务:{task}
           
            请按以下JSON格式返回响应:
            \\{
            "analysis": "说明你对任务的理解以及哪些变化会很有价值。
                        重点关注每种方式如何服务于任务的不同方面。",
            "tasks": [
                \\{
                "type": "adventure",
                "description": "规划充满冒险元素的行程,包括徒步、露营等活动"
                \\},
                \\{
                "type": "luxury",
                "description": "设计高端奢华的行程,包含精品酒店和私人定制体验"
                \\}
            ]
            \\}
    """;

    public static final String WORKER_PROMPT = """
             根据以下要求生成内容:
                 任务:{original_task}
                 风格:{task_type}
                 指南:{task_description}
            """;

定义 OrchestratorResponse 实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrchestratorResponse {

    private String analysis;

    private List<Task> tasks;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Task {

        private String type;

        private String description;
    }

}

在接口中定义 startOrchestratorWorkerWorkflow 方法

List<String> startOrchestratorWorkerWorkflow(String userInput);

在实现类中,首先使用协调者动态分解任务,返回一个 orchestratorResponse,然后工作者会执行多个子任务,最后返回一个列表

@Override
public List<String> startOrchestratorWorkerWorkflow(String userInput) {

    // 协调者动态分解任务
    OrchestratorResponse orchestratorResponse = openAiChatClient.prompt()
            .user(u -> u.text(PromptConstant.ORCHESTRATOR_PROMPT)
                    .param("task", userInput))
            .call()
            .entity(OrchestratorResponse.class);

    LOGGER.info("\n" + "OrchestratorResponse: " + "Analysis---"
            + orchestratorResponse.getAnalysis() + "\n" + "Tasks---"
            + orchestratorResponse.getTasks());

    // 工作者执行多个子任务
    List<String> stringList = orchestratorResponse.getTasks().stream().map(
            task -> openAiChatClient.prompt()
                    .user(u -> u.text(PromptConstant.WORKER_PROMPT)
                            .param("original_task", userInput)
                            .param("task_type", task.getType())
                            .param("task_description", task.getDescription()))
                    .call()
                    .content()
    ).toList();

    LOGGER.info("Worker Output: " + stringList);
    return stringList;
}

测试,可以看到,Orchestrator 将任务分解成了 4 个子任务(adventure、luxury、cultural、family)

2025-05-13 18:03:32 [http-nio-8095-exec-1] INFO  c.o.m.s.impl.WorkflowServiceImpl - 
OrchestratorResponse: Analysis---理解任务为根据用户需求定制秘鲁旅游规划书,重点在于满足不同旅行偏好。
有价值的变体包括行程强度、预算水平、文化深度和活动类型,以覆盖冒险、奢华、文化和家庭等不同需求。
Tasks---[
OrchestratorResponse.Task(type=adventure, description=规划充满冒险元素的行程,包括徒步、露营等活动), 
OrchestratorResponse.Task(type=luxury, description=设计高端奢华的行程,包含精品酒店和私人定制体验), 
OrchestratorResponse.Task(type=cultural, description=聚焦秘鲁历史与文化的深度探索,涵盖博物馆、遗址和当地社区互动), 
OrchestratorResponse.Task(type=family, description=设计适合家庭出游的行程,包含亲子友好活动和舒适住宿)
]
Worker Output: [# **秘鲁冒险之旅规划书**  
**目的地:秘鲁**  
**旅行风格:探险 & 户外**  
**时长:10**  

## **行程概览**  
秘鲁是冒险者的天堂,拥有安第斯山脉、亚马逊雨林和古老的印加遗迹。本行程专注于徒步、露营、户外探险和文化体验,适合热爱挑战的旅行者。  

---

## **📅 详细行程**  

### **Day 1-2: 利马(Lima)→ 库斯科(Cuzco**  
- **抵达利马**,短暂休整,适应时差。  
- **飞往库斯科**(海拔3,400米),适应高原环境。  
- **库斯科探险准备**- 租借徒步装备(背包、登山杖、睡袋等)。  
  - 短途徒步至**Sacsayhuamán**(印加军事要塞),欣赏库斯科全景。  

### **Day 3-5: 圣谷(Sacred Valley)→ 奥扬泰坦博(Ollantaytambo**  
- **圣谷探险**- 骑行或徒步探索**Moray**(印加农业实验梯田)和**Maras盐田**- **白水漂流**Urubamba河,Class III-IV级急流)。  
- **奥扬泰坦博过夜**,体验传统印加小镇风情。  

### **Day 6-9: 印加古道徒步(Inca Trail)→ 马丘比丘(Machu Picchu**  
- **43夜印加古道徒步**(经典路线):  
  - **Day 1**:**Km 82**出发,徒步至**Wayllabamba**营地(12km)。  
  - **Day 2**: 挑战**Dead Woman’s Pass**(海拔4,215米),露营于**Pacaymayo**- **Day 3**: 探索**Runkurakay****Sayacmarca**遗址,夜宿**Wiñay Wayna**营地。  
  - **Day 4**: 黎明抵达**太阳门(Inti Punku**,俯瞰**马丘比丘**,全天深度探索。  
- **可选**:下山后泡**Aguas Calientes**温泉放松。  

### **Day 10: 返回库斯科 → 利马 → 返程**  
- **乘火车返回库斯科**,短暂休整。  
- **飞回利马**,结束冒险之旅。  

---

## **🏕️ 住宿推荐**  
- **库斯科**Wild Rover Hostel(背包客氛围)  
- **圣谷**Pisac Inn(生态旅馆)  
- **印加古道**:露营(需向导安排)  
- **马丘比丘**Belmond Sanctuary Lodge(唯一山巅酒店,可选奢侈体验)  

---

## **⚠️ 冒险贴士****高原适应**:提前2天到库斯科适应海拔,多喝水,避免剧烈运动。  
✅ **装备清单**:登山鞋、防水外套、头灯、防晒霜、水袋。  
✅ **向导要求**:印加古道必须跟持证向导团队进入,提前6个月预订许可。  
✅ **安全提示**:避免独自徒步偏远路线,随身携带急救包。  

---

## **🌿 可选扩展冒险**  
- **彩虹山(Vinicunca**1日徒步(海拔5,200米)。  
- **亚马逊雨林(Puerto Maldonado**3天丛林探险,观察野生动物。  

**📌 结语**  
秘鲁的冒险之旅将挑战你的体能,同时带来无与伦比的自然与文化震撼。准备好征服高山、穿越雨林,揭开印加帝国的神秘面纱吧!  

**🚀 出发吧,探险者!**, # **秘鲁奢华之旅规划书**  
**目的地:秘鲁**  
**旅行风格:高端定制 & 奢华体验**  
**时长:10**  

## **✨ 行程亮点**  
- **私人向导全程陪同**,VIP通道免排队  
- **顶级精品酒店 & 豪华列车**  
- **独家文化体验 & 美食盛宴**  
- **直升机游览 & 私人游艇**  

---

## **📅 尊享行程**  

### **Day 1-2: 利马(Lima)—— 美食之都的奢华初体验**  
- **抵达利马**,专车接送至**Belmond Miraflores Park**(海景套房)。  
- **私人城市导览**- 参观**Larco Museum**(秘鲁黄金与文物珍藏)。  
  - **VIP品酒会**,品尝秘鲁国酒Pisco Sour- **米其林星级晚餐**- **Central**(世界50佳餐厅)或**Maido**(日秘融合料理)。  

### **Day 3-4: 库斯科(Cuzco)—— 印加帝国的贵族之旅**  
- **私人飞机前往库斯科**,入住**Palacio del Inka, a Luxury Collection Hotel**16世纪宫殿改建)。  
- **圣谷(Sacred Valley)尊享体验**- **私人导游**陪同游览**Pisac市场****Ollantaytambo遗址**- **Belmond Hiram Bingham豪华列车**前往马丘比丘(含香槟午餐)。  
- **安第斯山野餐**:厨师现场烹饪,搭配本地葡萄酒。  

### **Day 5-6: 马丘比丘(Machu Picchu)—— 云端奇迹的私密探索**  
- **入住Belmond Sanctuary Lodge**(马丘比丘山巅唯一酒店)。  
- **VIP日出游览**:清晨私人开放,避开人群。  
- **直升机返程**(俯瞰安第斯山脉)。  

### **Day 7-8: 的的喀喀湖(Lake Titicaca)—— 水上漂浮宫殿**  
- **私人飞机前往普诺(Puno**,入住**Titilaka Lodge**(全包式湖畔别墅)。  
- **私人游艇游览**乌鲁斯浮岛(Uros Islands)。  
- **星空晚宴**:湖边私人厨师定制菜单。  

### **Day 9-10: 利马—— 完美收官**  
- **返回利马**,入住**Country Club Lima Hotel**(高尔夫度假风)。  
- **最后狂欢**- **私人购物导览**(秘鲁羊驼毛精品店)。  
  - **海滨直升机巡游**(俯瞰太平洋海岸线)。  
- **专车送机**,结束尊贵之旅。  

---

## **🏨 顶奢住宿推荐**  
| 城市          | 酒店                              | 特色                          |  
|---------------|-----------------------------------|-------------------------------|  
| **利马**      | Belmond Miraflores Park           | 无边泳池+太平洋全景           |  
| **库斯科**    | Palacio del Inka                  | 殖民时期宫殿改建              |  
| **马丘比丘**  | Belmond Sanctuary Lodge           | 遗址旁唯一五星级              |  
| **的的喀喀湖**| Titilaka Lodge                    | 全包式私人湖畔别墅            |  

---

## **🎁 独家增值服务**  
- **24小时管家** & 英语/中文私人导游  
- **行李无忧**:全程专人运送  
- **摄影跟拍**:专业摄影师记录旅程  
- **健康护航**:随行高原反应医护团队  

---

## **🍾 不可错过的奢华体验**  
1. **马丘比丘私人日出仪式**(萨满祈福)  
2. **库斯科私人庄园晚宴**(印加后裔家族接待)  
3. **亚马逊河私人游艇巡游**(需延长行程)  

**💎 设计说明**  
本行程通过航空接驳最大化节约时间,每个节点都注入秘鲁最顶级的资源。从世界级餐厅到只有2间客房的野奢营地,彻底重新定义奢华旅行。  

**🛎️ 定制提醒**  
所有服务均可按需调整,包括延长亚马逊行程、增加珠宝采购等个性化需求。我们的旅行设计师将为您1v1优化方案。  

**🌺 您值得拥有最完美的秘鲁!**, # **秘鲁文化深度之旅规划书**  
**目的地:秘鲁**  
**旅行风格:历史文化沉浸式体验**  
**时长:12**  

---

## **🌄 行程核心理念**  
本行程专为文化爱好者设计,通过博物馆、考古遗址、传统工艺作坊和原住民社区互动,深入探索秘鲁5000年文明史。重点呈现:  
✔ **前哥伦布时期文明**(莫切、纳斯卡、印加)  
✔ **西班牙殖民艺术与建筑****活态安第斯传统文化**  

---

## **📜 文化行程路线**  

### **Day 1-3: 利马——殖民瑰宝与秘鲁文明之源**  
#### **文化焦点**:混血文化(Mestizo)的形成  
- **国家博物馆(MALI)**:秘鲁艺术史通览  
- **圣弗朗西斯科修道院**:地下墓穴与殖民时期宗教艺术  
- **帕查卡马克遗址**:前印加神圣之城  
- **巴兰科区(Barranco**:街头艺术与文学咖啡馆  
- **特别体验**- **秘鲁国菜烹饪课**(学习CevichePisco Sour制作)  
  - **私人收藏家宅邸参观**(预览未公开的莫切文物)  

### **Day 4-6: 特鲁希略与昌昌古城——莫切文明深度行**  
#### **文化焦点**:沙漠中的古老帝国  
- **太阳神庙与月亮神庙**:莫切文明政治宗教中心  
- **昌昌古城**:世界最大土坯城(UNESCO)  
- **西潘王墓博物馆**:堪比图坦卡蒙的黄金宝藏  
- **特别体验**- **传统芦苇船出海**(与当代渔民交流古老航海技术)  
  - **陶艺大师工作坊**(学习莫切浮雕陶器制作)  

### **Day 7-9: 库斯科与圣谷——活着的印加帝国**  
#### **文化焦点**:印加智慧与当代克丘亚文化  
- **科里坎查(太阳神殿)**:印加建筑与殖民教堂的层叠  
- **圣谷(Pisac+Chinchero**:梯田系统与纺织合作社  
- **奥扬泰坦博**:仍在使用的印加城市规划范本  
- **特别体验**- **克丘亚家族共进午餐**(参与传统Pachamanca地灶烹饪)  
  - **安第斯星象解读**(原住民天文学家讲解)  

### **Day 10-12: 普诺与的的喀喀湖——浮岛上的乌罗斯人**  
#### **文化焦点**:高原湖泊文明  
- **乌鲁斯浮岛**:用芦苇再造生活的智慧  
- **塔基列岛**:世界非遗纺织社区  
- **西卢斯塔尼墓塔**:前印加生死观实证  
- **特别体验**- **夜宿浮岛民宿**(参与芦苇船制作)  
  - **Capachica半岛仪式**(与萨满共同祈福)  

---

## **🏛️ 核心文化站点解析**  
| **遗址/博物馆**       | **文明归属** | **不可错过亮点**                  |  
|-----------------------|--------------|-----------------------------------|  
| 西潘王墓博物馆       | 莫切文明     | 孔雀羽头饰/黄金葬礼面具           |  
| 奥扬泰坦bo水利系统   | 印加文明     | 至今运作的灌溉网络                |  
| 塔基列纺织合作社     | 当代克丘亚   | 用编织记录历史的密码文字          |  

---

## **🎭 文化互动日历****Day3傍晚**:利马传统Marinera舞蹈私教课  
▸ **Day6全天**:参与北海岸圣佩德罗仙人掌仪式(需提前精神准备)  
▸ **Day9夜间**:库斯科Q'eswachaka节(如逢6月,见证草绳桥重建仪式)  

---

## **🛌 文化住宿推荐**  
- **利马**Casa Republica19世纪共和时期豪宅改建)  
- **特鲁希略**Hotel Libertador(殖民时期总督府旧址)  
- **库斯科**El Mercado Tunqui(前印加市场改造的设计酒店)  

---

## **📚 行前文化准备建议**  
1. **阅读清单**- 《印加帝国的末日》(Kim MacQuarrie- 《莫切文明的暴力与仪式》(Steve Bourget2. **影视推荐**:纪录片《秘鲁:隐藏的王国》(BBC)  
3. **语言基础**:学习10个克丘亚语问候语  

---

## **💡 专业贴士**  
- **摄影伦理**:拍摄原住民前务必征得同意(建议携带宝丽来即时赠送)  
- **纪念品采购**:库斯科Centro Qosqo认证的公平贸易商店  
- **学术支持**:可预约随行考古学家(需提前2月预定)  

**🌾 这不仅仅是一次旅行,而是一场文明对话。**  
从沙漠金字塔到漂浮岛屿,让我们沿着时间的纤维,触摸秘鲁文明的温度。, # **秘鲁家庭欢乐之旅规划书**  
**目的地:秘鲁**  
**旅行风格:亲子友好 & 家庭休闲**  
**时长:10**  

---

## **👨‍👩‍👧‍👦 行程特色****轻松节奏**:每天1个主要景点+充足休息时间  
✔ **趣味学习**:互动式文化体验激发孩子好奇心  
✔ **安全舒适**:家庭房住宿+专业儿童餐食  
✔ **交通优化**:包车服务+短途内陆航班  

---

## **📅 亲子行程安排**  

### **Day 1-2: 利马(Lima)—— 海滨初体验**  
- **住宿****Sheraton Lima Hotel & Convention Center**(家庭连通房,儿童泳池)  
- **活动**- **魔法水公园(Parque de la Reserva**:世界最大喷泉综合体夜间灯光秀  
  - **拉尔科博物馆(Larco Museum**:儿童专用讲解器+巧克力制作工坊  
  - **米拉弗洛雷斯海滨步道**:骑四轮协力车+品尝儿童友好版酸橘汁腌鱼  

### **Day 3-4: 帕拉卡斯(Paracas)—— 海洋奇遇**  
- **交通**:私人包车(3小时,配备儿童安全座椅)  
- **住宿****DoubleTree Resort by Hilton Paracas**(私人沙滩+儿童俱乐部)  
- **活动**- **鸟岛游船(Ballestas Islands**:看海狮/企鹅/海鸟(提供儿童望远镜)  
  - **沙漠越野车**:小型沙丘滑沙体验(5岁以上可参与)  
  - **生态手工课**:用贝壳制作纪念品  

### **Day 5-7: 库斯科(Cuzco)—— 印加探险**  
- **交通**1小时航班(选择上午班次减少疲劳)  
- **住宿****Novotel Cusco**(高原供氧系统+家庭游戏室)  
- **活动**- **圣谷小火车**:乘坐全景列车前往皮萨克(Pisac)集市  
  - **羊驼农场**:喂食+学习传统纺织(提供儿童尺寸纺织工具)  
  - **巧克力博物馆**:从可可豆到成品的互动体验(可制作专属巧克力)  
  - **儿童版印加古道**2小时轻松徒步到Moray圆形梯田  

### **Day 8-9: 马丘比丘(Machu Picchu)—— 奇幻之旅**  
- **交通**Vistadome景观列车(车厢魔术表演+儿童餐)  
- **住宿****Tierra Viva Machu Picchu**(家庭套房+热水浴缸)  
- **活动**- **马丘比丘寻宝游戏**:定制儿童探险手册完成打卡任务  
  - **温泉镇手工坊**:用天然粘土制作迷你印加文物  
  - **夜间故事会**:酒店安排克丘亚语童话讲述  

### **Day 10: 返回利马 —— 回忆封存**  
- **活动**- **家庭旅行相册DIY**:市区专业工作室1小时快制  
  - **拉尔科博物馆儿童证书**:完成所有文化挑战可获得  

---

## **🍽️ 亲子餐饮推荐**  
| **城市**   | **餐厅**                  | **特色**                      |  
|------------|---------------------------|-------------------------------|  
| 利马       | Panchita                   | 儿童餐含可食用乐高积木        |  
| 库斯科     | Papacho's                  | 印加主题汉堡+自制柠檬水站     |  
| 马丘比丘   | Tree House                 | 树屋座位+动物造型甜点         |  

---

## **🎒 行前准备清单**  
- **健康**- 儿科医生开具高原反应预防建议(库斯科海拔3400米)  
  - 准备儿童常用药+便携式血氧仪  
- **装备**- 可折叠婴儿车(鹅卵石路面适用)  
  - 亲子装(当地节日拍照更出片)  
- **教育**- 下载《印加文明儿童绘本》电子版  
  - 准备空白旅行日记本收集印章  

---

## **🌟 特别关怀服务**  
- **机场快速通道**:利马/库斯科机场VIP通关(避免排队)  
- **灵活调整权**:每天可免费取消1项活动(根据孩子状态)  
- **应急支持**24小时中文保姆服务(需提前48小时预约)  

---

## **📌 家长须知**  
1. **最佳季节**5-9月(旱季,适合户外活动)  
2. **年龄建议**5岁以上儿童体验更完整  
3. **文化礼仪**:提前教孩子用克丘亚语说"谢谢"Sulpayki**🦙 让羊驼见证家庭的成长之旅!**  
从海岸到高山,从古代文明到自然奇观,这将是孩子们终身难忘的南美课堂。]
评估者-优化器(Evaluator-Optimizer)

在评估者-优化器工作流中,一个 LLM 调用生成响应,而另一个调用在循环中提供评估和反馈

该模式对于翻译、代码生成等场景十分适用

示例: 通过评估优化实现一个 Java 队列代码生成

下面定义 Generator 和 的 Evaluator 的 Prompt,添加到常量类中

public static final String GENERATOR_PROMPT = 
        """
            你的目标是根据输入完成任务。如果存在之前生成的反馈,
            你应该反思这些反馈以改进你的解决方案。
            
            关键要求:响应必须是单行有效的JSON,除明确使用\\n转义外,不得包含换行符。
            以下是必须严格遵守的格式(包括所有引号和花括号):
            
            {"thoughts":"此处填写简要说明","code":"public class Example {\\n    // 代码写在这里\\n}"}
            
            响应字段的规则:
            1. 所有换行必须使用\\n
            2. 所有引号必须使用\\"
            3. 所有反斜杠必须双写:\\
            4. 不允许实际换行或格式化 - 所有内容必须在一行
            5. 不允许制表符或特殊字符
            6. Java代码必须完整且正确转义
            
            正确格式的响应示例:
            {"thoughts":"实现计数器","code":"public class Counter {\\n    private int count;\\n    public Counter() {\\n        count = 0;\\n    }\\n    public void increment() {\\n        count++;\\n    }\\n}"}
            
            必须严格遵循此格式 - 你的响应必须是单行有效的JSON。
        """;

public static final String EVALUATOR_PROMPT = 
        """
            评估这段代码实现的正确性、时间复杂度和最佳实践。
            确保代码有完整的javadoc文档。
            用单行JSON格式精确响应:
            
            {"evaluation":"PASS,NEEDS_IMPROVEMENT,FAIL", "feedback":"你的反馈意见"}
            
            evaluation字段必须是以下之一: "PASS", "NEEDS_IMPROVEMENT", "FAIL"
            仅当所有标准都满足且无需改进时才使用"PASS"。
        """;

在接口中定义 startEvaluatorOptimizerWorkflow 方法

String startEvaluatorOptimizerWorkflow(String userInput);

在实现类中,定义 loop 方法,在该方法通过 generate 生成代码,evaluate 评估生成的代码,若评估不通过,则进行优化(再次执行loop)

@Override
public String startEvaluatorOptimizerWorkflow(String userInput) {
    List<String> memory = new ArrayList<>();
    List<GenerationResponse> chainOfThought = new ArrayList<>();

    return loop(userInput, "", memory, chainOfThought).toString();
}

public RefinedResponse loop(String userInput, String context, List<String> memory, List<GenerationResponse> chainOfThought) {

    // Generator生成代码
    GenerationResponse generationResponse = generate(userInput, context);

    chainOfThought.add(generationResponse);
    memory.add(generationResponse.getCode());

    // Evaluator评估代码
    EvaluationResponse evaluationResponse = evaluate(userInput, generationResponse.getCode());

    // 若评估通过,则返回RefinedResponse
    if (Evaluation.PASS.equals(evaluationResponse.getEvaluation())){
        return new RefinedResponse(generationResponse.getCode(), chainOfThought);
    }

    // 若评估不通过,则进行优化(再次执行loop)
    StringBuilder newContext = new StringBuilder();
    newContext.append("之前的尝试:");
    for (String m : memory) {
        newContext.append("\n- ").append(m);
    }
    newContext.append("\nFeedback: ").append(evaluationResponse.getFeedback());

    return loop(userInput, newContext.toString(), memory, chainOfThought);
}

public GenerationResponse generate(String userInput, String context) {
    GenerationResponse generationResponse = openAiChatClient.prompt()
            .user(u -> u.text("{prompt}\n{context}\nTask: {task}")
                    .param("prompt", PromptConstant.GENERATOR_PROMPT)
                    .param("context", context)
                    .param("task", userInput))
            .call()
            .entity(GenerationResponse.class);
    System.out.println(String.format("\n=== 输出 ===\n思考: %s\n\n代码:\n %s\n",
            generationResponse.getThoughts(), generationResponse.getCode()));
    return generationResponse;
}

public EvaluationResponse evaluate(String userInput, String code) {
    EvaluationResponse evaluationResponse = openAiChatClient.prompt()
            .user(u -> u.text("{prompt}\nOriginal task: {task}\nContent to evaluate: {content}")
                    .param("prompt", PromptConstant.EVALUATOR_PROMPT)
                    .param("task", userInput)
                    .param("content",code))
            .call()
            .entity(EvaluationResponse.class);
    System.out.println(String.format("\n=== 评价输出 ===\n评价: %s\n\n反馈: %s\n",
            evaluationResponse.getEvaluation(), evaluationResponse.getFeedback()));
    return evaluationResponse;
}

测试

@GetMapping("/agent/generator_evaluator")
public String generatorEvaluator() {
    String userInput =
    """
    实现一个具有以下功能的Java队列:
         1. enqueue(x) - 将元素x添加到队列尾部
         2. dequeue() - 移除并返回队列头部元素
         3. getMin() - 获取队列中的最小值
         所有操作的时间复杂度应为O(1)。
         所有内部字段必须声明为private,使用时需加"this."前缀。
    """;
    return workflowService.startEvaluatorOptimizerWorkflow(userInput);
}
=== 输出 ===
思考: 使用双端队列维护最小值实现O(1)操作

代码:
 public class MinQueue {
    private java.util.Queue<Integer> mainQueue;
    private java.util.Deque<Integer> minDeque;
    public MinQueue() {
        this.mainQueue = new java.util.LinkedList<>();
        this.minDeque = new java.util.LinkedList<>();
    }
    public void enqueue(int x) {
        this.mainQueue.add(x);
        while (!this.minDeque.isEmpty() && this.minDeque.getLast() > x) {
            this.minDeque.removeLast();
        }
        this.minDeque.addLast(x);
    }
    public int dequeue() {
        if (this.mainQueue.isEmpty()) throw new RuntimeException("Queue is empty");
        int val = this.mainQueue.remove();
        if (val == this.minDeque.getFirst()) {
            this.minDeque.removeFirst();
        }
        return val;
    }
    public int getMin() {
        if (this.minDeque.isEmpty()) throw new RuntimeException("Queue is empty");
        return this.minDeque.getFirst();
    }
}


=== 评价输出 ===
评价: NEEDS_IMPROVEMENT

反馈: 实现正确且满足时间复杂度要求,但缺少Javadoc文档和更具体的异常处理。建议添加完整的类和方法文档,并使用更具体的异常类型如NoSuchElementException=== 输出 ===
思考: 改进后的实现添加了Javadoc文档和使用NoSuchElementException

代码:
 public class MinQueue {
    private java.util.Queue<Integer> mainQueue;
    private java.util.Deque<Integer> minDeque;
    /**
     * 构造一个新的MinQueue
     */
    public MinQueue() {
        this.mainQueue = new java.util.LinkedList<>();
        this.minDeque = new java.util.LinkedList<>();
    }
    /**
     * 将元素添加到队列尾部
     * @param x 要添加的元素
     */
    public void enqueue(int x) {
        this.mainQueue.add(x);
        while (!this.minDeque.isEmpty() && this.minDeque.getLast() > x) {
            this.minDeque.removeLast();
        }
        this.minDeque.addLast(x);
    }
    /**
     * 移除并返回队列头部元素
     * @return 队列头部元素
     * @throws java.util.NoSuchElementException 如果队列为空
     */
    public int dequeue() {
        if (this.mainQueue.isEmpty()) throw new java.util.NoSuchElementException("Queue is empty");
        int val = this.mainQueue.remove();
        if (val == this.minDeque.getFirst()) {
            this.minDeque.removeFirst();
        }
        return val;
    }
    /**
     * 获取队列中的最小值
     * @return 队列中的最小值
     * @throws java.util.NoSuchElementException 如果队列为空
     */
    public int getMin() {
        if (this.minDeque.isEmpty()) throw new java.util.NoSuchElementException("Queue is empty");
        return this.minDeque.getFirst();
    }
}


=== 评价输出 ===
评价: PASS

反馈: 实现完全符合要求,包括正确的时间复杂度、私有字段访问、完善的Javadoc文档和适当的异常处理。

Autonomous Agent(Agent

TODO

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Uranus^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值