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

12 段知识库、10 个口语问题:字二元组 Top-1 命中 7/10; 补一张 15 条的同义词表后 9/10——修好 3 个,同时弄坏 1 个。 三个错例的关键词(专票、 快递、免邮)在文档里一次都没出现,相似度全是 0,argmax 随手取了第一段。

📝 本节和前面几节的关系

上一章把微调讲完了。这一章换一条路:不训模型,只给它接一个知识库。第一步就是配嵌入模型。

一、它和对话模型是两个东西

Page Assist 是浏览器插件,它自己不跑模型,它把请求转发给本机的 Ollama(或者别的地方的 API)。 所以「训练 DeepSeek」这个说法,实际做的是给 DeepSeek 接一个知识库——模型参数一个都没动。 这件事需要两个模型同时在场:对话模型(deepseek-r1 之类)负责答, 嵌入模型负责把问题和文档都变成向量,好让它们能算相似度。 新手最常卡的一步就是只拉了对话模型,忘了拉嵌入模型,然后知识库那一栏永远是灰的。 📋 实操清单(要在浏览器里点): 第一步,装 Page Assist 扩展,打开设置,把 Ollama 地址填成 http://localhost:11434。 第二步,在终端拉嵌入模型:ollama pull nomic-embed-text。体积几百 MB,比对话模型小得多。 第三步,回扩展的设置里找到 RAG/知识库 那一栏,把「嵌入模型」下拉框选成刚拉的那个。 第四步,随便上传一份文档,看它有没有显示「已向量化」。这一步不通,后面全是白费。

二、为什么非要用嵌入模型

不用嵌入模型行不行? 行,那就退化成字面匹配——而字面匹配在「用户说法和文档写法不一致」时会彻底失效。 Demo 用 12 段客服文档和 10 个故意口语化的问题跑了一遍。 字二元组(两个字一组)做向量,Top-1 只命中 7/10。 错的三个是「专票几天能寄到」「快递不动了找谁」「多少钱免邮」。 原因很干脆:「专票」「快递」「免邮」这三个词,12 段文档里一次都没出现。 于是这三条问题的相似度全是 0。 而在全零的数组上取最大,argmax 只会返回第一个下标——答案稳定地落到第一段「退换货总则」上。 补一张同义词表(专票→增值税专用发票、快递→物流、免邮→包邮),命中从 7/10 涨到 9/10。 这就是嵌入模型要解决的事:把字面匹配换成语义匹配。

三、同义词表为什么撑不住

看起来补表就够了,但 Demo 里还有一条反例:补表之后「修好 3 个,也弄坏 1 个」。 被弄坏的是「东西用过了还能退吗」。 表里把「用过了」换成「使用」, 而「使用」这两个字在最长那段(会员等级说明)里也出现过一次——于是它被拉到了完全不相干的文档上。 这就是规则法的死结:每加一条规则,都可能给别的查询开一个后门。文档越多、说法越多, 这张表就越补不完。 嵌入模型的训练目标,正是把「说法不同、意思相同」映射到相近的向量上,而不是靠人一条条补映射。 一个模型换掉一张永远补不完的表。

四、选模型看三件事

① 语言。 中文场景优先中文或多语模型。英文模型对中文的分词粒度很差,效果会明显掉。 ② 维度。 常见 384、512、768、1024。 维度高不一定好——它只影响表达力上限,而你的知识库可能只有几百段,高维度带来的收益很有限, 存储和算力倒是线性涨。 ③ 体积。 嵌入模型通常几十 MB 到 1 GB 上下,要能塞进内存。 它每次检索都要跑一遍,所以延迟也归它管。 经验做法: 先用一个中等体积的中文模型跑通,建 20 条测试问题测命中率,不够再换大模型。别一上来就上最大的—— 换模型的收益,往往小于把切块和 k 调对的收益。

五、动手 Demo:字面匹配 7/10,加同义词 9/10:修好 3 个也弄坏 1 个

12 段客服知识库文档(60 字到 600 字不等),10 个口语化问题,每个问题标注了正确文档和关键句。 用字二元组做向量,比三种方案:原样、加同义词归一、再加 IDF 加权。

def bigrams(s):
    t = u''.join(ch for ch in s if ch not in u' \n,。、;:!?()')
    return [t[i:i + 2] for i in range(len(t) - 1)]

SYN = {u'专票': u'增值税专用发票', u'快递': u'物流', u'免邮': u'包邮',
       u'用过了': u'使用', u'退钱': u'退款', u'清零': u'过期清零'}

