第 8 章 历史、记忆与状态

上一章结束在一个问题上:材料是对的、证据能核验,可用户上一轮说过的话,这一轮还算不算数?

最常见的第一反应是:“把对话历史都带上不就行了。”这一章解释为什么不行——以及为什么这个答案错得很有价值,因为它恰好指出了第三篇要建立的东西。


一、聊天记录都在,它为什么还会忘

一个反复出现的投诉是这样的:

第 2 轮  用户:我们是 A 公司,想了解产品 A
第 3 轮  AI  :好的,主要想解决什么问题?
第 4 轮  用户:想先确认功能和使用条件
第 5 轮  AI  :明白。方便告诉我您是哪家单位吗?

单位在第 2 轮就说过了,第 5 轮又问了一遍。

这类问题最刺眼的地方在于:所有信息都在上下文里。 第 2 轮那句话没有被截断,没有被压缩,就明明白白摆在那儿,模型也确实“看得见”。

通常的第一反应,是在提示词里加一句提醒:

注意:回答前请先回顾对话历史,确认用户是否已经提供过相关信息,
不要重复询问已经得到的内容。

加上之后,情况会好一阵子。然后它会复发——在更长的会话里、在用户表述比较绕的时候、在同一轮里给了好几个信息的时候。

如果一个问题能被一句提醒缓解,却不能被它根治,那说明缺的不是提醒。

二、三样东西

问题的根源在于,“用户说过的东西”这件事,其实混着三个不同的概念。

术语说明 为了和历史、状态区分,本书把“这一轮从历史、状态和其他来源里筛选出来、实际放进模型上下文的那部分内容”称为工作记忆。它不泛指 Agent 领域里的各种记忆机制——跨会话的用户画像、长期记忆库这些东西,仍然要继续追问一句:它保存的到底是证据、是状态,还是只供本轮参考的上下文。

表 8-1 历史、工作记忆与状态的区别

历史 工作记忆 状态
是什么 发生过什么 这一轮实际让模型看见什么 系统现在承认什么
形态 原始对话记录 筛选、摘要后的上下文片段 结构化字段与流程阶段
谁维护 按追加式日志记录 上下文编排逻辑(第 6 章) 状态管理逻辑(第三篇)
会不会变 不覆盖,只追加 每轮重算 会被更新、纠正、撤回
出问题时 存不全、查不到 该看的没看见 重复追问、旧值污染、流程走错

图 8-1 强调了三者之间最重要的方向:历史为证据与归并提供材料,已提交状态是当前权威读入口,工作记忆则从任务所需的信息中按轮组装。

历史、已提交状态与工作记忆的职责区别

图 8-1 历史、已提交状态与工作记忆

历史那一列需要一句限定:在本书的参考架构里,业务历史按追加式日志处理——纠正通过新增记录表达,而不是覆盖旧记录。实际系统当然还要服从数据最小化和留存期限的要求(第 23 章)。

核心区别只有一句话:

历史记录的是“发生过什么”,状态记录的是“系统现在承认什么”。

这两件事经常不一样。用户在第 2 轮说“我们是 A 公司”,到第 6 轮又说“刚才说错了,是 B 公司”——历史里两句话都在,而且都真实发生过;但系统当前应该承认的单位只有一个。

历史无法回答这个问题。它只能提供推导这个答案所需要的材料。

三、“把历史都带上”为什么不够

明白了上面的区分,第一节那个故障就有解释了:每一轮,模型都要从原始对话里重新推导一次“单位是什么”。

这件事有四个问题。

第一,它需要模型每轮重新做一次语义判断,而这不是确定性的查表。 从五轮对话里认出“A 公司是单位名称”,是判断而不是查询。即使大多数轮次都能判对,也会留下反复失败的机会——而你无法预测是哪些轮次。第一节那种“时好时坏”,就可能由这种重复判断产生。

第二,历史里有过期的信息。 用户改过口、纠正过、撤回过,旧的那句话仍然原样躺在历史里。模型每次重新推导时,都要重新处理一次这个冲突,而且可能得出不同的结论。

第三,否定和拒绝最容易被读漏。 “不方便接电话”在历史里只是短短几个字,周围是几百字的产品讨论。它表达的却是一个必须长期生效的渠道约束。信息的重要性和它在文本里的篇幅,往往不成比例。

