【图文详解】基于Spring AI的旅游大师应用开发、多轮对话、文件持久化、拦截器实现

本文涉及的产品
多模态交互后付费免费试用,全链路、全Agent
简介: 【图文详解】基于Spring AI的旅游大师应用开发、多轮对话、文件持久化、拦截器实现

目录

一、应用概述

  • 本 AI 旅游大师应用聚焦多轮对话能力构建,致力于为用户提供专业、贴心的旅游咨询服务,涵盖行程规划、目的地推荐、游玩贴士解答等场景,助力用户打造优质旅行体验。

1.1、核心要点设计

1.1.1、系统提示词

  • 系统提示词是 AI 应用的 “灵魂”,决定 AI 行为模式、专业性与交互风格 。
  • 1、基础定义示例
你是一位旅游大师,为用户提供专业旅游咨询服务,涵盖行程规划、目的地推荐、游玩问题解答等
  • 2、进阶优化思路

现实中用户咨询旅游问题时,专业 “旅游大师” 会主动引导挖掘需求。因此系统提示词可设计为,让 AI 主动抛出引导性问题,深入了解用户背景,示例引导问题:

你最近有没有特别想去旅行的目的地类型呀,比如自然风光、历史人文还是海滨度假?
这次旅行你大概计划安排几天时间呢?
同行人员有什么特点不,像亲子、情侣、朋友结伴,这会影响行程规划哦~

通过这类引导,能更精准捕捉用户需求,输出贴合的旅游方案。

1.1.2、多轮对话记忆

  • 参考 Spring AI 官方文档,Spring AI 提供 ChatClient API 与 AI 大模型交互。相较于之前直接用 Spring Boot 注入 ChatModel 调用大模型,自行构造 ChatClient 可打造功能更丰富、灵活的 AI 对话客户端,推荐以此方式调用 AI ,支持复杂灵活的链式调用Fluent API 。
    在这里插入图片描述

  • 简要流程说明:

    • 初始化 ChatClient ,配置与大模型的连接参数(如模型地址、API 密钥等,需结合实际使用的大模型平台)。
    • 在对话过程中,利用 ChatClient 记录上下文信息,每次用户新输入,结合历史对话内容一起发送给大模型,确保 AI 理解连续对话逻辑。
    • 解析大模型返回结果,整理成自然流畅、贴合旅游咨询场景的回复,回应用户,持续推进多轮对话。

1.2、ChatClient VS ChatModel

1.2.1、链式调用

// 基础用法(ChatModel)
ChatResponse response = chatModel.call(new Prompt("你好"));

// 高级用法(ChatClient)
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultSystem("你是旅游顾问")
    .build();

String response = chatClient.prompt().user("你好").call().content();

1.2.2、ChatClient的多种构建方式

// 方式1:使用构造器注入
@Service
public class ChatService {
    private final ChatClient chatClient;

    public ChatService(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("你是旅游顾问")
            .build();
    }
}

// 方式2:使用建造者模式
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultSystem("你是旅游顾问")
    .build();

