🧠 第六章 语言模型训练:高效赋能业务场景
📄 一句话概括

同样的配置下,检索命中 10/10,但端到端答案只有 9/10。 差的那一条是「办张金卡要花多少」——材料已经在候选里,选句子那一步选错了。 更关键的是: k 从 1 加到 5,这条错例一直没消失,说明把 k 调大治不了抽取错。

📝 本节和前面几节的关系

前三节把库、k、提示词配好了。这一节回答最后一个问题:怎么判断这套配置到底能不能用。

一、先建一份带关键句的测试集

测试集不是「随便问几个问题看看」。 它要满足三条:每条有标准答案的关键句、覆盖不同的问法、数量够看出趋势。 关键句的作用是让判定可以自动化——答案里包含这句就算过,不包含就算没过。 虽然粗糙,但比「我感觉还行」强一百倍。 问法要覆盖三类: 和文档用词一致的、口语化的、故意问文档里没有的。第三类专门测「它会不会老实说不知道」。 数量上,20 条能看出大方向,50 条能比较两个配置。 别用训练时看过的问题去测——那等于自己给自己出题。

二、两列指标:检索命中 vs 端到端通过

只看一列,你永远不知道问题出在哪。Demo 的输出是并排的两列:

k 检索命中 答案通过 命中却没答对
1 9/10 8/10 1
3 10/10 9/10 1
5 10/10 9/10 1
「检索命中」问的是「正确文档有没有进前 k」,「答案通过」问的是「最后的答案对不对」。两者的差,
就是生成(或抽取)环节丢掉的。
这张表里很干脆:k=1 时检索差 1 条、答案也差 1 条;
k=3 之后检索满了,答案还差 1 条,而且 k 再大也不会好。
这就是分两列的价值:如果只有「答案通过」这一列,你会以为是检索不行,于是把 k 调到 10;
而真正该改的是提示词或者重排。

三、那一条错在哪

错例是「办张金卡要花多少」。 正确答案在「会员等级说明」里:「银卡满两千元,金卡满八千元,钻石满三万元。」 检索没问题——这段确实进了候选。 但 Demo 里的抽取替身挑中的是同一段里的另一句:「等级评定看的是近十二个月的累计实付金额……」。 为什么? 因为问题里「花多少」被同义词换成了「消费金额」, 而「金额」那一句和问题的字面重合更多。于是「最像问题的那句」输给了「含答案的那句」。 这个失败模式在真模型上同样存在: 它会挑一段读起来最相关的文字,而不是挑那句真正给出数字的。解法不是调 k, 是让片段更小——把「等级说明」按档拆成三段,一句一段,抽取就不会选错。

四、测试通过之后还要看什么

准确率过了 90%,还有四件事决定它能不能上线: ① 延迟。 检索加生成一共几秒? 本机跑 7B 模型,首字延迟一两秒是正常的, 但如果检索本身要 3 秒(比如每次重建索引),体验就崩了。 ② 引用可点。 回答里的 [1] 能不能跳回原文? 这是让用户信任它的唯一办法——不能核对的答案,业务方不敢用。 ③ 拒答率。 拿一批「文档里没有」的问题去问,它应该老实说不知道。 拒答率太低说明它在编,太高说明检索太严。 ④ 一致性。 同一个问题问三次,答案应该基本一致。 如果三次三个说法,说明它没在「读材料」,而在「自由发挥」。

五、动手 Demo:检索命中 10/10,答案只有 9/10:差在抽取那一步

对 k 取 1/3/5,每次先算「正确文档有没有进前 k」,再把候选片段切开成句, 用抽取式替身选出「与问题最像的一句」当答案,最后判断答案里有没有那条关键句。 三列并排,损失就分解开了。

