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

「应对能力」不是一个能力,是一堆具体毛病的集合:答非所问、丢了信息、重复追问、跟不上改主意、 一句话两个请求只接住一个。 这一节把六种手段拆开看,重点放在两个能立刻见效的:槽位追踪 + 主动追问,和错误处理。

📝 本章和前面几章的关系

上一节管的是「上下文里放什么」,这一节管的是「拿着这段上下文怎么做事」。 状态机与流程控制,17 课 编程原理 里讲过; 这一节讲的是把对话当成一个带状态的过程,而不是一串独立的问答。

一、针对性的数据集与微调

先别急着「提升能力」,先把毛病列出来。列不出来,就没有验收标准。 每一类毛病找二十条真对话,标出正确答法,进训练集。 这比再多喂一万条通用数据有效得多——通用数据治不了具体毛病。 标的时候要标「这一轮该做什么」,不只是「该答什么」。缺参数时该追问,比给一个猜出来的答案更对。

二、实时反馈与自适应性

三道闸:置信度低就换个说法重问,超时就给模板答案,连续两轮没进展就转人工。 阈值不要拍脑袋。拿一个月的线上日志,把「误转人工率」定在业务能接受的那个点上,再回头定阈值。 这三道闸的意义不在准确率,在兜底。多轮系统最怕的不是答错,是答错之后一直错下去。

三、强化学习优化

RLHF 在多轮上的难点是奖励稀疏:这一句答得好不好,往往要等三轮之后才知道。 两种常见处理:把奖励摊到每一轮(过程奖励),或者用偏好对做 DPO,绕开奖励模型。 这条路成本高,一般是有单轮基础之后才做。单轮都答不好,做多轮强化学习是浪费算力。

四、上下文感知与个性化

上下文感知是「知道现在说到哪了」,个性化是「知道这个人是谁」。两件事经常被混为一谈。 前者靠槽位和状态机,后者靠用户画像与历史偏好。两样都是工程,不是模型能力。 演示里的策略 B 和 C 就是上下文感知的最小版本:记住已收的槽位,只问缺的那个。 它不需要更强的模型,只需要一张表。

个性化的风险是过度。 给老客户省掉确认步骤很贴心,给新客户省掉就是事故——判断依据要能说清,否则不要自动做。

五、多模态与多任务

多模态是让模型看图、听声;多任务是让一个模型干几种活。两样都会让训练难度上一个台阶。 多任务的风险是互相打架:两个任务的输出格式不一样,混训会让格式变得不稳定,表现是「时好时坏」。 现在的通行做法是「一个基座 + 多个 LoRA」,用哪个挂哪个。比混训稳,也比混训省。

六、错误处理与恢复策略

三类失败要分开:答不出、答错、答跑题。三类的处理方式完全不同。 答不出就追问,或者承认不知道;答错要能被发现(给可验证的字段、给来源);答跑题靠意图识别拦下来。 恢复比预防更重要。 多轮对话里没有一次不错的系统,只有错了能拉回来的系统。评价一个系统, 看它出错之后第几轮能回到正轨。

七、动手 Demo:三个槽位分三轮给,看追问答几次才凑齐

场景:用户分三轮说出部位、预算、时间。系统要凑齐这三个槽位才能出方案。 对比三种策略:不记历史、记历史按缺追问、把槽位压成一句写进系统提示。 最后再看一句带两个意图的话会怎么丢。

SLOTS = [(u'部位', (u'外立面', u'楼顶', u'入口')),
         (u'预算', (u'万', u'预算')),
         (u'时间', (u'下周', u'月底', u'明天'))]

# 策略 B:每轮抽槽位,缺什么问什么
got = {}
for t in UTTER:
    got.update(extract(t))
    miss = [n for n, _k in SLOTS if n not in got]
    print(u'、'.join(got), miss[0] if miss else u'(不用追问)')

# 策略 C:把已收槽位压成一句系统提示
st = u'已知:部位=外立面;预算=二十万;时间=下周。'

实跑输出:

--------------------------------------------------------------------
三个槽位分三轮给,看追问答几次才凑齐
--------------------------------------------------------------------


  ① 场景:一句话给一个槽位
  --------------------------------------------------------------------
| 轮 | 用户说 |
|:---:|---|
| 1 | 外立面那块灯光太暗了,得改 |
| 2 | 预算大概二十万左右 |
| 3 | 下周就得进场,来得及吗 |

  三句话分别给了部位、预算、时间。真机上还会夹着闲聊、反问、改主意。

  ② 策略 A:不记历史,每轮独立处理
  --------------------------------------------------------------------
| 轮 | 输入 | 本轮抽到 | 系统追问 | 算无效 |
|:---:|---|---|---|:---:|
| 1 | 外立面那块灯光太暗了… | 部位 | 请问要改的是哪个部位? | 是 |
| 2 | 预算大概二十万左右… | 预算 | 请问要改的是哪个部位? | 是 |
| 3 | 下周就得进场,来得及… | 时间 | 请问要改的是哪个部位? | 是 |

  每轮都抽到了当轮那个槽位,但一个都没留住——第三轮结束时手里还是空的。
  追问 3 次,三次问的都是「哪个部位」。
  ! 这就是「无状态」的代价:每一轮都从零开始,用户被问了三次同一个问题。
  很多「这机器人怎么这么笨」的抱怨,根子在这里,不在模型。

  ③ 策略 B:记历史,缺什么问什么
  --------------------------------------------------------------------