1.2.3、ChatClient的多种方式

  • 基于 Spring AI ,需提前引入相关依赖,如 spring - ai - openai 等,且配置好大模型 API 密钥 ),涵盖系统提示词、多轮对话、ChatClient 响应格式等核心逻辑
  public static void main(String[] args) {
        // 1. 初始化 ChatModel(需替换为实际大模型配置,如 OpenAI、通义千问等)
        ChatModel chatModel = initializeChatModel();  // 2. 构建 ChatClient,配置默认系统提示词(旅游大师角色)
        ChatClient chatClient = ChatClient.builder(chatModel)
               .defaultSystem("You are a professional travel master, providing travel consultation services. " +
                        "Offer itinerary planning, destination recommendations, and travel tips. " +
                        "Interact in a {voice} style.") 
               .build();
        // 3. 动态调整系统提示词变量(设置交互风格为 “专业且热情”)
        String voiceStyle = "professional and enthusiastic";

        // 4. 示例 1:返回 ChatResponse(含元数据,如 token 用量)
        ChatResponse responseWithMeta = chatClient.prompt()
               .system(sp -> sp.param("voice", voiceStyle)) 
               .user("帮我规划去冰岛 7 天的旅行行程,包含极光观测、冰川徒步") 
               .call()
               .chatResponse();
        System.out.println("=== 示例 1:ChatResponse 输出 ===");
        System.out.println("AI 回复内容:" + responseWithMeta.getResult().getOutput().getContent());
        System.out.println("本次调用消耗 token:" + responseWithMeta.getUsage().getTotalTokens());

        // 5. 示例 2:返回单个实体对象(自动映射 AI 输出)
        DestinationRecommend singleEntity = chatClient.prompt()
               .system(sp -> sp.param("voice", voiceStyle))
               .user("推荐一个适合秋季旅行的国内城市,说明理由和必去景点") 
               .call()
               .entity(DestinationRecommend.class);
        System.out.println("\n=== 示例 2:单个实体对象输出 ===");
        System.out.println("推荐城市:" + singleEntity.city());
        System.out.println("推荐理由:" + singleEntity.reason());
        System.out.println("必去景点:" + String.join(", ", singleEntity.attractions()));

        // 6. 示例 3:返回泛型集合(多城市玩法推荐)
        List<CityActivity> multiEntity = chatClient.prompt()
               .system(sp -> sp.param("voice", voiceStyle))
               .user("分别推荐三亚(夏季)、哈尔滨(冬季)的特色玩法") 
               .call()
               .entity(new ParameterizedTypeReference<List<CityActivity>>() {});
        System.out.println("\n=== 示例 3:泛型集合输出 ===");
        multiEntity.forEach(ca -> 
                System.out.println(ca.city() + " 特色玩法:" + ca.activity())
        );

        // 7. 示例 4:流式返回文本内容(模拟打字机效果)
        Flux<String> streamText = chatClient.prompt()
               .system(sp -> sp.param("voice", voiceStyle))
               .user("详细描述丝绸之路自驾 15 天的行程故事,包含沿途文化、美食、风景") 
               .stream()
               .content();
        System.out.println("\n=== 示例 4:流式文本输出 ===");
        streamText.subscribe(
                segment -> System.out.print(segment), 
                error -> System.err.println("流式失败:" + error)
        );

        // 8. 示例 5:流式返回 ChatResponse(含元数据)
        Flux<ChatResponse> streamWithMeta = chatClient.prompt()
               .system(sp -> sp.param("voice", voiceStyle))
               .user("详细介绍京都全年不同季节的旅行亮点") 
               .stream()
               .chatResponse();
        System.out.println("\n=== 示例 5:流式 ChatResponse 输出 ===");
        streamWithMeta.subscribe(
                chatResp -> {
                    String segment = chatResp.getResult().getOutput().getContent();
                    int token = chatResp.getUsage().getTotalTokens();
                    System.out.println("片段内容:" + segment + "(本段 token:" + token + ")");
                },
                error -> System.err.println("流式失败:" + error)
        );
    /**
     * 初始化 ChatModel(需替换为实际大模型配置,以下为伪代码示例)
     * 真实场景中,需引入对应大模型依赖,配置 API 密钥、模型名称等
     */
    private static ChatModel initializeChatModel() {
        // 示例:若使用 OpenAI,需配置 apiKey、baseUrl 等
        // return new OpenAiChatModel(apiKey, baseUrl, modelName); 
        // 此处为简化演示,返回空实现(实际需替换为真实逻辑)
        return new ChatModel() {}; 
    }

1.3、Advisor

  • Advisor(顾问 / 拦截器 )机制的执行流程与关键要点:
    • 执行流程:从用户 Prompt 构建 AdvisedRequest 和 AdvisorContext,经 Advisor 链处理(可能修改请求、决定是否传递 ),最终由框架指定 Advisor 发请求给 ChatModel;响应回传经 Advisor 链转为 AdvisedResponse,再处理后返回客户端。
    • 拦截器特性:多个拦截器组合成链条(类似责任链模式 ),执行顺序由 getOrder 方法决定,而非代码编写顺序,开发中常用多个拦截器增强 AI 能力(如 MessageChatMemoryAdvisor 管理对话记忆、QuestionAnswerAdvisor 做 RAG 检索 ) 。

在这里插入图片描述
在这里插入图片描述

1.3.1、Advisors 机制作用及示例

  • 1、作用:初始Spring AI 的 Advisors(顾问 / 拦截器) 可在 调用 AI 前 / 后 执行增强逻辑,适配旅游大师场景:

    • 前置增强:改写 Prompt(如补充旅游知识库检索结果、校验提示词安全性 )。
    • 后置增强:记录对话日志(如用户咨询偏好、AI 回复质量 )、处理返回结果(如格式化行程推荐 )。
  • 2、 示例:MessageChatMemoryAdvisor和QuestionAnswerAdvisor

    • MessageChatMemoryAdvisor:维持多轮对话记忆,自动关联历史咨询(如问 “冰岛行程调整”,AI 能基于之前的 7 天规划回复 )
    • QuestionAnswerAdvisor:结合向量存储(RAG),检索旅游知识库(如景点开放时间、签证政策 ),补充 Prompt 回复准确性。
var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        new MessageChatMemoryAdvisor(chatMemory), // 对话记忆 advisor
        new QuestionAnswerAdvisor(vectorStore)    // RAG 检索增强 advisor
    )
    .build();

