2023-03-25 06:25
ChatGPT 的连续对话机制,是通过在 prompt 中加入之前的对话内容来实现的。一个典型的对话 prompt 示例如下:
openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Who won the world series in 2020?"},
{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
{"role": "user", "content": "Where was it played?"}
]
)
在上面的 prompt 中,用户向 ChatGPT 提问的内容其实只是最后一条,即“它发生在哪里”。而 ChatGPT 之所以能理解“它”的指代,是因为这条 prompt 同时包含了用户与 ChatGPT 的上一回合对话,里面提及了“the World Series in 2020”。这部分内容构成了最后那条问题的上下文语境,ChatGPT 借此实现了带有语境的、连续的对话功能。
一定意义上,prompt 所包含的之前的对话内容相当于是 ChatGPT 的“记忆”。只有在 prompt 中被“记住”的内容,才会影响这条 prompt 的输出;被排除在外的对话记录,就像被“遗忘”了一样,不会出现在 ChatGPT 生成响应的过程中。(对此,下文将有直观的展示。)
但是,目前 ChatGPT 的模型,对于每回合对话有 token 数限制(可理解为字数限制)。例如 GPT-3.5 模型的一个 prompt 和它产生的应答内容,二者加起来不能超过 4096 个 token。Token 数量与文本长度没有固定的换算关系,但可以估算大概。对于中文,这大约是三千汉字左右。注意,这并不能看作一个 prompt 可以包含的对话记录容量,因为还要留出余量用于输出应答内容。Prompt 实际可用的容量要更小。
为了直观地展示这一限制对于 ChatGPT 的“记忆”能力的影响并探索其实现方式,本文进行了一系列实验。
实验通过在 ChatGPT 网站上与其对话来进行。每次测试会开启一个新的对话序列。目前已知 ChatGPT 不会在对话序列之间分享信息,即:对话 A 所包含的信息,不会被添加到对话 B 的任何 prompt 中。这方便了我们测试不同的场景。同时,ChatGPT 的消息修改功能也极大地提高了测试的效率。在一个对话序列中,上一条消息是下一条消息的父消息。例如用户发送消息 A,收到 ChatGPT 的回答 B,A 即是 B 的父消息,B 又是用户下一条消息 C 的父消息。而由于用户可以修改之前的输入内容,替换产生新的消息,导致一条父消息可能包含多条子消息。这种情况下,对话将产生分支。例如某个对话序列中包含下列消息列表:
用户消息A -> ChatGPT消息B -> 用户消息C -> ChatGPT消息D -> ...
\_-> 用户消息C' -> ChatGPT消息E -> ...
这里,用户修改了已存在的消息 C,使对话出现了分支。
在对话界面上可以切换展示不同分支的消息序列。重要的是,不同分支之间也不会分享信息。这使我们在测试中可以“逆转”某一条消息的结果,即通过修改消息和切换分支,抹去某条消息对下文的影响。
在这些基础上,实验具体采用了这样的结构:在对话的开头部分,要求 ChatGPT 记住一些信息;随后与之进行持续对话并产生长文本;当对话持续了一定长度之后,测试 ChatGPT 是否还能提供之前记住的信息。
第一次实验的文本以这样的方式展开(C:ChatGPT):
(我)我们来玩一个游戏。
(C)好的,我很乐意玩游戏!请问您想玩哪种游戏呢?
(我)我先告诉你一个开门密码:AsDfGh。请你记住它。此外还有一个登录密码:XY1234.
(C)好的,我已经记住了开门密码“AsDfGh”和登录密码“XY1234”。请问接下来我们要玩什么游戏呢?
(我)然后我会给你讲一些故事。在故事的结尾会包含新的密码。我会偶尔提问不同的密码,你需要正确回答。
(C)好的,我明白了。我会尽力记住密码并随时准备回答您的问题。请开始您的故事吧!
(我)以下是我的第一个故事:
东晋东间,阳羡县有位叫许彦的人正在绥安山里走,遇见一个十七八岁的书生,躺在路旁,说自己脚痛,并请求许彦打开手提的鹅笼子,他要钻进去。许彦开始以为他开玩笑,便打开鹅笼。结果,那书生真的就钻了进去。奇怪的是,那笼子也不变大,书生也没变小,他却与一对鹅并坐在一起,鹅也并不惊慌。许彦提起那笼子,也并不觉得重。
来到一棵大树下休息时,书生才走出来,对许彦说:“我想为你设一薄宴,以示感谢。”许彦点点头说:“好。”于是,那书生从嘴里吐出一个铜盘和精巧的盒子,盒子中有各种饭菜,山珍海味罗列在一起。那器皿全是铜的,气味芳美。世所罕见。酒喝了数巡,那书生才对许彦说道:“这些日子,带了一个女人跟着我;今天,我想暂时把她唤来。”许彦说:“很好。”于是,书生又从嘴里吐出一个女子,年纪大约十五六岁,容貌绝美,衣服华丽,同他们坐在一起饮酒。
有顷,书生便醉倒了。那女子对许彦说:“我虽然与书生相好,可实际上却怀有外心,并偷偷地领来一个男子。书生既然睡着了,我想暂时把他唤来,希望你不要说。”许彦说:“好吧。”于是,女子便从口中吐出一个男人来,年纪大约二十三四岁,也显得十分聪颖可爱,并同许彦寒暄畅叙。书生将要醒来,那女子又吐出一鲜艳华美且可移动的屏风,与他躺到一起。
那男人对许彦说:“这女子与我虽然有情,但也非一心一意。方才我还偷着约一个女子前来,现在想趁此机会看看她。希望你不要泄露此事。”许彦说:“好”。于是,这男人又从口中吐出一个女子,年纪在二十岁左右。与他同宴共饮。调笑好长时间,听见屏风内的书生有动静,这男人说:“他们已经睡醒了。”然后将所吐的女子吸回口中。
不一会儿,书生处的那个女子就出来了,对许彦说:“书生快要起来了!”然后将那男人吞进口中,单独与许彦对坐。书生起来后对许彦说:“这一小觉睡得太久了,让你单独坐着,挺难受吧?天已经很晚了,只好跟你告别。”说罢,便将那小女子连同所有铜的器皿又全吞进口中,只留下一个二尺多的大铜盘送给许彦,并告别道:“别后咱们无所寄托,只有相互回忆吧!”
我的管理密码是MMmm64。
(此条消息1080 token。)
(C)谢谢你的故事,密码是MMmm64。
(我)现在,作为游戏的一部分,告诉我管理密码是什么?
(C)您的管理密码是MMmm64。
(我)我讲的故事是关于什么的?
(C)您讲的故事是一个古代的神话故事,其中描述了一个神秘的书生,他能从口中吐出各种物品和人物,还有一个奇妙的鹅笼。故事的主题是神秘和神奇的力量,有些版本的故事中可能还有一些道德故事的含义。
(我)书生是来自什么县?
(C)书生并没有提到他来自哪个县,只是在阳羡县的山里遇到了许彦。
(我)许彦来自什么地方?
(C)故事中并没有明确提到许彦的来处,只知道他在东晋东间的阳羡县旅行。
首先我们确认 ChatGPT 能够针对故事内容进行正确回应。这意味着完整的故事被包含在了后面几条消息的 prompt 中。
接下来,我修改了在管理密码的问答之后,“我讲的故事是关于什么的?”这一条消息:
(C)您的管理密码是MMmm64。
(我)开门密码呢?
(C)开门密码是“鹅笼草屋”。
(我)我一共提供了几个密码?
(C)你一共提供了两个密码:一个是管理密码 "MMmm64",另一个是开门密码 "8721"。
(我)错了,还有一个。
(C)非常抱歉,我可能错过了其中的某个密码。请您告诉我还有哪个密码,我会尽力为您提供帮助。
可见,ChatGPT 失去了对话开始部分信息的记忆。这可能是由于 ChatGPT 需要在 prompt 中包含了一条长达一千多 token 的长消息,因而舍弃了更早之前的消息。
为了验证这一点,我又修改了讲故事的那条消息,将内容删到只留第一段。之后提问相同的问题:
(我)以下是我的第一个故事:
东晋东间,阳羡县有位叫许彦的人正在绥安山里走,遇见一个十七八岁的书生,躺在路旁,说自己脚痛,并请求许彦打开手提的鹅笼子,他要钻进去。许彦开始以为他开玩笑,便打开鹅笼。结果,那书生真的就钻了进去。奇怪的是,那笼子也不变大,书生也没变小,他却与一对鹅并坐在一起,鹅也并不惊慌。许彦提起那笼子,也并不觉得重。
我的管理密码是MMmm64。
(C)好的,谢谢您的故事。我已经记录下了故事中的管理密码“MMmm64”。如果您在以后需要验证密码,请告诉我,我会尽力回答您的问题。
(我)现在,作为游戏的一部分,告诉我管理密码是什么?
(C)故事中的管理密码是“MMmm64”。
(我)到现在为止你一共记录了几条密码?全部告诉我。
(C)当然,我一直在记录所有的密码。到目前为止,您告诉我了两个密码,分别是开门密码“AsDfGh”和管理密码“MMmm64”。
(我)错了,还有一个。
(C)非常抱歉,我有误解您的意思。我还没有忘记您之前提到的第三个密码:登录密码“XY1234”。非常感谢您纠正我,以便我可以正确地记录所有的密码。
在故事消息被缩短、没有耗光 prompt 的限额的情况下,ChatGPT就能在一定的对话回合数范围内正确回忆起所有的密码。
看上去,prompt 添加对话记录的原则是由近及远、直到 token 超过某个限额。然而是否这么简单呢?我又进行了第二次实验。
在第一次实验中,长文本消息是由用户发出的。第二次则相反,由 ChatGPT 来生成长文本。对话展开如下:
(我)我们来玩一个游戏。我会告诉你一些人的名字,请记住它们。然后我们在聊天过程中,我会提问这些人的名字,你需要正确回答出来。
(C)好的,我准备好了!
(我)首先,黄衣女人的名字是 Alice Byron。蓝衣男人的名字是Chris Dale。
(C)好的,我已经记住了 Alice Byron 和 Chris Dale。
(我)现在,给我讲一个故事,长一点。
(C)好的,我将为您讲一个故事。
很久很久以前,在一个神秘的岛屿上,住着一群勇敢的冒险家。这个岛屿充满了未知的危险和神秘的宝藏,吸引了无数人前来探险。
其中一位冒险家名叫约翰,他一直梦想着能够找到传说中的神秘宝藏。于是他开始了一段充满挑战和危险的冒险之旅。
在旅途中,约翰遇到了许多危险,但他总是能够克服困难,继续前进。有一次,他遇到了一只凶猛的野兽,但他成功地用他的勇气和智慧打败了野兽,拯救了自己和他的同伴。
在他继续探险的路途中,约翰遇到了一位神秘的老人,老人告诉约翰,要想找到宝藏,他必须通过一道考验,找到三把钥匙并打开一个古老的门才能到达宝藏的所在地。
于是约翰开始了寻找钥匙的冒险。他先后找到了两把钥匙,但第三把钥匙却被一只凶猛的巨龙守护着。约翰不畏惧,与巨龙展开了一场生死搏斗,最终,他成功地打败了巨龙,拿到了最后一把钥匙。
最终,约翰成功地打开了那扇古老的门,发现了传说中的神秘宝藏。他激动得流下了眼泪,因为他知道这是他一生中最重要的冒险,他成功了。
从此以后,约翰变得非常有名,成为了传说中最伟大的冒险家之一,他的故事也被讲述了很多次,成为了一代人的经典传说。
(此条消息 635 token。)
(我)故事里的冒险家叫什么名字?
(C)这个故事中的冒险家叫约翰。
(我)现在是第三个人名:绿衣女人的名字是Elle Franklin。
(C)好的,我已经记住了 Elle Franklin 的名字。
(我)刚才的故事里约翰要找到多少把钥匙才能打开宝藏?
(C)在故事中,约翰需要找到三把钥匙才能打开宝藏的门。
(我)很好,你还记得黄衣女人的名字吗?
(C)是的,黄衣女人的名字是 Alice Byron。
我们发现 ChatGPT 仍然保留有对话开头的记忆,考虑到文本长度不够的可能性,于是修改了最后一条提问名字的消息,让它继续生成长文本:
(C)在故事中,约翰需要找到三把钥匙才能打开宝藏的门。
(我)很好,现在请给我讲第二个故事。
(C)好的,我将为您讲另一个故事。
这是一个发生在很久很久以前的故事。在那个时候,世界上有一座宏伟的城市,它的名字叫做亚特兰蒂斯。亚特兰蒂斯是一个富裕繁荣的城市,拥有无尽的财富和宝藏。
然而,这个城市的统治者十分自大和傲慢,他们不断地掠夺和压迫其他国家和地区,让许多人为他们的自私行为付出了代价。
最终,天谴降临,一场大灾难袭击了亚特兰蒂斯。城市被淹没在了海水之下,所有的人都在灾难中遇难了。据说,这座城市的宝藏和财富仍然埋藏在海底,成为了一段传说和谜团。
许多人试图寻找亚特兰蒂斯的宝藏,但是没有人能够成功。有一天,一位叫做约翰的冒险家决定挑战自己的命运,前往海底探险寻找这个神秘的城市。
在海底的探险中,约翰遭遇了许多危险,但是他并没有放弃。最终,他找到了亚特兰蒂斯的遗迹,并成功地找到了这座城市的宝藏。
约翰知道这个宝藏不属于他,而是属于世界上所有的人。于是他将宝藏捐献给了一家慈善机构,用来帮助需要帮助的人们。
从此以后,约翰成为了世界上最著名的探险家之一。他的故事激励了无数的人去探险,去追求自己的梦想和目标。
(此条消息 594 token。)
(我)现在你一共讲了几个故事?
(C)到目前为止,我已经为您讲了两个故事。
(我)这两个故事有什么共同点?
(C)这两个故事的共同点在于它们都是关于冒险和探索的。在第一个故事中,约翰冒险去寻找一个宝藏,并面临了许多危险和挑战。在第二个故事中,约翰也是一名冒险家,他前往海底寻找亚特兰蒂斯的宝藏。
除此之外,这两个故事也都强调了勇气和决心的重要性。约翰在两个故事中都表现出了勇气和决心,尽管面临着困难和危险,他依然坚持自己的目标,最终取得了成功。
(我)第一个故事中,约翰和什么生物展开了生死搏斗?
(C)在第一个故事中,约翰和一只凶猛的狮子展开了生死搏斗。
(我)现在,请回答蓝衣男人的名字。
(C)蓝衣男人的名字是 Chris Dale。
(我)我一共告诉你几个人的名字?全部列出来。
(C)您一共告诉我了三个人的名字,它们是:
Alice Byron(黄衣女人)
Chris Dale(蓝衣男人)
Elle Franklin(绿衣女人)
有趣的事情发生了!通过询问细节,我们可以确认 ChatGPT 已经遗忘了第一个故事(它将巨龙说成了狮子。同时我也通过修改消息,确认它已经无法复述第一个故事的情节)。但是对于更早之前的人名信息却能够准确提供。
为了进一步确认,我修改了提问人名的消息,插入了关于第二个故事细节的追问:
(C)在第一个故事中,约翰和一只凶猛的狮子展开了生死搏斗。
(我)第二个故事里,城市被海水淹没的原因是什么?
(C)在第二个故事中,城市被海水淹没的原因是因为一次大地震导致海底地壳移动,从而引发了海啸和城市的沉没。
(我)现在,请回答我一共告诉过你多少个人名?列出他们的名字。
(C)到目前为止,您一共告诉了我三个人名。他们的名字是:
Alice Byron
Chris Dale
Elle Franklin
(我)列出名字和服装颜色
(C)当然,以下是这三个人的名字和他们穿着的服装颜色:
Alice Byron - 黄色
Chris Dale - 蓝色
Elle Franklin - 绿色
我们发现,仅仅三回合对话后,不仅第一个,连第二个故事也已经被遗忘。但 ChatGPT 仍然能够说出对话一开始涉及的内容。显然,最后几次对话的 prompt 舍弃了关于故事的两个长文本消息,但保留了更早的、关于人名的消息。
可见 prompt 对于对话记录是有取舍的,可能会跳过一些消息、添加更早前的消息。这种取舍是如何进行的呢?是根据文本长度还是根据消息发出者来筛选?对此我们进行了第三个实验。
第三次实验的文本展开过程与第二次非常相似,区别在于第二次由 ChatGPT 生成的两个故事,通过复制粘贴,变为由用户讲述。其他的对话则尽量与第二次实验中保持一致。(文本从略。)
在讲述完两个故事之后,立即提问 ChatGPT 关于人名的问题,我们发现它无法答出对话开头提到的两个人名:
(我)现在,请回答我一共告诉过你多少个人名?列出他们的名字。
(C)你告诉我了三个人名,它们是:
约翰(John)
Elle Franklin
亚历克斯(Alex)
它能够说出一共有三个人名,并正确回答出其中一个(Elle Franklin),这很可能是由于我们在两个故事之间告诉过它“第三个人名”:
(我)现在是第三个人名:绿衣女人的名字是Elle Franklin。
它据此推断出了人名数量。但是另外两个人名的信息已经被忘记了。
这与第二次实验的结果不相符,说明消息由谁来发出确实会影响 prompt 的取舍。在这次实验中,原本被舍弃掉的长文本又回到了 prompt 中,导致对话早期的消息被挤了出去。而长文本消息之所以被保留,只因为它们是用户发的消息。所以我们可以推断,ChatGPT 在构建 prompt 时,会加入更多的用户方消息。
同时,我们也知道 ChatGPT 不会只加用户消息而舍弃全部的应答消息,因为第二次实验中我们针对 ChatGPT 生成的故事进行追问,其在一定回合内的应答是正确的。因此综合起来,我们可以合理推测 ChatGPT 构建 prompt 的对话记录获取机制:
“由当前消息向上回溯,在一定回合或者一定 token 限额内,添加双方的消息内容;当超出这一范围,继续回溯但只添加用户发出的消息。直到 token 数超过总限额或回溯到达对话起始。”
根据有限测试数据猜测:大约在回溯三回合内或者数百 token 上限之内的对话回合,双方消息都会被添加;超过此范围则舍弃应答消息。一个 prompt 的总限额大约有一千多到两千 token,剩余的部分留给应答输出。
最后,为了彻底验证这一猜想,证明只有用户发的消息被包含进了 prompt,我们又进行了一个实验。
第四次实验的过程也与第二次非常相似,长文本也是由 ChatGPT 生成。唯一的不同是:对话所涉及的人名信息由 ChatGPT 来提供。从头到尾,在用户发的消息中都没有真正提到过这些人名:
(我)我们来玩一个游戏。我会告诉你一些人的名字,请记住它们。然后我们在聊天过程中,我会提问这些人的名字,你需要正确回答出来。
(C)好的,我会尽力记住这些人的名字并尽力回答你的问题。让我们开始吧!
(我)首先,我需要你随机生成两个英文人名,一个女名,一个男名。
(C)好的,这里是两个随机生成的英文人名,一个女名和一个男名:
女名:Sophie
男名:Ethan
(我)请记住:黄衣女人的名字是上面的女名。蓝衣男人的名字是上面的男名。
(C)好的,我记住了。黄衣女人的名字是Sophie,蓝衣男人的名字是Ethan。
(我)现在,请给我讲一个故事,长一点也可以。
(C)好的,这是一个关于Sophie和Ethan的故事。
Sophie和Ethan是大学时代的同学,他们在同一个学校的音乐系学习,都非常热爱音乐。
...
(故事从略)
(我)这个故事中提到了一首古典音乐作品,它的名字是什么?
(C)故事中提到的那首古典音乐作品的名字是《春之声》。
(我)请记住,绿衣女人的名字和这首古典音乐作品的名字相同。
(C)好的,我记住了。绿衣女人的名字是《春之声》。
(我)很好,现在请讲第二个故事。
(故事从略)
在两个故事讲述完毕后,针对故事细节进行追问。发现 ChatGPT 和第二次实验一样,很快就遗忘了前面故事的内容。
(我)第一个故事中,那对恋人的名字是什么?
(C)第一个故事中,那对恋人的名字是黄衣女人的名字是Emily,蓝衣男人的名字是John。
但是当问题修改为针对人名的提问时,ChatGPT 的回答与第二次实验不同:
(我)目前为止,你一共记录了几个名字?全部列出来,连同他们的服装颜色。
(C)目前为止,我记录了四个名字,他们的服装颜色如下:
黄衣女人(女名)
蓝衣男人(男名)
绿衣女人(名字与古典音乐作品相同)
红衣男人
注意:ChatGPT 可以回答出我们提供给它的三个人(而非四个)的服装颜色,但无法说出他们的名字。正确的服装颜色表明它已经回溯到了对话起始。但它在回答中所提供的信息,全部来自用户发出的消息。由于人名信息只包含在它自己发出的消息中,它就说不上来了。这完全证明了 prompt 的对话记录中,早期的对话消息只有用户发出的会被添加进来。
(ChatGPT 在回答中提到的第四个人,很可能来自于它讲的第二个故事。这个故事包含一个女主人公,这可能让 ChatGPT 计数失误,多加了一个人数。而“红衣男人”的描述是随机生成的。ChatGPT 作为语言模型,永远只会在任何位置输出它认为最合理、出现概率最大的文字。当它认定需要列出四个人而第四个人找不到具体描述时,它就按照前三个人的模式输出了第四个人的服装和性别。)
ChatGPT 在对话中的 prompt 受到最大 token 数的限制,有时不能包含完整的对话记录。对此,它对近几回合的消息包含了双方发送的内容;稍早之前的消息则只包含用户发送的内容,以此方式来构建 prompt 中的对话记录。这可能是为了在 token 限额内尽量筛选更重要的对话记录而做出的折衷。
这种方法并不能很好地应对对话回合数过多、包含长文本等情况。如果要打破 token 数限制,可以尝试的其他方法包括: