大模型没有记忆,多轮对话的上下文靠四种消息拼出来

大模型有个反直觉的地方。

它没有记忆。

这一轮说的话,下一轮就当没发生过。

原因是它没有状态,输出只看这一次喂进去的内容。

很多接口也不在服务端替你保存会话历史。

想让它记住,只能在程序里自己维护一份消息列表。

大模型无记忆与消息列表解法的对比示意图

消息是这套东西里最小的单元。

它既代表模型收到的输入,也代表模型给出的输出。

一轮对话,就是一条或多条消息拼出来的。

每条消息不只有文字,还带着一小包元信息。

这包信息记着谁在说话,也记着这话出在第几轮。

这套框架给了一套跨模型统一的消息标准。

换来换去,无论用哪家的模型,写法都不用大改。

格式自动对齐,换模型不用重写一遍。

加多模态内容或者自定义字段,都留了口子。

再往里拆,一条消息就三个字段。

角色,标明它是哪一类。

内容,也就是文字本身。

元数据,可选,装着编号和时间这些零碎。

按角色分,常用的消息有四种。

系统消息,开场就设定角色和行为边界。

用户消息,就是用户的一次输入。

助手消息,是模型给出的回复。

它可能只是文字。

也可能带着一串工具调用。

工具消息,专门用来回填工具跑出来的结果。

四种,各有各的用处。

自己动手做的AI工具,多半绕不开这一层。

写法上有两种。

先看字典那种。

系统消息写成一段带角色的内容。

用户和助手同理,只把角色字段换个值。

工具消息要多挂一个调用编号。

整份写成列表直接喂进去,就能拿到回复。

字典写法跑通后的输出截图

另一种写法用对象。

大项目里更常见。

系统消息换成对应类的构造。

用户和助手各有自己的类,工具也有。

差别只在括号里的参数,语义和字典完全一样。

好处是字段有约束,写错在编辑阶段就能看见。

工具消息这里有一条硬要求。

它的调用编号,必须和上一条助手消息里的对得上。

对不上,模型就接不住这条结果。

对象写法跑通后的输出截图

把两种写法摆在一起,配对关系就清楚了。

四个角色各配一对写法,一个也不落。

用途也分明,有的设定行为,有的装用户输入,有的装模型回复,还有一个装工具结果。

接下来逐个说字段。

系统消息最省事,内容只有一个参数。

参数名可以直接省掉,只写内容那一段。

用户消息也一样,内容字段名可以省。

它多出来的是元数据那一块。

比如给消息加个名字,或者加个编号。

同一种消息出现多条时,就靠这些字段区分。

但不是所有模型都认这套。

支不支持,要看模型供应商怎么讲。

有的接口写着支持名字字段,实测却读不出来。

换另一家中转服务试,名字直接变成了未知。

名字字段返回未知的实测结果截图

助手消息身上的东西最多。

内容之外,它的响应里还挂着一包元数据。

不同模型给的东西不一样,常带这一次任务的用量。

还有一个工具调用字段。

模型决定调工具时,这个字段里就会排出一串调用。

没调工具,它就是空的。

每一笔调用是一个小字典,三个关键字段。

工具名,说明要调哪一个。

参数,把这一次要用到的输入装进去。

示例里那个查天气的工具,参数写的就是城市名。

编号,这笔记调用的唯一标识。

一条助手消息里可以同时排好几笔。

既要查天气,又要拉新闻,就排两条。

编号就是那把钥匙,结果回来得早或晚,都靠它对上号。

还有一点容易忽略。

模型决定调用工具时,文字内容往往是空的。

真正有事的那部分,全挤在调用字段里。

供应商返回的元数据也不一样,有的给模型名和结束原因,有的连调用费用都替你算好。

用量那一块通常有三个数。

输入和输出各用了多少词元,加起来又是多少。

三个数摆在一起,成本就能直接算。

偶尔还会冒出一个不合规调用的字段。

那说明模型拼出来的参数有问题,得回头改工具的描述。

工具消息这边,核心参数也是三个。

内容装工具的输出,名字写工具名。

调用编号必须和助手消息里那笔完全一致。

顺序上也有讲究。

工具消息要紧挨着匹配它的那条助手消息。

模型是按顺序往下读的,一个结果落在它的调用之前,模型会当成没发生过。

四个角色里,只有这一对是靠编号绑在一起的。

其余三个各说各话,互不依赖。

日常AI编程里,这几行几乎是标配。

工具调用返回的助手消息截图

回到最开始那个问题。

做AI对话应用,多轮上文怎么记住是一道必答题。

最笨也最直接的办法,是每一轮都把完整历史传一遍。

第一轮,前面只有系统消息和用户那一句。

第二轮,前面三条都留着,再加一轮新的问答。

第三轮再往上摞,越摞越长。

这里有一条容易踩的线。

每次都要往原来那份列表里追加,不能新建。

别图省事每轮重建列表,上文会当场断掉。

有人第二轮直接开了个新列表。

结果模型当场失忆,连上一句问过什么都答不上来。

还有一种错,是忘了把模型的回复存回去。

下一轮再问,模型自然接不上自己刚说过的话。

历史越摞越长,开销也跟着涨。

词元按量计费,长对话的成本会肉眼可见地爬。

一个实用的收口办法,是只留最近几轮。

系统消息永远留着,它定义角色,不能丢。

更早的对话直接扔掉,只保留最近若干轮。

写成一个函数,之后按需调用。

这样每轮带上去的历史都是短而新的。

模型既知道当前在聊什么,也不会把账算爆。

保留几轮没有标准答案。

三五轮是常见的起点。

宁愿多留一轮,也别让模型接不上话。

任务复杂就再往上加。

还有一种做法是把旧历史压成摘要。

摘要省地方。

代价是细节会丢,取舍要看场景。

有些框架已经把这段逻辑封装好了。

也可以自己写。

几十行就够。

对话历史裁剪后的运行输出截图

内容字段本身还有一层讲究。

它是弱类型的,字符串和列表都能装。

列表里每一项通常还是一段小字典。

图片和文字可以混在同一条消息里。

接口文档页面的截图

纯文字直接给字符串,参数名可以省。

要多模态,就把内容写成一段段字典。

两种形态,按需要挑一种。

四种消息摆在一起,多轮对话的骨架就清楚了。