🦙 第4章 解决 Llama 2 大模型多轮对话难题
📄 一句话概括

「信息流失」的具体表现是:用户第一句说的预算,第五句就没人记得了。 根子不在模型,在上下文里到底还留着什么。 这一节把四种存法摆在一起量:全拼接、滑窗、摘要、槽位。 同一段十轮对话,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 秒实验

  1. 把滑窗从三轮改成五轮,重跑第③步。多留的两轮救回了哪个事实?token 涨了多少?
  2. 把摘要规则里的 [0-9] 换成「阿拉伯数字或中文数字(一二三四五六七八九十)」, 看「三十厘米」能不能救回来,摘要会变长多少。
  3. 给槽位再补两个字段(比如「工期」「甲方名称」),看拼成一句话要多少 token。 想一想:槽位多到什么程度,就不比全拼接省了?

常见误区

  1. 以为上下文越长越好。演示里给全部历史,token 是滑窗的三倍多,而关键事实只多几个; 更麻烦的是,长上下文里靠前的内容会被忽略。
  2. 用滑窗处理业务对话。 用户第一句报的参数,滑窗到第八句就没了,而用户不会重说一遍,他只会觉得你没记住。
  3. 把摘要当成无损压缩。摘要一定会丢东西,问题只是丢的是不是关键。 所以摘要之后必须有一道检查:五个关键字段还在不在。
自测三问(点开对答案) 1. 为什么说上下文向量解决的是「知识太多」,不是「指代太多」? —— 因为检索靠相似度,而「它」「那个」跟前文里的名词一点都不像,检索不出来。 向量索引适合「资料库很大、要用哪段拿哪段」,不适合「这句话在指哪一句」。 2. 槽位方案最省,为什么不是所有场景都能用? —— 因为它要求事先定义好字段。业务对话的字段有限且稳定,所以能用; 开放话题没有固定字段可定,只能退回摘要。 3. 「靠复述保命」为什么不算设计? —— 因为它是运气。演示里预算还在,只是因为用户最后又提了一次; 用户不提的时候,滑窗照样会把它丢掉,而你不会察觉。

历史管好了,下一节往上一层看:除了管上下文,还有哪些手段能提高多轮应对能力。

相关笔记