for j, (q, gi, key) in enumerate(QS):
    top = int(np.argmax(cos(A, Q[j])))
    hit += (top == gi)

实跑输出:

--------------------------------------------------------------------
字面匹配 7/10,加同义词 9/10:修好 3 个也弄坏 1 个
--------------------------------------------------------------------
  文档数                   12 段
  问题数                   10 个
  词表大小                 631 个字二元组
  同义词条数               15 条(手写)
  方案                 Top-1 命中   命中文档平均长度
  -------------------------------------------------
  ① 字二元组(原样)        7/10              83 字
  ② +同义词归一            9/10              88 字
  ③ +IDF 加权              9/10              88 字

  ① 只对「问法和写法一样」的问题有效。错的 3 个是:
  专票几天能寄到、快递不动了找谁、多少钱免邮
  原因一模一样:**关键词一个都不在文档里。**
  「专票」「快递」「免邮」这三个词,12 段文档里一次都没出现。
  于是这 3 条问题的相似度全是 0——**并列最后,argmax 随手取了第一段。**

  补一张 15 条的同义词表(专票→增值税专用发票、快递→物流、免邮→包邮),命中从 7/10 涨到 9/10。
  但注意它不是免费的:**修好 3 个的同时弄坏了 1 个**——剩下的那条错例是「东西用过了还能退吗」。
  因为「用过了」被换成「使用」,而「使用」在最长那段里也出现过,
  **同义词把答案引到了不相干的文档上。**
  这就是「配置文本嵌入模型」这一步真正在解决的事:**把字面匹配换成语义匹配。**
  同义词表是最土的办法,它撑不住「用户有一百种说法」,**而且每加一条都可能带来新的误配**;
  嵌入模型的训练目标,是把「说法不同、意思相同」映射到相近的向量上,而不是靠人一条条补映射。
  另外还有一件事要在配置时确认:**向量库用余弦还是内积。**余弦会除掉向量长度,内积不会——
  不归一化就用内积,**长文档会被系统性抬高**,哪怕它跟问题关系不大。

第一屏三个方案:7/10、9/10、9/10。① 到 ② 提升明显, ② 到 ③ 一点没动——IDF 在 12 段的小语料上没再带来收益,这也是实话。 ② 的 9/10 不是「修好 3 个」那么简单:它修好 3 个、弄坏 1 个。 坏的那条是「东西用过了还能退吗」。 为什么坏?因为「用过了→使用」,而「使用」在最长那段里也出现过。 一段 600 字的长文档,天然会撞上更多词——这就是长文档的隐性优势。 最后那两句讲度量:余弦会除掉向量长度,内积不会。不归一化就用内积,长文档会被系统性抬高。 这条在配置向量库时一定要确认,它比换模型更容易被忽略。 诚实说明:那 15 条同义词是我按这批问题手写的,换成别的问题集就不适用了。 这个 Demo 要说明的是「字面匹配的边界在哪」,不是「这张表有用」。

30 秒实验

  1. 把同义词表删掉两条(专票、免邮),看命中率掉回多少。 这两条各对应一个「零重叠」的问题,删一条掉一个。
  2. 把最长的「会员等级说明」拆成三段,再跑一遍 ①。长文档被拆小之后,它的「隐性优势」就没了。
  3. 把问题改成文档里的书面说法(专票→增值税专用发票),再跑 ①。 命中会立刻回到 10/10——这证明字面匹配没错,错在「用户不这么说话」。

常见误区

  1. 「装了插件就能用知识库」。还差一个嵌入模型。对话模型和嵌入模型是两个模型,要分别拉。
  2. 「嵌入模型越大越好」。维度只决定表达力上限,几百段的知识库用不上 1024 维。 先把切块和 k 调对。
  3. 「补一张同义词表就能替代嵌入模型」。每加一条都可能开一个后门,Demo 里就出现了修 3 个坏 1 个。
自测三问(点开对答案) 1. 为什么只拉对话模型,知识库用不了? —— 因为知识库要靠嵌入模型把问题和文档变成向量才能算相似度。没有嵌入模型,检索这一步根本不存在。 2. 三个错例为什么相似度全是 0? —— 因为问题里的关键词(专票、快递、免邮)在文档里一个字都没出现,字二元组重合一格都没有。 全零时 argmax 只会返回第一个下标。 3. 补同义词表为什么会「弄坏」一条? —— 因为替换后的词可能在别的文档里也出现。 「用过了→使用」把问题引到了含「使用优惠券」的长文档上。

嵌入模型配好了,下一步是建库。而建库时第一个要拍板的数是:每次检索捞几段回来。

相关笔记