TomLBZ / koishi-plugin-openai

Calls OpenAI API for Koishi.js
48 stars 10 forks source link

Feature: 希望能加一个让模型忘记之前的记忆的命令 #4

Open yi03 opened 1 year ago

yi03 commented 1 year ago

有时候反复问AI类似的问题,AI就会陷入死胡同,然后反复复读类似的话。希望能加一个命令,发了之后就让它忘记之前的记忆。 https://beta.openai.com/docs/api-reference/completions/create#completions/create-stop 查了查API,不知道这个接口是不是干这个的。 谢谢!

TomLBZ commented 1 year ago

这个 Feature Request 戳到了一个大难题。

机器人的记忆的确是个难点,这是因为OpenAI的接口是无状态(Stateless)的,毕竟GPT系列模型都没有状态。因此,记忆部分是咱利用prompt engineering模拟出来的,原理是每次都利用不同状态的记忆生成额外的上下文,帮助无状态AI作出“好像有记忆一样”的回答。因此,如果记忆中相对高频率地出现某种类似的话(相对频率高于正常对话),AI就会“认为”自己“学到了规律/找到了捷径”,进而陷入死胡同。

这个问题我目前还没有很好的解决办法,需要一些时间思考研究。目前有4个感觉都不太好的基本思路,先扔在这里抛砖引玉一下:

  1. 在把语料加入记忆之前额外调用一次API判断其主题或关键词,并与已经存在的语料比较。生成上下文时每个主题出现频次固定为1。优点:可以很大程度上解决死胡同问题。缺点:需要花费更高的Token数量,并且记忆的体积会大大增加(字典体积提升常数倍),或者需要依赖额外的数据库。
  2. 取消“字面记忆”,直接将新的语料转换成总结或者关键词。每次只使用设置中的示例对话维持人设,而不刻意维持上下文。优点:可以完全解决死胡同问题。缺点:无法正确回答“刚才我们都说了什么话?”这类完全取决于上下文的问题。
  3. 增加一个指令,例如“/忘 xxxx”,来清除包含字符串“xxxx”的记忆。优点:实现起来简单。缺点:其实没有解决死胡同问题,只能每次机器人进入复读状态的时候都让用户再输一次这个指令,而且可能意外遗忘与字符串相关的其他记忆。
  4. 在每次生成的上下文后附加上示例对话,而生成总结时排除示例对话。优点:实现相对容易,部分程度上能解决复读。缺点:消耗的Token数量几乎翻倍,而且无法根绝死胡同问题。

如果你或者其他人还有其他更好的想法,欢迎在这里继续回复,谢谢!

yi03 commented 1 year ago

这个 Feature Request 戳到了一个大难题。

机器人的记忆的确是个难点,这是因为OpenAI的接口是无状态(Stateless)的,毕竟GPT系列模型都没有状态。因此,记忆部分是咱利用prompt engineering模拟出来的,原理是每次都利用不同状态的记忆生成额外的上下文,帮助无状态AI作出“好像有记忆一样”的回答。因此,如果记忆中相对高频率地出现某种类似的话(相对频率高于正常对话),AI就会“认为”自己“学到了规律/找到了捷径”,进而陷入死胡同。

这个问题我目前还没有很好的解决办法,需要一些时间思考研究。目前有4个感觉都不太好的基本思路,先扔在这里抛砖引玉一下:

  1. 在把语料加入记忆之前额外调用一次API判断其主题或关键词,并与已经存在的语料比较。生成上下文时每个主题出现频次固定为1。优点:可以很大程度上解决死胡同问题。缺点:需要花费更高的Token数量,并且记忆的体积会大大增加(字典体积提升常数倍),或者需要依赖额外的数据库。
  2. 取消“字面记忆”,直接将新的语料转换成总结或者关键词。每次只使用设置中的示例对话维持人设,而不刻意维持上下文。优点:可以完全解决死胡同问题。缺点:无法正确回答“刚才我们都说了什么话?”这类完全取决于上下文的问题。
  3. 增加一个指令,例如“/忘 xxxx”,来清除包含字符串“xxxx”的记忆。优点:实现起来简单。缺点:其实没有解决死胡同问题,只能每次机器人进入复读状态的时候都让用户再输一次这个指令,而且可能意外遗忘与字符串相关的其他记忆。
  4. 在每次生成的上下文后附加上示例对话,而生成总结时排除示例对话。优点:实现相对容易,部分程度上能解决复读。缺点:消耗的Token数量几乎翻倍,而且无法根绝死胡同问题。

