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 秒实验
- 把同义词表删掉两条(专票、免邮),看命中率掉回多少。 这两条各对应一个「零重叠」的问题,删一条掉一个。
- 把最长的「会员等级说明」拆成三段,再跑一遍 ①。长文档被拆小之后,它的「隐性优势」就没了。
- 把问题改成文档里的书面说法(专票→增值税专用发票),再跑 ①。 命中会立刻回到 10/10——这证明字面匹配没错,错在「用户不这么说话」。
常见误区
- 「装了插件就能用知识库」。还差一个嵌入模型。对话模型和嵌入模型是两个模型,要分别拉。
- 「嵌入模型越大越好」。维度只决定表达力上限,几百段的知识库用不上 1024 维。 先把切块和 k 调对。
- 「补一张同义词表就能替代嵌入模型」。每加一条都可能开一个后门,Demo 里就出现了修 3 个坏 1 个。
自测三问(点开对答案)
1. 为什么只拉对话模型,知识库用不了? —— 因为知识库要靠嵌入模型把问题和文档变成向量才能算相似度。没有嵌入模型,检索这一步根本不存在。 2. 三个错例为什么相似度全是 0? —— 因为问题里的关键词(专票、快递、免邮)在文档里一个字都没出现,字二元组重合一格都没有。 全零时 argmax 只会返回第一个下标。 3. 补同义词表为什么会「弄坏」一条? —— 因为替换后的词可能在别的文档里也出现。 「用过了→使用」把问题引到了含「使用优惠券」的长文档上。嵌入模型配好了,下一步是建库。而建库时第一个要拍板的数是:每次检索捞几段回来。
- ☐ (本节完,下一节 6.1.2 创建知识库)