| 轮 | 已收槽位 | 系统追问 | 还缺 |
|---|---|---:|:---:|
| 1 | 部位 | 预算大概多少? | 2 |
| 2 | 部位、预算 | 什么时候要进场? | 1 |
| 3 | 部位、预算、时间 | (不用追问) | 0 |

  三轮凑齐 3 个槽位,追问 2 次,每次问的都是当时真缺的那个。
  ! 注意第二轮的追问问的是「预算」而不是「部位」——因为部位已经在第一轮拿到了。
  追问的价值不在问,在于不问已知的。

  ④ 策略 C:把已收槽位压成一句写进系统提示
  --------------------------------------------------------------------
| 策略 | 第 3 轮上下文 | 槽位 | 无效追问 |
|---|---:|---:|---:|
| A 无状态 | 16 | 0 / 3 | 3 |
| B 记住原话 | 50 | 3 / 3 | 1 |
| C 槽位压缩 | 77 | 3 / 3 | 1 |

  A 的上下文最省,因为它只看当前这一句——省下来的都变成了无效追问。
  C 比 B 多 27 token,那是「把槽位再写一遍」的成本。
  ! 轮数一多,这个差别就翻过来了——原话每轮涨,槽位只涨字段。十轮之后 C 会明显更省。

  ⑤ 一句话两个意图
  --------------------------------------------------------------------
| 处理方式 | 识别到 | 完成 |
|---|---|:---:|
| 单意图处理 | 只接第一个 | 1 / 2 |
| 双意图处理 | 改方案、报价 | 2 / 2 |

  这句话里有两个意图。单意图的处理只接住第一个,报价那个被静默丢掉。
  ! 用户不会说「我有两个请求」。一旦你只处理一个,他不会重问,他会觉得你没听懂。
  所以「一句话多意图」这句检查要写进评测表。

①三句话分别给了一个槽位。真实对话比这乱得多:夹着闲聊、反问、临时改主意,而且用户经常一次给两个。 ②策略 A 不记历史:三轮都问「哪个部位」,三次全是无效追问。 很多「这机器人怎么这么笨」的抱怨,根子在这里,不在模型。 注意它每一轮其实都抽到了槽位(部位、预算、时间各一次),只是没存下来。抽得到和留得住,是两件事。 ③策略 B 记历史:追问两次,每次问的都是当时真缺的那个。 价值不在「问」,在「不问已知的」——这一点让人觉得系统在听。 ④三种策略一起看:A 的第 3 轮上下文最省(只有当前一句),省下的全变成了无效追问; C 比 B 多 27 token,那是把槽位再写一遍的成本。 轮数一多这个差别就翻过来:原话每轮都涨,槽位只涨字段。十轮之后 C 会明显比 B 省。 ⑤一句话两个意图:单意图处理只接住「改方案」,报价被静默丢掉。 用户不会说「我有两个请求」,他不会重问,他只会觉得你没听懂。 所以「一句话多意图」要写进评测表。它是那种不漏在指标上、只漏在体感上的毛病。

30 秒实验

  1. 给 SLOTS 加一个「联系人」,但三轮话里都没提。 看策略 B 会追问几次——这就是「必填项缺失」时系统卡住的样子。
  2. 把策略 B 的追问改成「一次把所有缺的都问一遍」,看重填次数从 2 降到 1, 但用户的认知负担变成多少?
  3. 改 extract(),让它在同一句里抽两个槽位(比如「外立面二十万」),重跑策略 B。 看看实际需要几轮。

常见误区

  1. 以为换个更大的模型就能解决。 无状态的问题换多大模型都一样——它连上一句说了什么都不在输入里,再强也没用。
  2. 只做预防不做恢复。多轮系统一定有出错的时候,没有回退、没有转人工,一次失败就丢掉整个会话。
  3. 把个性化做成「猜」。猜错了比不问更糟:客户以为你记着,其实你记的是别人的。 凡是自动跳过的步骤,都要能说清依据。
自测三问(点开对答案) 1. 为什么「不记历史」的系统会重复追问同一个问题? —— 因为它每一轮都从零开始,输入里只有当前这一句。 上一轮拿到的部位不在输入里,它就会认为还缺,于是再问一遍。这不是笨,是信息没给到。 2. 槽位压缩为什么轮数越多越划算? —— 因为原话的长度随轮数线性涨,而槽位只涨字段名。十轮之后原话上千 token,槽位还是那几十个。 交叉点通常在五六轮。 3. 「一句话两个意图」为什么容易漏? —— 因为大多数系统按「一次一个意图」设计,第二个意图既不报错也不进日志。 用户不会重问,只会觉得没被听懂,所以你连 badcase 都收不到。

手段讲完了,下一节回答那个绕不开的问题:怎么知道它到底变好了没有。

相关笔记