String response = this.chatClient.prompt()
    // 对话时动态设定拦截器参数,比如指定对话记忆的 id 和长度
    .advisors(advisor -> advisor.param("chat_memory_conversation_id", "4548")
            .param("chat_memory_response_size", 10))
    .user(userText)
    .call()
    .content();

1.3.1、Advisors 分类

  • 两种模式:Advisors 分流式(Streaming )和非流式(Non - Streaming ),用法差异小,返回值不同;若自定义实现,建议同时覆盖两种模式以保证通用性。
  • 执行流程:图示呈现非流式(基于 CallAroundAdvisor )与流式(基于 StreamAroundAdvisor )的请求处理流程,均经 Advisor 链与 Chat Model 交互。
    在这里插入图片描述
  • 对话记忆实现:介绍 ChatMemoryAdvisor 的几种内置方式。一般更推荐 MessageChatMemoryAdvisor,因契合现代大语言模型(LLM )对话模型设计,利于维持上下文连贯性 。
    • MessageChatMemoryAdvisor(历史对话作消息集加提示词 ):把对话历史以独立消息形式(含角色标识,如用户、助手 )添加到提示,保留原始对话结构,示例 JSON 展示多轮消息及角色。
    • PromptChatMemoryAdvisor(历史对话加系统文本 ):将对话历史加入提示的系统文本,可能丢失原始消息边界,示例呈现拼接后的系统文本格式。
  • ChatMemory:是 ChatMemoryAdvisor 依赖的对话历史存储组件,定义了添加、查询、清空消息的方法(如 add 存消息、get 查消息、clear 清空 )。提供多种存储方式,包括:
    • InMemoryChatMemory(内存存 )
    • CassandraChatMemory(Cassandra 持久化,带过期 )
    • Neo4jChatMemory(Neo4j 持久化,无过期 )
    • JdbcChatMemory(JDBC 持久化,无过期 )
    • 也支持自定义实现 ChatMemory 接口扩展存储。

二、多轮对话应用开发

2.1、多轮对话记忆

  • 参考 Spring AI Alibaba 示例(实际用 Spring AI )
    在这里插入图片描述
  • 初始化逻辑:用 Spring 构造器注入阿里大模型 dashscopeChatModel,初始化 ChatClient;指定系统 Prompt(领域专家角色与服务范围 ),配置基于内存的对话记忆 InMemoryChatMemory 和 MessageChatMemoryAdvisor ,实现多轮对话能力。
public class TravelMasterApp {

    private final ChatClient chatClient;

    // 系统提示词:定义旅游大师角色与服务逻辑
    private static final String SYSTEM_PROMPT = "扮演深耕旅游规划领域的专家。开场向用户表明身份,告知可咨询旅行难题。" +
            "围绕独行、结伴、家庭游三种场景提问:独行关注安全保障、小众玩法探索困扰;" +
            "结伴关注行程协调、成员意见分歧矛盾;家庭游关注亲子/长辈适配项目、行程节奏问题。" +
            "引导用户详述旅行意向、人员构成、特殊需求,以便定制专属旅行方案。";

