「信息流失」的具体表现是:用户第一句说的预算,第五句就没人记得了。 根子不在模型,在上下文里到底还留着什么。 这一节把四种存法摆在一起量:全拼接、滑窗、摘要、槽位。 同一段十轮对话,token 从 507 降到 42,关键事实从丢三个变成一个不丢。
上一节造了数据,这一节解决推理时的上下文管理。 缓存与页面置换,29 课 Linux 是怎样工作的 里讲过; 这一节讲的是当「淘汰」发生在对话历史上,淘汰掉的东西意味着什么。
一、拼接历史:最简单,也最贵
把前面所有轮次原样拼进上下文,是默认做法。好处是零丢失,代价是 token 线性涨。 演示里十轮 507 token,六轮 351。 每生成一个 token,这五百多个都要重读一遍——注意是每个 token 都重读,不是每轮。 真实对话一轮上百 token,三十轮就是三千多。这时候问题不是「贵不贵」,而是「会不会超出上下文窗口」。
拼接还有个隐性代价:越靠前的内容,模型越容易忽略。这不是玄学, 「中间被淹没」是长上下文里反复被观测到的现象。
二、上下文向量:压成一个数串
用嵌入模型把历史编成向量,需要时把最相关的几句捞回来放进上下文。 它不是「压缩」,是「索引」——原文还在库里,只是不常驻上下文,需要时才取。所以上下文长度是恒定的。 代价是「捞不回来就真丢了」。 检索靠字面或语义相似,而「刚才那个数」这种指代,相似度算不出来——它跟前文里的「二十万」一点都不像。
这条路解决的是「知识太多」,不是「指代太多」。 要治指代,还得靠槽位或者多轮数据。完整做法是第 6 章的 RAG。
三、对话状态追踪:把事实抠出来存表
槽位是业务对话里最划算的做法。先定义几个字段——部位、预算、时间——每轮抽一遍,抽到就填。 演示里五个事实压成一句话只要 42 token,五个一个不少。这个性价比是四种方案里最高的。 抽槽位可以用规则(关键词匹配),也可以用小模型。 规则能覆盖八成,剩下两成——同义说法、口语省略——交给模型。
难点是「改主意」:用户先说二十万,后说二十五万。槽位要能更新,而且要知道哪次是改主意、哪次是补充。 前者覆盖,后者累加。
四、状态压缩:摘要与裁剪
摘要适合开放话题:没有槽位可定,只能把过程压成几句话。 裁剪(滑动窗口)适合闲聊。用在业务上是灾难:演示里窗口只留 156 token,五个事实剩两个。 有意思的是演示里「预算」居然还在——因为最后一句又复述了一遍。 靠复述保命不算设计,用户不复述的时候它就没了。
摘要的风险在「压什么」。
按含数字的规则抽,会漏掉中文数字:「挪三十厘米」就是这么丢的,因为它匹配不到 [0-9]。
五、增量学习与在线学习
增量学习是把新对话定期喂回去重训。它让模型跟上新说法,风险是只喂新数据会忘掉旧的——这叫灾难性遗忘。 在线学习是每来一条就更新一次。工程上很少真这么做:一是贵,二是没法回滚。 模型被一条脏数据带坏,你连什么时候坏的都不知道。 务实的组合是「离线重训 + 在线检索」:模型一季一版,新知识放进检索库,当天生效。 把「学得快」和「记得住」拆给两套系统,比逼一个模型同时做到容易得多。
六、动手 Demo:十轮对话,四种存法,看关键事实还剩几个
一段十轮的工程对话,里面埋了五个关键事实:部位、预算、色温、防护等级、灯位偏移量。 然后用四种方式处理历史,逐个检查这五个事实还在不在。 「还在」的判据是关键词还在上下文里。 这个判据很粗——它能证明「没丢」,不能证明「模型会用」,后者得靠第 4.5 节的评测。
FACTS = [(u'部位:外立面', u'外立面'), (u'预算:二十万', u'二十万'),
(u'色温:三千', u'三千'), (u'防护:IP65', u'IP65'),
(u'灯位:挪三十厘米', u'三十厘米')]
# 方案一:全拼接
ctx = join(TURNS)
print(sum(1 for _a, kw in FACTS if kw in ctx))
# 方案二:滑窗,只留最近三轮
ctx = join(TURNS[-6:])
# 方案三:摘要,每轮抽一句含数字或关键词的
for _r, t in ms:
for seg in re.split(u'[,。;]', t):
if re.search(u'[0-9]', seg):
out.append(seg.strip()); break
# 方案四:槽位,拼成一句话
st = u''.join(u'%s=%s;' % (k, v) for k, v, _s in slots)
实跑输出:
--------------------------------------------------------------------
十轮对话,四种存法,看关键事实还剩几个
--------------------------------------------------------------------
① 这段对话里有五个关键事实
--------------------------------------------------------------------
| 事实 | 原文里怎么出现 |
|---|---|
| 部位:外立面 | 外立面 |
| 预算:二十万 | 二十万 |
| 色温:三千 | 三千 |
| 防护:IP65 | IP65 |
| 灯位:挪三十厘米 | 三十厘米 |
十轮、二十句。事实分散在第 1、2、3、5、7 轮——越靠前越容易被后面的历史挤掉。
② 全拼接:token 随轮数线性涨
--------------------------------------------------------------------
| 保留轮数 | 上下文 token | 事实还在 |
|---:|---:|---:|
| 2 轮 | 133 | 2 / 5 |
| 4 轮 | 239 | 4 / 5 |
| 6 轮 | 351 | 5 / 5 |
| 8 轮 | 458 | 5 / 5 |
| 10 轮 | 507 | 5 / 5 |
全拼接的好处是零丢失——十轮时五个事实全在。代价是 token 线性涨。
! 线性涨听着不吓人,但真实对话一轮就上百 token。
三十轮就是三千多 token,每一次生成都要重新读一遍。
③ 滑动窗口:只留最近三轮
--------------------------------------------------------------------
| 事实 | 最近三轮里 |
|---|:---:|
| 部位:外立面 | 丢了 |
| 预算:二十万 | 还在 |
| 色温:三千 | 还在 |
| 防护:IP65 | 丢了 |
| 灯位:挪三十厘米 | 丢了 |
窗口只留 156 token,但五个事实只剩 2 个。
! 滑窗最适合闲聊——反正前面说什么不重要。用在业务对话上就是灾难。
注意③里预算居然「还在」,那是因为最后一句又复述了一遍——靠复述保命不算设计。
④ 摘要压缩:每轮抽一句进摘要
--------------------------------------------------------------------
| 事实 | 摘要里 |
|---|:---:|
| 部位:外立面 | 还在 |
| 预算:二十万 | 还在 |
| 色温:三千 | 还在 |
| 防护:IP65 | 还在 |
| 灯位:挪三十厘米 | 丢了 |
摘要 115 token,留住了 4 个事实。
! 压缩的关键是「压什么」。
这里按「含数字或关键词」抽,所以预算、色温、防护都留下了,而「挪三十厘米」用的是中文数字,
匹配不到 [0-9] 这条规则——规则抽不准的地方,就是摘要方案的风险点。
⑤ 状态槽位:把事实存成表,不存原话
--------------------------------------------------------------------
| 槽位 | 值 | 来源 |
|---|---|---|
| 部位 | 外立面 | 第 1 轮 |
| 预算 | 二十万 | 第 2 轮 |
| 防护 | IP65 | 第 3 轮 |
| 色温 | 三千 | 第 4 轮 |
| 灯位偏移 | 三十厘米 | 第 5 轮 |
槽位拼成一句话只有 42 token,五个事实一个不少。
! 代价是要预先定义槽位。
业务对话的槽位是有限且稳定的,所以这条路在客服、报修、报价这类场景里最划算;
开放闲聊没有槽位可定,只能用摘要。
⑥ 四种方案摆在一起
--------------------------------------------------------------------
| 方案 | token | 事实 | 适合 |
|---:|---:|---|---|
| 全拼接 | 507 | 5 / 5 | 闲聊、短对话 |
| 滑动窗口 | 156 | 2 / 5 | 不需要记忆的场景 |
| 摘要压缩 | 115 | 4 / 5 | 长对话、开放话题 |
| 状态槽位 | 42 | 5 / 5 | 业务对话、要精确的字段 |
! 真实系统里这四种是叠加用的:槽位存硬字段,摘要存过程,最近几轮存原话。
单独用一种,不是丢信息就是烧 token。
还有一种做法是把历史编成一个向量(上下文向量),那是第 6 章 RAG 的路子,这里不展开。
①先看这五个事实在第几轮出现:第 1、2、3、4、5 轮。越靠前,越容易被后来的历史挤掉。 ②全拼接:轮数翻倍,token 也差不多翻倍,从 133 到 507。五个事实一个不丢。 这是基线,其他三种方案都要拿它来比。 ③滑窗只留 156 token,五个事实剩两个。 丢掉的是部位、防护、灯位——全是第一轮和第三轮说的,早被挤出去了。 预算居然还在,因为最后一句又复述了一遍。这种情况在真实对话里不多见,拿它当运气,别当设计。 ④摘要 115 token,留住四个。丢的是「挪三十厘米」——它既没有阿拉伯数字,也没命中关键词表。 摘要的风险就在这:规则覆盖不到的地方,会安静地丢。 ⑤槽位 42 token,五个一个不少。 这是四种里 token 最省、保真最高的,但它有一个前提:槽位要人先定义好。 ⑥把四种摆在一起看:从 507 到 42,token 降了十二倍,保真度反而升了。 这不是「压缩做得好」,是「存原话」和「存事实」本来就是两件事。 真实系统里这四种是叠加的:槽位存硬字段、摘要存过程、最近几轮存原话。 单独用一种,不是丢信息就是烧 token。
30 秒实验
- 把滑窗从三轮改成五轮,重跑第③步。多留的两轮救回了哪个事实?token 涨了多少?
- 把摘要规则里的
[0-9]换成「阿拉伯数字或中文数字(一二三四五六七八九十)」, 看「三十厘米」能不能救回来,摘要会变长多少。 - 给槽位再补两个字段(比如「工期」「甲方名称」),看拼成一句话要多少 token。 想一想:槽位多到什么程度,就不比全拼接省了?
常见误区
- 以为上下文越长越好。演示里给全部历史,token 是滑窗的三倍多,而关键事实只多几个; 更麻烦的是,长上下文里靠前的内容会被忽略。
- 用滑窗处理业务对话。 用户第一句报的参数,滑窗到第八句就没了,而用户不会重说一遍,他只会觉得你没记住。
- 把摘要当成无损压缩。摘要一定会丢东西,问题只是丢的是不是关键。 所以摘要之后必须有一道检查:五个关键字段还在不在。
自测三问(点开对答案)
1. 为什么说上下文向量解决的是「知识太多」,不是「指代太多」? —— 因为检索靠相似度,而「它」「那个」跟前文里的名词一点都不像,检索不出来。 向量索引适合「资料库很大、要用哪段拿哪段」,不适合「这句话在指哪一句」。 2. 槽位方案最省,为什么不是所有场景都能用? —— 因为它要求事先定义好字段。业务对话的字段有限且稳定,所以能用; 开放话题没有固定字段可定,只能退回摘要。 3. 「靠复述保命」为什么不算设计? —— 因为它是运气。演示里预算还在,只是因为用户最后又提了一次; 用户不提的时候,滑窗照样会把它丢掉,而你不会察觉。历史管好了,下一节往上一层看:除了管上下文,还有哪些手段能提高多轮应对能力。
- ☐ (本篇完,下一篇 4.4 提高大模型多轮对话应对能力)
相关笔记
- 29 课 Linux 是怎样工作的:缓存与淘汰策略
- 造数据集:模板与轮次结构
- 保存与加速:KV 缓存占的是显存
- 🦙 Llama 大模型实践 · 索引