第四,历史会增长。 会话越长,重新推导的材料越多、代价越高、也越容易出错;而一旦开始压缩历史,前面三个问题会同时加剧——被压掉的很可能正是那五个字。

生产观察 重复追问的根因,通常不是“模型没看见”,而是系统里没有任何一个地方存着这个答案。每一轮都在重新计算它,于是每一轮都有出错的机会。把一个本该确定的事实,交给一次概率推理去反复重建,这是架构问题,不是提示词问题。

四、多轮本身就更难

除了架构上的原因,还有一个客观因素。

研究支持 多项研究观察到:同样的信息,分散在多轮中给出,比一次性给全更容易导致失败;随着对话轮数增加,指令遵循的表现也会下降。不同任务、不同模型上的幅度差别很大,这里不作为定律引用。具体文献与实验条件见附录 E。

这和第 1 章的规则密度、第 6 章的内容竞争是同一族现象:上下文里需要同时被正确处理的东西越多,每一件被正确处理的概率就越低。

但这一节的重点不是“模型不够好”。即使先假设模型在这件事上表现完美,问题仍然存在——

即使模型在这方面表现完美,“每轮从历史重算状态”仍然是一个不好的架构。 因为它把一件本可以确定下来的事(这个字段的当前值是什么),变成了每轮都要重做一次的判断。

已经提交的状态应该被读取,而不是每一轮从历史里重新推导一遍。

这句话是第三篇的全部动机。

五、状态该长什么样

那么“存下来”是什么意思?

最小的要求有四条:

可查询      随时能问出"单位现在是什么",不必重读一遍对话
可更新      用户纠正时有明确的写入路径,而不是再多一条历史
唯一表示    同一个业务字段,系统只有一份当前值
能表达未知  "没有值""没问过""问了被拒绝"是三件不同的事

第三条要说准:唯一的不是证据来源,而是当前的权威表示。 一个字段的值完全可能由多条证据共同支撑——用户说过一次、后来又确认了一次、外部系统也返回了同样的值。但不管背后有多少条证据,后续逻辑读到的“当前单位是什么”必须只有一个答案,不能各个模块各拿一个版本。这一点在第 12 章会展开。

还要澄清一件事:状态不是“真相”,而是系统当前承认、并据此行动的权威表示。 它可能被抽错、被归并错,也可能过一会儿就被用户纠正。它之所以权威,不是因为它一定正确,而是因为——

系统必须有一个唯一的、可更新、可追溯的地方,明确自己此刻究竟在相信什么。

新证据表明它错了,就通过明确的更新路径去改它,而不是让各个模块各自重新猜一个版本。

第四条尤其重要,也最常被漏掉。第 3 章讲过一次(输出契约里没有“无法判断”这个出口,模型就会被迫编一个),第 7 章又见过一次(注入字段无效时没有出口,模型补出了一个错误的产品)。到第 10 章会看到,它在状态层的形态是:空值、未知、已拒绝、已过期,是四件完全不同的事,而不少简单实现会把它们统一存成“空”,状态差异随之丢失。

需要说明的是,用一个 JSON 存字段,并不等于有了状态。 关键不在数据结构,而在于它是不是系统读取当前值的唯一入口——如果模型依然可以从历史里读出另一个版本并据此行动,那这份 JSON 只是又一段上下文而已。

六、历史压缩:什么可以压,什么不能

有了状态之后,历史的角色反而清楚了:它是证据与审计的来源,但不应该直接充当“当前状态”的权威来源。

历史仍然会进上下文(对话需要连贯性),所以还是要压缩。而压缩要保真到什么程度,取决于一件事:你有没有一个独立的状态层。

情况 A:已经有正式的状态层。 这时职责很清楚——

原始历史        → 审计与证据追溯
已提交状态      → 当前事实与流程控制
摘要 / 工作记忆 → 叙事连贯性

用户有没有拒绝过、当前单位是什么,都不需要靠摘要来保存。摘要甚至可以写成“用户已说明需求背景,当前字段以状态为准”,不必复制每一句原话。这种情况下压缩可以相当激进。

情况 B:还没有状态层。 如果完整历史没法每轮都带进去,下一轮就不得不主要依赖摘要来重新理解当前情况。这时摘要实际上承担了一部分“保管事实”的职责,因此压缩必须保守,并且分三级处理:

表 8-2 历史压缩的分级处理