    public TravelMasterApp(ChatModel dashscopeChatModel) {
        // 初始化基于内存的对话记忆,存储多轮对话上下文
        ChatMemory chatMemory = new InMemoryChatMemory();
        // 构建 ChatClient,配置系统提示词、对话记忆拦截器
        chatClient = ChatClient.builder(dashscopeChatModel)
                .defaultSystem(SYSTEM_PROMPT) // 注入旅游大师系统提示词
                .defaultAdvisors(
                        new MessageChatMemoryAdvisor(chatMemory) // 对话记忆拦截器,维持多轮上下文
                )
                .build();
    }

    // 可扩展对话方法,如接收用户提问并获取 AI 回复
    public String getTravelAdvice(String userQuestion) {
        return chatClient.prompt()
                .user(userQuestion)
                .call()
                .content();
    }
}
  • doChat 方法封装旅游咨询的对话逻辑,接收用户提问 message 和对话唯一标识 chatId,调用 ChatClient 与大模型交互,返回 AI 生成的旅游建议。
  • 关键参数控制:
    • CHAT_MEMORY_CONVERSATION_ID_KEY(chatId ):关联多轮对话记忆,确保用户后续追问(如 “刚才的川西行程能加个寺庙景点吗?” )时,AI 能基于历史对话回复。
    • CHAT_MEMORY_RETRIEVE_SIZE_KEY(值为 10 ):控制从对话记忆中检索的历史消息长度(单位一般为 token 或消息条数,按需调整 ),避免记忆过多导致回复冗余。
 /**
     * 处理用户旅游咨询的对话方法
     * @param message 用户提问内容(如“求川西 5 天亲子游行程”)
     * @param chatId  对话唯一标识(用于关联多轮对话记忆,如“TRAVEL_PARENT_202408”)
     * @return AI 生成的旅游建议回复
     */
    public String doChat(String message, String chatId) {
        ChatResponse response = chatClient.prompt()
                .user(message) // 传入用户旅游咨询内容
                .advisors(spec -> spec
                        .param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) // 指定对话记忆 ID,关联多轮对话
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 20)      // 控制对话记忆检索长度(按需调整)
                )
                .call() // 发起 AI 调用
                .chatResponse(); // 获取包含元数据的完整响应

        String content = response.getResult().getOutput().getText();
        log.info("用户提问:{},AI 回复:{}", message, content);
        return content;
    }
}

2.2、自定义advisor

  • Advisor 类似 Servlet 拦截器、Spring AOP 切面,可增强 AI 调用(如前置鉴权、后置日志 )。官方Advisor 难满足业务时,可自定义扩展。
  • 实现步骤:需选接口实现。
    • 建议同时实现 CallAroundAdvisor(处理同步请求响应,非流式 )。
    • StreamAroundAdvisor(处理流式请求响应 ),以覆盖不同交互场景 。
    • 执行顺序:实现 getOrder 方法指定优先级,值越小越先执行 。
public class MyCustomAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
    // 实现方法...
}
  • 非流式(CallAroundAdvisor):实现 aroundCall 方法,分三步:前置处理请求(processRequest )、调用下一个 Advisor(chain.nextAroundCall )、后置处理响应(processResponse )。
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
    // 1. 处理请求(前置处理)
    AdvisedRequest modifiedRequest = processRequest(advisedRequest);

    // 2. 调用链中的下一个Advisor
    AdvisedResponse response = chain.nextAroundCall(modifiedRequest);

    // 3. 处理响应(后置处理)
    return processResponse(response);
}
  • 流式(StreamAroundAdvisor):实现 aroundStream 方法,先处理请求,再通过 chain.nextAroundStream 调用后续 Advisor,并用 map 处理流式响应。
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
    // 1. 处理请求
    AdvisedRequest modifiedRequest = processRequest(advisedRequest);

    // 2. 调用链中的下一个Advisor并处理流式响应
    return chain.nextAroundStream(modifiedRequest)
               .map(response -> processResponse(response));
}
  • 执行顺序:实现 getOrder 方法指定优先级,值越小越先执行 。
