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

前面所有的结论,最后都要落成一条字符串。这一条字符串拼错了,前面全白做。 这一节把四种拼法摆在一起看:token 差不太多,但「模型看到几个轮次边界」差很多。

📝 本章和前面几章的关系

上一节验完了模型,这一节把「怎么问」定下来。 字符串怎么切分、怎么转义,17 课 编程原理 里讲过; 这一节讲的是一串字符里的「结构」怎么被模型读出来——靠的不是标签,是位置和习惯。

一、Llama 2 的模板长什么样

三层结构:最外层 <s> 与 </s>,中间用 [INST] 与 [/INST] 包住用户的话, 系统提示包在 <<SYS>> 与 <</SYS>> 里。 系统提示只在第一个 [INST] 里出现一次。多轮时后面每轮只有 [INST] 用户话 [/INST] 助手话 </s>。 </s> 的位置是关键:它在助手话后面,告诉模型「该我说完了」。 少一个,模型会一直往下编——因为它在等一个结束信号。

[INST] 与 [/INST] 是成对的。少一个收尾,模型分不清哪句是用户说的、哪句该它接。

二、四种拼法,差在哪

朴素拼接(Q:/A:):谁都看得懂,token 最省。 但模型在预训练里没见过这个约定,学的是「模仿这个格式」,不是「接着对话」。 只有 [INST] 不带系统提示:省 token,但丢了角色设定。适合「不需要人设」的场合。 SYS 在首轮:官方写法,124 token。角色、格式约束都在这段里。

再加一句「已知:部位=外立面;预算=二十万;」:144 token,多 20 个。 这 20 个 token 买的是「硬字段不会被截断」。

三、系统提示里该放什么

放三样:角色(你是谁)、格式约束(先结论后依据、不超过多少字)、硬字段(已知的槽位)。 不放三样:具体业务知识(该进检索)、示例对话(占 token 且容易过拟合)、 长篇否定式清单(模型记不住「不要做」的十条)。 硬字段放这里的理由是位置:历史会被截断,系统提示不会被截断。每次都一定读到的位置, 才配放不能丢的东西。

这也解释了一个常见现象:把预算写进用户第一句话,十轮之后它就没了;写进系统提示,三十轮之后它还在。

四、拼错的样子

系统提示写在 [INST] 外面:模型当普通文本读,角色设定基本失效。 每轮都写一遍系统提示:模型以为指令在变,输出风格来回跳,一会儿正式一会儿口语。 少一个 </s>:回答完不收尾,一直往下编,最后被长度上限截断。

共同点:都不报错。所以拼完必须把字符串原样打印出来看一遍,比调参快十倍。

五、本章小结

这一章六节,绕着一个问题转:多轮对话里,模型为什么忘事。 4.1 七条原则要能落成代码;4.2 保多轮与拆单轮要混用;4.3 槽位存字段、摘要存过程、最近几轮存原话。 4.4 追问的价值在于不问已知的;4.5 评测输出必须是分类不是分数; 4.6 系统提示只写一次、槽位句每次都在。

如果只记一句:多轮的问题八成不在模型,在「你给了它什么」。历史拼错、槽位没存、 系统提示重复——这三样修好,7B 的表现能上一个台阶。 下一章换个方向:不再改对话形式,而是往里灌一个行业的全部知识。

六、动手 Demo:同一段对话四种拼法,token 与可解析性各差多少

同一段两轮对话,用四种方式拼:朴素拼接、只有 [INST]、SYS 在首轮、SYS 加槽位句。 量三件事:token 多少、能解析出几个轮次、系统提示在不在。最后把推荐写法原样打出来。

# 官方写法:SYS 只在第一个 [INST] 里
s = (u'<s>[INST] <<SYS>>\n%s\n<</SYS>>\n\n%s [/INST] %s </s>'
     u'<s>[INST] %s [/INST] %s </s>' % (SYS,) + tuple(t for _r, t in CONV2))