如果你或者其他人还有其他更好的想法,欢迎在这里继续回复,谢谢!

提issue的时候没看您的代码,不好意思。今天看了一下您的代码,我的理解是texts过长了就总结成summaries,summaries过长了就总结成topics,每次回复都会附带上最近的texts, summaries和topics。 我之前一直以为是openai有记忆相关的接口,没想到是这么实现的,好机智。

其实我的需求是,有一个reset指令,输入之后就忘记和这个人说过的所有话,如果能选择是忘记texts、summaries、topics中的某几项记忆就更好了,比如发送指令删除texts和summaries中关于数学的记忆。和您的办法3比较像。 另外根据我的观察,复读似乎主要是因为最近的texts里有几乎完全一样、只有某些关键词不一样的话(有些群友就是爱这么问)。所以我把textMemoryLength改小似乎对复读现象有改善。 又想了一下,意识到summary的记忆一段时间内每次向openai提交的都一样,所以似乎summary的记忆更可能导致复读。 举个例子,比如我和bot聊了很多数学相关的话题,之后再问她其他领域的问题,她也老拐到数学上去,如果能选择删除texts和summaries中关于数学的记忆就好了,或者只保留一两条关于数学的记忆也可以。 我的想法是找一下新消息和最近几条文本的重复度,重复度就用最基础的找公共子串的算法来算,如果重复度过高就只记录新消息不同的关键词。

TomLBZ commented 1 year ago

其实我的需求是,有一个reset指令,输入之后就忘记和这个人说过的所有话,如果能选择是忘记texts、summaries、topics中的某几项就更好了。和您的办法3比较像。因为我的大多数群友和bot对话话题都挺跳跃,不太会期望bot记住太过久远的对话。 ~另外根据我的观察,复读似乎主要是因为最近的texts里有几乎完全一样、只有某些关键词不一样的话(有些群友就是爱这么问)。所以我把textMemoryLength改小似乎对复读现象有改善。~ 又仔细研究了研究,意识到summary的记忆一段时间内每次向openai提交的都一样,所以似乎summary的记忆更可能导致复读。 我的想法是找一下新消息和最近几条文本的重复度,重复度就用最基础的找公共子串的算法来算,如果重复度过高就只记录新消息不同的关键词。

多谢反馈,我明白你的需求了,下一个版本会加上一条重置记忆的指令,用户可以用这条指令选择性清除bot和自己的某一类记忆o( ̄▽ ̄)ブ

补充: 总结记忆之所以一段时间内(等于字面记忆的长度)向openai提交的完全一样,是为了节省Token。理论上更好的办法是每次有新消息都更新一个消息队列,然后用队列内的消息生成总结(正如现在对关键词做的一样)。但是因为总结远远比关键词长,而且消息更新的频率实际上非常高,所以这种做法会快速花掉过多的Token,因此没有选择那样实现。 之所以存储了逐字的记忆而不是只存储关键词,是因为对于无状态的OpenAI来说,逐字的记忆可以保留说话的语气、人设、情绪、措辞等信息,有助于AI维持自洽的对话。这也是为什么不论对话是什么内容,记忆永远是逐字优先。但归根结底Prompt的长度很有限,能操作的空间因此并不大,因此采用了一种折衷的办法——对于比较久远或者过于久远的消息,分别利用总结与关键词提高信息密度。在这里,总结与关键词负责告诉OpenAI内容上的信息,而字面记忆除了内容上,还负责着措辞与语气上的信息。

flag: 我争取在下下个版本找到一个更好的在记忆中解决复读的办法┏┛墓┗┓

yi03 commented 1 year ago

其实我的需求是,有一个reset指令,输入之后就忘记和这个人说过的所有话,如果能选择是忘记texts、summaries、topics中的某几项就更好了。和您的办法3比较像。因为我的大多数群友和bot对话话题都挺跳跃,不太会期望bot记住太过久远的对话。 ~另外根据我的观察,复读似乎主要是因为最近的texts里有几乎完全一样、只有某些关键词不一样的话(有些群友就是爱这么问)。所以我把textMemoryLength改小似乎对复读现象有改善。~ 又仔细研究了研究,意识到summary的记忆一段时间内每次向openai提交的都一样,所以似乎summary的记忆更可能导致复读。 我的想法是找一下新消息和最近几条文本的重复度,重复度就用最基础的找公共子串的算法来算,如果重复度过高就只记录新消息不同的关键词。