级别 内容 处理
逐字保留 用户明确表述的字段值、承诺、拒绝与撤回、双方确认过的结论 原文进摘要,不改写
语义压缩 业务问答的结论、已经解释过的知识点 压成一句话要点
可以丢弃 寒暄、重复确认、被撤回后的旧值、模型自己的长篇解释 删除

判断标准是:凡是可能被用来重建状态的内容,必须逐字保真。 一旦摘要把“不方便接电话”改写成“用户对联系方式有顾虑”,重建时就失去了对联系渠道做精确判断的依据——转述不能当证据。

但要说清楚:情况 B 是降级方案,不等价于有状态层。 它能撑住一部分场景(比如提示词托管在第三方平台、根本拿不到应用层),但它把保管事实的责任,压在了一个每轮都会被重写的产物上。

最后,压缩通常也是一个需要单独设计和验证的处理步骤;如果由模型完成,它同样需要契约和评测。

  • 契约:输入是待压缩的轮次和保真清单,输出是结构化摘要,其中逐字保留区必须原样;
  • 评测:用压缩前和压缩后的历史分别重建一次状态,比对关键字段。

关于这个评测要补一句:如果两次重建的结果不一致,说明这条链路存在风险,但不能直接断定是压缩器丢了东西——差异也可能来自抽取环节的方差或归并逻辑。更严格的做法,是拿一份人工确认过的状态作为基准,检查压缩后的上下文能否支撑同样的结论。第 19 章会讲这类评测怎么设计。

即便如此,这个思路仍然值得记住:它把一个模糊问题(“摘要质量好不好”)变成了一个可判定的问题——“压缩之后,还能得出同样的状态吗”。

七、三者的分工

把这一章合起来,分工是这样的:

历史        原始事件记录。回答"当时发生了什么",用于审计、追溯,必要时重建
  │
  ├──→ 工作记忆  本轮实际放进上下文的那部分。回答"这一轮该让模型看见什么"
  │
  └──→ 状态      归并后的权威表示。回答"系统现在承认什么"

记住这三个问题,比记住三个名词有用:

历史问“当时发生了什么”;工作记忆问“这一轮需要看什么”;状态问“系统现在承认什么”。

三种常见的用错方式,对应三种典型故障:

表 8-3 三类概念混用的典型故障

用错的方式 典型故障
把摘要当状态 用户的拒绝、改口、冲突被总结掉了,系统据此做出错误决定
把状态混进普通历史、不标明身份 模型同时看到历史旧值和当前状态,却不知道哪个才是权威版本
用历史当审计凭证,却又压缩过 出问题时无法还原当时到底发生了什么

第 6 章预告过第一种,这里可以把话说完整:摘要是上下文编排的产物,状态是系统当前承认的权威表示。前者服务于这一轮的表达,后者服务于所有轮次的一致性。

也别忘了第 6 章那句:注入不改变身份。 把状态注入上下文完全没问题,问题在于注入时有没有标明它的身份——如果它被拍平成一段和普通历史看不出区别的文字,模型就无从判断该信哪一个。

八、于是问题变成了

假设你接受了本章的结论:需要一份显式的、权威的状态。

一系列新问题会立刻出现——

  • 一个字段是空的,到底表示还没问、问了被拒,还是填错了?
  • 用户先说北京后说上海,信哪一个?是纠正,还是又多了一个需求?
  • 什么时候可以推进到下一步?条件由谁判断,判断几次?
  • 系统现在处在流程的哪个阶段?暂停之后怎么恢复?
  • 状态算错了怎么办?话术和状态冲突时听谁的?

这些问题,第一篇的契约回答不了,第二篇的上下文和检索也回答不了。它们属于另一个层次。

回到第 5 章那张复杂度诊断树,你会发现自己刚刚跨过了那条线:

正确性依赖"前几轮的哪些事实现在仍然有效"吗?
    是 → 显式状态:槽位、证据、归并      〔第 10–12 章〕

这条线的一边是“让模型看见正确的东西”,另一边是“让系统记住正确的东西”。

第三篇讲的就是后者。不过在进入之前,第二篇还剩最后一件事——系统有时候不只要记住,还得真的去做点什么。而“去做”会带来一类全新的风险:它可能宣称自己做了一件从未发生过的事。

读到不对的地方?

勘误、疑问,或者你在自己项目里的验证与反例,都欢迎交流。

微信联系
微信二维码

扫码添加,备注「从提示词到系统」