# 加槽位句:多一行「已知:…」
st = u'已知:部位=外立面;预算=二十万;'
s = (u'<s>[INST] <<SYS>>\n%s\n%s\n<</SYS>>\n\n%s [/INST] %s </s>'
     u'<s>[INST] %s [/INST] %s </s>' % (SYS, st) + tuple(t for _r, t in CONV2))

# 数一数:拼完之后有几个 [INST],就是几轮
n_turn = s.count(u'[INST]')

实跑输出:

--------------------------------------------------------------------
同一段对话四种拼法,token 与可解析性各差多少
--------------------------------------------------------------------


  ① 要拼的这段对话
  --------------------------------------------------------------------
| 角色 | 内容 |
|---|---|
| 用户 | 外立面改亮,预算二十万 |
| 助手 | 结论:轮廓加重点。理由:石材适合洗墙。 |
| 用户 | 色温定多少 |
| 助手 | 结论:三千。理由:暖白跟石材搭。 |

  两轮四句。同一份内容,拼法不同,模型看到的「边界」就不同。

  ② 四种拼法
  --------------------------------------------------------------------
| 拼法 | token | 轮数 | 系统提示 |
|---|---:|:---:|:---:|
| 朴素拼接 | 72 | 2 | 无 |
| 只有 [INST] | 81 | 2 | 无 |
| SYS 在首轮 | 124 | 2 | 有 |
| SYS + 槽位句 | 144 | 2 | 有 |

  token 差得不多,但「能不能解析出轮数」差很多——朴素拼接靠「Q:」这个约定,
  模型在预训练里没见过这个约定。
  ! 这是多轮微调最费钱的一处:格式不对,模型学的是「模仿这个格式」,不是「接着对话」。

  ③ 两种写法的实际字符串
  --------------------------------------------------------------------
  ┌─ 朴素拼接
  │ Q:外立面改亮,预算二十万
  │ A:结论:轮廓加重点。理由:石材适合洗墙。
  │ Q:色温定多少
  │ A:结论:三千。理由:暖白跟石材搭。
  └─
  ┌─ SYS 在首轮
  │ <s>[INST] <<SYS>>
  │ 你是工程助理,回答先给结论再给依据,不超过六十个字。
  │ <</SYS>>
  │ 
  │ 外立面改亮,预算二十万 [/INST] 结论:轮廓加重点。理由:石材适合洗墙。 </s><s>[INST] 色…
  └─
  看第二段:<<SYS>> 只出现一次,位置在第一个 [INST] 里面。
  ! 常见的错写法是把系统提示塞在 [INST] 外面,或者每轮都写一遍。
  前者模型当普通文本读,后者会以为指令每轮都变。

  ④ 推荐模板:SYS 在首轮 + 槽位句
  --------------------------------------------------------------------
  ┌─ 推荐写法(144 token)
  │ <s>[INST] <<SYS>>
  │ 你是工程助理,回答先给结论再给依据,不超过六十个字。
  │ 已知:部位=外立面;预算=二十万;
  │ <</SYS>>
  │ 
  │ 外立面改亮,预算二十万 [/INST] 结论:轮廓加重点。理由:石材适合洗墙。 </s><s>[INST] 色…
  └─
  比官方写法多一句「已知:部位=外立面;预算=二十万;」,20 token。
  ! 这一句的作用是把硬字段从历史里提出来,放在模型每次都一定读到的位置。
  历史可以截断,系统提示不会被截断——这就是它值这几个 token 的原因。

  ⑤ 真机上的拼法
  --------------------------------------------------------------------

  [真机代码 · 本沙箱不执行]
  | from transformers import AutoTokenizer
  | 
  | tok = AutoTokenizer.from_pretrained('meta-llama/Llama-2-7b-chat-hf')
  | ids = tok.apply_chat_template(msgs, tokenize=True, add_generation_prompt=True)
  | out = model.generate(ids, max_new_tokens=256)
  要 transformers 与 7B 权重,本沙箱都没有,不执行。

  apply_chat_template 做的就是上面那几种拼法,它按模型的模板文件来拼。
  自己手拼的意义在于:出问题时你知道每一段是哪来的。
  ! 最后一条经验:模板拼错的表现不是报错,是「答得有点像但总差一点」。
  遇到这种情况,先把拼好的字符串原样打印出来看一遍,比调参快十倍。

  ⑥ 本章小结
  --------------------------------------------------------------------
  这一章六节,绕着一个问题转:多轮对话里,模型为什么会「忘事」。