多谢反馈,我明白你的需求了,下一个版本会加上一条重置记忆的指令,用户可以用这条指令选择性清除bot和自己的某一类记忆o( ̄▽ ̄)ブ

补充: 总结记忆之所以一段时间内(等于字面记忆的长度)向openai提交的完全一样,是为了节省Token。理论上更好的办法是每次有新消息都更新一个消息队列,然后用队列内的消息生成总结(正如现在对关键词做的一样)。但是因为总结远远比关键词长,而且消息更新的频率实际上非常高,所以这种做法会快速花掉过多的Token,因此没有选择那样实现。 之所以存储了逐字的记忆而不是只存储关键词,是因为对于无状态的OpenAI来说,逐字的记忆可以保留说话的语气、人设、情绪、措辞等信息,有助于AI维持自洽的对话。这也是为什么不论对话是什么内容,记忆永远是逐字优先。但归根结底Prompt的长度很有限,能操作的空间因此并不大,因此采用了一种折衷的办法——对于比较久远或者过于久远的消息,分别利用总结与关键词提高信息密度。在这里,总结与关键词负责告诉OpenAI内容上的信息,而字面记忆除了内容上,还负责着措辞与语气上的信息。

flag: 我争取在_下下个版本_找到一个更好的在记忆中解决复读的办法┏┛墓┗┓

(在您回复我的上一条前我刚好在编辑,我在这里重说一遍我更新的话吧。) 举个例子,比如我和bot聊了很多数学相关的话题,之后再问她其他领域的问题,她也老拐到数学上去,如果能选择删除texts和summaries中关于数学的记忆就好了,或者只保留一两条关于数学的记忆也可以。

