目录
一、应用概述
- 本 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、测试验证。