@Override
public int getOrder() {
    // 值越小优先级越高,越先执行
    return 100; 
}

2.2.1、自定义日志advisor拦截器

  • 1、Spring AI 的 SimpleLoggerAdvisor 日志拦截器
    • 默认问题:内置的 SimpleLoggerAdvisor 用 Debug 级别输出日志,而 Spring Boot 项目默认日志级别是 Info,导致日志看不到。
    • 解决方法:修改配置文件(yaml ),指定 org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor 的日志级别为 debug ,即可显示该拦截器的日志 。
    • 进阶建议:为更灵活控制,可参考官方文档和其源码,自定义日志 Advisor,实现如 Info 级别输出、仅记录用户提问和 AI 回复文本等需求 。
logging:
  level:
    org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor: debug
  • 2、核心功能与设计
    • 实现 CallAroundAdvisor(处理同步请求 - 响应 )和 StreamAroundAdvisor(处理流式请求 - 响应 ),作为 Spring AI 调用链中的拦截器,用于增强日志打印逻辑。用 @Slf4j 输出 INFO 级别日志,仅聚焦用户提问和 AI 回复文本,相比内置 SimpleLoggerAdvisor 更简洁、贴合业务需求。
    • 核心方法职责:
      • before:前置处理,打印用户输入的 prompt(request.userText() ),记录 AI 请求内容。
      • observeAfter:后置处理,打印 AI 回复文本(advisedResponse.response().getResult().getOutput().getText() ),记录 AI 响应内容。
      • aroundCall:同步调用链路逻辑,串联 before 前置处理 → 调用下一个 Advisor → observeAfter 后置处理。
      • aroundStream:流式调用链路逻辑,串联 before 前置处理 → 调用下一个 Advisor → 通过 MessageAggregator 聚合响应并执行 。
      • observeAfter 后置处理。
/**
 * 自定义日志 Advisor
 * 打印 info 级别日志、只输出单次用户提示词和 AI 回复的文本
 */
@Slf4j
public class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {

    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }

    @Override
    public int getOrder() {
        return 0;
    }

    private AdvisedRequest before(AdvisedRequest request) {
        log.info("AI提问: {}", request.userText());
        return request;
    }

    private void observeAfter(AdvisedResponse advisedResponse) {
        log.info("AI 响应: {}", advisedResponse.response().getResult().getOutput().getText());
    }

    public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
        advisedRequest = this.before(advisedRequest);
        AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);
        this.observeAfter(advisedResponse);
        return advisedResponse;
    }

    public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
        advisedRequest = this.before(advisedRequest);
        Flux<AdvisedResponse> advisedResponses = chain.nextAroundStream(advisedRequest);
        // aroundStream 方法利用 MessageAggregator 工具类将 Flux 流式响应聚合成单个 AdvisedResponse,用于需要观测完整响应的场景.
        return (new MessageAggregator()).aggregateAdvisedResponse(advisedResponses, this::observeAfter);
    }
}

2.3、结构化输出