| 节 | 一句话 |
|:---:|---|
| 4.1 | 七条原则要能落成七行代码 |
| 4.2 | 保多轮省 token,拆单轮样本多,两种都要 |
| 4.3 | 槽位存字段、摘要存过程、最近几轮存原话 |
| 4.4 | 追问的价值在于不问已知的 |
| 4.5 | 评测输出必须是分类,不是分数 |
| 4.6 | 系统提示只写一次,槽位句每次都在 |

  ! 如果只记一句:多轮的问题八成不在模型,在「你给了它什么」。
  历史拼错、槽位没存、系统提示重复——这三样修好,7B 的表现能上一个台阶。

①两轮四句。内容是同一份,拼法不同,模型看到的「边界」就不同。 ②token 从 72 到 144,翻了一倍。 但要注意:翻倍的不是内容的量,是结构的量。系统提示和标记占了七成。 「能解析出几个轮次」这一列全都一样是 2,因为每种拼法都保留了两次用户发言。 但它靠的东西不同:朴素拼接靠 Q: 这个约定,官方写法靠 [INST]。 ③把两种字符串原样打出来看,差别一眼就出来:官方写法里 <<SYS>> 只出现一次, 位置在第一个 [INST] 里面。 常见的错写法有两种:把系统提示塞在 [INST] 外面,或者每轮都写一遍。 前者模型当普通文本读,后者会以为指令每轮都在变。 ④推荐的写法只比官方多一行:「已知:部位=外立面;预算=二十万;」,多 20 token。 这 20 个 token 的作用是把硬字段从历史里提出来,放在每次都一定读到的位置。 历史会被截断,系统提示不会被截断——这就是它值这 20 个 token 的原因。 ⑤apply_chat_template 做的就是上面那几种拼法,它按模型的模板文件来拼。 自己手拼的意义在于:出问题时你知道每一段是哪来的。 最后一句经验:模板拼错的表现不是报错,是「答得有点像但总差一点」。遇到这种情况, 先把拼好的字符串原样打印出来看一遍。 ⑥本章小结那张表是六节的压缩。如果只带走一句:多轮的问题八成不在模型,在「你给了它什么」。

30 秒实验

  1. 把系统提示从一百多字扩到四百字,重跑第②步。token 涨到多少?按一百轮对话算,多花多少?
  2. 故意把第一个 </s> 删掉,看拼出来的字符串变成什么样。再想想:模型会怎么表现?
  3. 把「已知:…」那句话从系统提示移到第一句用户话里,重跑一遍。两者的 token 一样,差别在哪? (提示:想想哪一处会被截断)

常见误区

  1. 以为模板只是格式。模板决定模型看到几个轮次边界,边界错了,学到的就是「模仿格式」。
  2. 每轮都带系统提示。多花 token 是小事,输出风格来回跳是大事,而且很难归因到模板上。
  3. 手拼完不打印。 拼错不报错,只表现成「答得有点像但总差一点」,于是你开始调 temperature、换模型、 加数据——全是白费。
自测三问(点开对答案) 1. 为什么系统提示只写在第一个 `[INST]` 里? —— 因为这是 Llama 2 模板的定义:`<>` 段属于对话的开头部分。 每轮都写,模型会以为指令被重新下达,输出风格会跟着变。 2. 硬字段为什么放在系统提示,而不是第一句用户话里? —— 因为位置不同、命运不同。历史会被截断、会被滑窗挤掉,系统提示每次都完整带上。 「不能丢」的东西,要放在「不会被丢」的位置。 3. 模板拼错的典型表现是什么? —— 不报错,只是答得「有点像但总差一点」:格式偶尔对、收尾偶尔多、人称偶尔乱。 这种症状最容易被误判成模型能力不足,于是白调一堆参数。

第 4 章到此结束。下一章换个方向:不再改对话形式,而是把一个行业的全部知识灌进去。

相关笔记