for k in (1, 3, 5):
    ret = ok_n = 0
    for j, (q, gi, key) in enumerate(QS):
        top = list(np.argsort(-cos(A * idf, Q[j] * idf))[:k])
        ret += (gi in top)                 # 第一列:检索命中
        best = max((s for di in top for s in SENT[di]),
                   key=lambda s: float(cos(vec(s, vocab)[None, :] * idf,
                                            Q[j] * idf)[0]))
        ok_n += (key in best)              # 第二列:答案通过

实跑输出:

--------------------------------------------------------------------
端到端评估:检索命中 10/10,答案只有 9/10
--------------------------------------------------------------------
  k   检索命中   答案通过   命中却没答对
  -------------------------------------
  1      9/10      8/10              1
  3     10/10      9/10              1
  5     10/10      9/10              1

  三列连起来读:**检索命中 10/10,答案只有 9/10。**差在「抽取」这一步。
  k=3 时「命中却没答对」是 1 条:办张金卡要花多少。
  材料已经在候选里,**但选句子那一步选错了**——因为「与问题最像的那句」不一定是「含答案的那句」。
  这个分解很重要:**端到端差,到底是检索的锅还是生成的锅?**不分开看就永远分不清,
  于是常见做法是「把 k 调大」,而 k 调大治不了抽取错。
  说明:这里的「模型」是抽取式替身,挑与问题最像的一句当答案。
  **真模型会改写、会编,真实通过率通常更低。**

done -> _out_39h.txt

第一行 k=1:检索 9/10、答案 8/10。差的那 1 条是检索丢的。 第二行 k=3:检索满了 10/10,答案还是 9/10。 这时差的 1 条与检索无关——材料在候选里,抽取选错了句。 第三行 k=5:一模一样。 这就是「k 调大治不了抽取错」的直接证据。如果只看总通过率, 你会以为 k=5 也没效果是「模型不行」;分两列才看得出是「不该在检索上使劲」。 要说明这里的「模型」是个替身:从候选里挑与问题最像的一句。 它不会改写、不会编,所以通过率偏高——真模型通常更低。 但「检索 vs 生成」这个分解方法,换真模型一样用。 还有一层:这条错例的根因是片段太大。整段「会员等级说明」600 字,里面四句话都在讲等级, 挑错一句几乎是必然的。 切小之后这个问题自然消失——这是「切块比 k 重要」的又一个证据。

30 秒实验

  1. 把「会员等级说明」那句 600 字的长段按句拆成 5 段,重新跑一遍。 那条错例大概率会消失,而 k 一格的成本反而下降。
  2. 把同义词表里的「花多少→消费金额」删掉,再跑一遍。 错误可能从「选错句」变成「检索不中」——换了一种病,不是没病。
  3. 构造 3 条「文档里没有答案」的问题加进 QS,把关键句设成「暂未查到」。 看这个抽取替身会不会老实拒答——它不会,这就是它和真模型的差距。

常见误区

  1. 「拿训练时看过的问题测」。那等于自己给自己出题,测出来的数字没有意义。测试集要独立准备。
  2. 「只看最终答案对不对」。它无法区分检索错和生成错,于是你会把力气花错地方。
  3. 「通过率 90% 就能上线」。还要看延迟、引用能不能核对、拒答率和一致性。 这四项决定用户信不信它。
自测三问(点开对答案) 1. 为什么测试集里的关键句很重要? —— 因为它把「答案对不对」变成可自动判定的字符串包含检查,不需要人工每次去读。 2. 「检索命中」和「答案通过」的差值说明了什么? —— 说明材料已经找到但没用好,问题出在生成或抽取环节。这时候调 k 没用,要改提示词或切块。 3. 为什么片段太大会导致「挑错句」? —— 因为一段里混了多个相近主题的句子,「与问题最像的那句」很容易不是「含答案的那句」。

6.1 这一节走完了:嵌入模型、建库、提示词、测试。 6.2 换一个客户端做同样的事——Cherry Studio,并且把「向量化」这件事本身算清楚。

相关笔记