Spring AI 连续对话
Spring AI 连续对话
在前面Spring AI Chat 简单示例中我们只调用了一次请求,返回了一个结果,我们见过的各种 chat 都是支持连续对话的,AI 需要记住我们的上下文才能让对话连贯起来,通过 API 调用的时候每次对话都是一次无状态的独立请求,想要实现连续对话就需要我们自己记住对话的历史,在每次调用 API 的时候将对话历史传递给 API。
本文就简单实现连续对话,并且引申一些相关的扩折或者优化。
基本功能
首先要知道 Spring AI 中交互的消息对象,在
Prompt
中存在
public Prompt(List
构造方法,可以传递多个消息,Message 中存在消息类型 MessageType,包含下面几种类型:
public enum MessageType {
USER("user"),
ASSISTANT("assistant"),
SYSTEM("system"),
FUNCTION("function");
这四种类型分别对应的 Message 实现,如
UserMessage
,
SystemMessage
。
在 API 调用返回的
ChatResponse
包含的
Generation
中返回的是
AssistantMessage
消息,因此只需要通过
List
按顺序记录消息历史就可以实现。
var openAiApi = ...//创建 API
//创建客户端,注意withMaxTokens参数,可以不限制
ChatClient chatClient = new OpenAiChatClient(openAiApi, OpenAiChatOptions.builder()
.withModel("gpt-3.5-turbo")
.withTemperature(0.4F).build());
//使用集合记录消息历史
List<Message> messages = new ArrayList<>();
//可以预设系统消息
messages.add(new SystemMessage("你是一个聊天机器人,所有回复请使用中文。"));
//演示从控制台输入内容进行对话
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print(">>>>> ");
String message = scanner.nextLine();
//退出命令
if (message.equals("exit")) {
break;
}
//添加到消息历史
messages.add(new UserMessage(message));
//发送所有消息
ChatResponse response = chatClient.call(new Prompt(messages));
//将返回的助手消息添加到消息历史
messages.add(response.getResult().getOutput());
//输出返回的内容
System.out.println("<<<<< " + response.getResult().getOutput().getContent());
}
最近在看《大道朝天》,所以问几个相关的问题进行测试:
>>>>> 大道朝天的作者是谁?
<<<<< 大道朝天的作者是明代著名文学家冯梦龙。
>>>>> 你知道猫腻是谁吗?
<<<<< 猫腻是中国知名网络小说作家,代表作品有《择天记》、《将夜》等。
>>>>> 大道朝天的作者是谁?
<<<<< 大道朝天的作者是明代著名文学家冯梦龙。
>>>>> exit
可以看到 AI 的回复是错误的,尤其是有历史后,回复会变得更固定。当我去掉对话历史时,回复的内容会有所不同:
>>>>> 大道朝天的作者是谁?
<<<<< 大道朝天的作者是唐代诗人李白。
>>>>> 大道朝天的作者是谁?
<<<<< 大道朝天的作者是唐代诗人杜甫。
>>>>> 大道朝天的作者是谁?
<<<<< 大道朝天的作者是庄子。
>>>>> 大道朝天的作者是谁?
<<<<< 大道朝天的作者是李白。
接下来是几个基于对话的扩展。
预设上下文
当前很火的 RAG 技术实际上就是将已有知识向量化之后通过向量查询,将查询的结果作为上下文提供给 AI,在此基础上进行回答,上面的问题中,我们可以增加一个上下文,让 AI 回答的更准确,修改前面示例增加上下文:
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个聊天机器人,所有回复请使用中文。"));
messages.add(new SystemMessage("猫腻 ,本名晓峰,1977年出生于湖北省宜昌市夷陵区,网络作家,橙瓜见证·网络文学20年十大玄幻作家,百强大神作家,百位行业人物。\n" +
"猫腻曾就读于四川大学(未毕业),后从事网络文学创作,现为阅文集团白金作家。" +
"代表作《朱雀记》《庆余年》《间客》《将夜》《择天记》《大道朝天》、《映秀十年事》等,其中《朱雀记》于2007年获得新浪第四届原创大赛·奇幻武侠奖一等奖;" +
"《间客》于2013年获得首届“西湖·类型文学双年奖”银奖 ,于2018年入选“中国网络文学20年20部作品”,位列榜首 ;《将夜》于2015年获得首届网络文学双年奖金奖 。"));
把百度的一段内容放到了上下文,再重新进行上面的对话:
>>>>> 大道朝天的作者是谁?
<<<<< 《大道朝天》的作者是网络作家猫腻。
>>>>> 你知道猫腻是谁吗?
<<<<< 猫腻是网络作家的笔名,本名是晓峰,是一位知名的网络文学作家,代表作品包括《朱雀记》、《庆余年》、《间客》、《将夜》、《择天记》、《大道朝天》等。
>>>>> 猫腻获得过哪些奖?
<<<<< 猫腻曾获得过多个奖项,其中包括:
1. 《朱雀记》于2007年获得新浪第四届原创大赛·奇幻武侠奖一等奖;
2. 《间客》于2013年获得首届“西湖·类型文学双年奖”银奖;
3. 《将夜》于2015年获得首届网络文学双年奖金奖。
有了上下文后 AI 的回答更准确了。
减少 token 数
AI 的调用价钱和 token 数相关,所以如果支持无限长度的对话会使得成本快速升高,所以一般的对话会限制连续对话的次数,或者通过 AI 将对话总结后减少需要传递的内容。
如果想减少次数,最简单的就是判断对话数,然后减少前面的内容,在原始代码中修改如下:
messages.add(new UserMessage(message));
// messages 保留最近的10次对话
if (messages.size() > 10) {
messages.remove(0);
}
如果通过总结的方式,还可以额外调用一次 AI,然后去掉前面几次的 user 和 assistant 内容,将总结的内容追加到最后,再加上本次的用户消息进行发送。
支持多人对话
示例中只能单线程的一个人进行对话,如果通过 http 提供服务,想要支持多人对话,最简单的方式就是给每个对话分配一个对话id,然后将对话id和对应的对话历史联系起来,例如使用
Map
:
Map<String, List<Message>> chatMessage = new ConcurrentHashMap<>();
对话时都带着对话id即可将多人的对话区分开。
连续对话的实现比较简单,我们能额外做的就是在 API 调用时如何获取并提供预设信息,如何处理额外的 API 调用实现一些特殊需求,在消息最终展示给用户时需要做的各种处理。