2.3.1 基本原理与流程

  • 1、流程
    • 调用大模型前,转换器在提示词附加格式指令,指导模型生成指定结构响应;
    • 调用后,将模型文本输出转换为 JSON、XML 等结构化数据实例 。
    • 使用说明:其仅 “尽最大努力” 转换模型输出为结构化数据,因 AI 模型可能无法理解指令或生成符合要求的内容,故建议在程序中实现验证、异常处理机制,确保输出符合预期 。
      在这里插入图片描述
  • 2、原理围绕 Spring AI 的 StructuredOutputConverter 接口展开

    • 接口定义:StructuredOutputConverter 继承 Converter 和 FormatProvider,用于获取结构化输出(如映射到 Java 类、数组 )。
    • 集成接口:
      • FormatProvider:提供格式指令给 AI 模型,通过 getFormat 方法实现。
      • Converter:将模型文本输出转为指定目标类型 T。
      • 转换器实现:Spring AI 提供多种实现,包括 AbstractConversionServiceOutputConverter(用预配置转换服务 )、BeanOutputConverter(转 Java Bean )、MapOutputConverter(转 Map )等,满足不同结构化输出需求 。
        在这里插入图片描述
    • 3、Spring AI 提供的多种结构化输出转换器及其功能:
      • AbstractConversionServiceOutputConverter:借预配置 GenericConversionService 转换 LLM 输出。--
      • AbstractMessageOutputConverter:支持 Spring AI Message 转换。
      • BeanOutputConverter:基于 ObjectMapper 转输出为 Java Bean。
      • MapOutputConverter:转输出为 Map 结构。
      • ListOutputConverter:转输出为 List 结构 。
        在这里插入图片描述
  • 4、官方示例
    在这里插入图片描述

    • BeanOutputConverter:定义 ActorsFilms 记录类,通过 ChatClient 调用大模型,用 .entity 结合类或 ParameterizedTypeReference,将 AI 输出转为 Java 类或其列表(如 List )。
    • MapOutputConverter:借助 ChatClient 发起请求,通过 ParameterizedTypeReference>,将模型输出转为含数字列表等的 Map 结构。
    • ListOutputConverter:利用 ChatClient,结合 ListOutputConverter 和 DefaultConversionService,把模型输出转为字符串列表(如冰淇淋口味列表 ) 。
// 定义一个记录类
record ActorsFilms(String actor, List<String> movies) {}

// 使用高级 ChatClient API
ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
        .user("Generate 5 movies for Tom Hanks.")
        .call()
        .entity(ActorsFilms.class);
Map<String, Object> result = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("Provide me a List of {subject}")
                    .param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'"))
        .call()
        .entity(new ParameterizedTypeReference<Map<String, Object>>() {});
List<String> flavors = ChatClient.create(chatModel).prompt()
                .user(u -> u.text("List five {subject}")
                            .param("subject", "ice cream flavors"))
                .call()
                .entity(new ListOutputConverter(new DefaultConversionService()));

2.3.2 旅游报告实践

  • 1、引入依赖。
<dependency>
    <groupId>com.github.victools</groupId>
    <artifactId>jsonschema-generator</artifactId>
    <version>4.38.0</version>
</dependency>
  • 2、根据record特性快速定义旅游报告类。
record TravelReport(String title, List<String> suggestions) {
}
  • 3、使用ChatClient对象 。
public LoveReport doChatWithReport(String message, String chatId) {
    LoveReport loveReport = chatClient
            .prompt()
            .system(SYSTEM_PROMPT + "每次对话后都要生成旅游结果,标题为{用户名}的旅游报告,内容为建议列表")
            .user(message)
            .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                    .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
            .call()
            // 关键步骤
            .entity(LoveReport.class);
    log.info("loveReport: {}", loveReport);
    return loveReport;
}

4、编写单元测试用例,代码测试。

@Test
    void doChatWithReport() {
        String chatId = UUID.randomUUID().toString();
        String message = "你好,我是爱健身的王嘉尔,我想去英国旅游,给出一份旅游报告";
        TravelApp.TravelReport TravelReport = TravelApp.doChatWithReport(message, chatId);
        Assertions.assertNotNull(TravelReport);
    }

5、结果验证。
在这里插入图片描述

2.4、对话记忆文件持久化

2.4.1、难点

  • 设计优势:Spring AI 对话记忆解耦 “存储” 与 “记忆算法”,可通过修改 ChatMemory 存储改变对话记忆保存位置。
  • 接口方法:ChatMemory 需实现消息的增(add )、查(get )、删(clear )操作。默认实现参考:InMemoryChatMemory 用 ConcurrentHashMap 维护对话信息,key 为对话 id,value 为消息列表。
  • 自定义 ChatMemory 实现的关键难题:在保存和读取消息时,需处理 Message 对象与文本的转换,即对象的序列化(存为文本 )和反序列化(文本转对象 ),这一过程存在复杂度 。,Message 接口的多子类(如 UserMessage )因无统一结构、无参构造和 Serializable 接口,导致 JSON 序列化复杂 。
    在这里插入图片描述