另外又有一个想法,就是发送list指令,能列出目前的所有记忆,然后手动选择删除某些记忆。 或者再进一步,可以移植记忆。比如直接发送消息指定texts, summaries和topics的内容。 虽然这么做就有点太像bot了,不过感觉可能还挺实用(

TomLBZ commented 1 year ago

举个例子,比如我和bot聊了很多数学相关的话题,之后再问她其他领域的问题,她也老拐到数学上去

多谢反馈!这个观察很有价值。我会想办法解决这个问题。雀食,哪怕以前聊过很多数学,新的话题也不能总是拐到数学上(好像现实中热爱数学的人似乎也会这样子拐话题呢(划掉

或者再进一步,可以移植记忆。比如直接发送消息指定texts, summaries和topics的内容。 虽然这么做就有点太像bot了,不过感觉可能还挺实用(

的确太像bot了……但是移植记忆似乎是有用的,比如迁移bot的时候,如果bot的部署人可以导出全部的记忆,ta就不会忘了自己,也不会忘了以前对话过的用户。这个倒是可以实现一下,但是优先级暂时比较靠后,可能要多等一等。

yi03 commented 1 year ago

这个 Feature Request 戳到了一个大难题。

机器人的记忆的确是个难点,这是因为OpenAI的接口是无状态(Stateless)的,毕竟GPT系列模型都没有状态。因此,记忆部分是咱利用prompt engineering模拟出来的,原理是每次都利用不同状态的记忆生成额外的上下文,帮助无状态AI作出“好像有记忆一样”的回答。因此,如果记忆中相对高频率地出现某种类似的话(相对频率高于正常对话),AI就会“认为”自己“学到了规律/找到了捷径”,进而陷入死胡同。

这个问题我目前还没有很好的解决办法,需要一些时间思考研究。目前有4个感觉都不太好的基本思路,先扔在这里抛砖引玉一下:

  1. 在把语料加入记忆之前额外调用一次API判断其主题或关键词,并与已经存在的语料比较。生成上下文时每个主题出现频次固定为1。优点:可以很大程度上解决死胡同问题。缺点:需要花费更高的Token数量,并且记忆的体积会大大增加(字典体积提升常数倍),或者需要依赖额外的数据库。
  2. 取消“字面记忆”,直接将新的语料转换成总结或者关键词。每次只使用设置中的示例对话维持人设,而不刻意维持上下文。优点:可以完全解决死胡同问题。缺点:无法正确回答“刚才我们都说了什么话?”这类完全取决于上下文的问题。
  3. 增加一个指令,例如“/忘 xxxx”,来清除包含字符串“xxxx”的记忆。优点:实现起来简单。缺点:其实没有解决死胡同问题,只能每次机器人进入复读状态的时候都让用户再输一次这个指令,而且可能意外遗忘与字符串相关的其他记忆。
  4. 在每次生成的上下文后附加上示例对话,而生成总结时排除示例对话。优点:实现相对容易,部分程度上能解决复读。缺点:消耗的Token数量几乎翻倍,而且无法根绝死胡同问题。

如果你或者其他人还有其他更好的想法,欢迎在这里继续回复,谢谢!

拍脑袋想到一个保持记忆的办法。 把所有历史对话都存起来,每个对话都提取关键词再embedding存起来,然后对话的时候把输入的对话也提取关键词embedding,然后搜索历史对话中词向量相似度最高的三条对话作为related memory。用related memory来代替现在summary memory,每次给openai发送text memory, related memory, topic memory。 提取关键词embedding这一步使用本地计算,不调用openai接口。 我对ai是外行,可能某些概念理解有误。

TomLBZ commented 1 year ago

拍脑袋想到一个保持记忆的办法。 把所有历史对话都存起来,每个对话都提取关键词再embedding存起来,然后对话的时候把输入的对话也提取关键词embedding,然后搜索历史对话中词向量相似度最高的三条对话作为related memory。用related memory来代替现在summary memory,每次给openai发送text memory, related memory, topic memory。 提取关键词embedding这一步使用本地计算,不调用openai接口。 我对ai是外行,可能某些概念理解有误。

目前的版本(v2.0.1)已经重构到用embedding保存记忆的模式,但是embeddings还是用的openai接口。等一波可以本地算embedding vector的库。至于忘记记忆相关的功能,我倒是已经设想好了实现思路,就是缺时间写代码。争取下周再发布一个更新

yi03 commented 1 year ago

https://v2ex.com/t/921750 看到和自己思路一模一样的实现,虽然也是很自然的思路了🤣

话说大佬有没有试过不提取关键词,整句话直接做embedding,我看那个链接里有人发的这个链接是这么做的:https://github.com/openai/openai-cookbook/blob/main/apps/web-crawl-q-and-a/web-qa.ipynb

反正本质就是一个本地的搜索引擎,办法应该挺多的(

TomLBZ commented 1 year ago

https://v2ex.com/t/921750 看到和自己思路一模一样的实现,虽然也是很自然的思路了🤣

话说大佬有没有试过不提取关键词,整句话直接做embedding,我看那个链接里有人发的这个链接是这么做的:https://github.com/openai/openai-cookbook/blob/main/apps/web-crawl-q-and-a/web-qa.ipynb

我这里embedding就是整句做的,也是整句存储的,但是之所以还要提取关键词,是为了对下一轮回复进行启发。我举个例子: 我说:唐僧和孙悟空都是西游记的人物。其中关键词有["唐僧","孙悟空","西游记","人物"]。 根据向量余弦搜索,假设查询到了两个match["孙悟空保护唐僧西天取经","西游记是我国四大名著之一"],则这里多出了两个从未出现过的关键词["西天取经","四大名著"]。它没有在原句的关键词组中出现。 于是我们舍弃第一组match,进行第二次余弦搜索,假设有句话"齐天大圣在西天取经之前曾经大闹天宫",这句话的余弦距离与我的输入相差较大,不易被搜索到。而第二次搜索时使用关键词进行了预选,于是提高了搜索到的概率。 这个行为的目的是在数据量极大、余弦距离过于集中的时候引入发散性。如果太过发散,ChatGPT也会帮我们在最后合成的时候进行修正。

yi03 commented 1 year ago

https://arxiv.org/abs/2304.13343 看到个这个论文,和现在这个插件使用的思路很像,多加了一个 summary of memory 和 recency score。 也许可以抄一抄他的prompt,有空的话也可以试试他的 summary of memory 和 recency score。 (论文原作者的代码仓库刚才不知道为啥被删了,搜到了一个fork: https://github.com/toufunao/SCM4LLMs)

不过他这篇论文每个对话会请求 openai 三次,前两个请求是问是否使用 memory ,第三个请求是回复,会增大响应延迟。我想到的解决办法是 是否使用 memory 和 是否使用 summary of memory 这两个问题合并成一个问题问,还有就是 gpt-3.5-turbo 现在延迟比较高,也许这种简单的任务可以交给 claude 或者其他速度快的模型。

论文机翻: 使用自控记忆系统释放大规模语言模型的无限长度输入能力.pdf

yi03 commented 1 year ago

https://github.com/imartinez/privateGPT 另一个思路类似但不用联网的库。