2.4.2、实现

  • 1、引入依赖。
<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>5.6.2</version>
</dependency>
  • 2、编写基于文件的持久化存储。
/**
 * 基于文件持久化的对话记忆
 */
public class FileBasedChatMemory implements ChatMemory {

    private final String BASE_DIR;
    private static final Kryo kryo = new Kryo();

    static {
        kryo.setRegistrationRequired(false);
        // 设置实例化策略
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
    }

    // 构造对象时,指定文件保存目录
    public FileBasedChatMemory(String dir) {
        this.BASE_DIR = dir;
        File baseDir = new File(dir);
        if (!baseDir.exists()) {
            baseDir.mkdirs();
        }
    }

    @Override
    public void add(String conversationId, List<Message> messages) {
        List<Message> conversationMessages = getOrCreateConversation(conversationId);
        conversationMessages.addAll(messages);
        saveConversation(conversationId, conversationMessages);
    }

    @Override
    public List<Message> get(String conversationId, int lastN) {
        List<Message> allMessages = getOrCreateConversation(conversationId);
        return allMessages.stream()
                .skip(Math.max(0, allMessages.size() - lastN))
                .toList();
    }

    @Override
    public void clear(String conversationId) {
        File file = getConversationFile(conversationId);
        if (file.exists()) {
            file.delete();
        }
    }

    private List<Message> getOrCreateConversation(String conversationId) {
        File file = getConversationFile(conversationId);
        List<Message> messages = new ArrayList<>();
        if (file.exists()) {
            try (Input input = new Input(new FileInputStream(file))) {
                messages = kryo.readObject(input, ArrayList.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return messages;
    }

    private void saveConversation(String conversationId, List<Message> messages) {
        File file = getConversationFile(conversationId);
        try (Output output = new Output(new FileOutputStream(file))) {
            kryo.writeObject(output, messages);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private File getConversationFile(String conversationId) {
        return new File(BASE_DIR, conversationId + ".kryo");
    }
}
  • 3、编写单元测试用例。
public LoveApp(ChatModel dashscopeChatModel) {
    // 初始化基于文件的对话记忆
    String fileDir = System.getProperty("user.dir") + "/chat-memory";
    ChatMemory chatMemory = new FileBasedChatMemory(fileDir);
    chatClient = ChatClient.builder(dashscopeChatModel)
            .defaultSystem(SYSTEM_PROMPT)
            .defaultAdvisors(
                    new MessageChatMemoryAdvisor(chatMemory)
            )
            .build();
}
  • 4、测试验证。
    在这里插入图片描述

在这里插入图片描述

目录
相关文章
|
2月前
|
存储 人工智能 自然语言处理
用Spring AI搭建本地RAG系统:让AI成为你的私人文档助手
想让AI帮你读懂PDF文档吗?本文教你用Spring AI和Ollama搭建一个本地RAG系统,让AI成为你的私人文档助手。无需GPU,无需云端API,只需几行代码,你的文档就能开口说话了!
|
人工智能 Java Serverless
【MCP教程系列】搭建基于 Spring AI 的 SSE 模式 MCP 服务并自定义部署至阿里云百炼
本文详细介绍了如何基于Spring AI搭建支持SSE模式的MCP服务,并成功集成至阿里云百炼大模型平台。通过四个步骤实现从零到Agent的构建,包括项目创建、工具开发、服务测试与部署。文章还提供了具体代码示例和操作截图,帮助读者快速上手。最终,将自定义SSE MCP服务集成到百炼平台,完成智能体应用的创建与测试。适合希望了解SSE实时交互及大模型集成的开发者参考。
8707 59
|
2月前
|
消息中间件 自然语言处理 搜索推荐
超卖率从3%到0.2%:商品API如何让商家告别“库存噩梦”?
本文深入解析电商API接口的核心功能与技术架构,涵盖商品管理、订单处理、支付系统与物流履约四大模块,揭示其如何实现跨平台、跨场景的无缝衔接,支撑现代电商业务高效运转。