把 10MB 中文语义模型塞进浏览器:STSB 做到教师模型的 93%,但线上 top1 只有 42%

先说结论

我花了两天自训了一个 8.4MB 的中文语义嵌入模型,塞进浏览器离线跑。它在标准评测上达到了教师模型的 93.2%,但上线后真实检索 top1 只有 42%。

而我把教师模型(bge-large)拿去跑同一个任务,top1 是 62% ------ 也就是说,这个任务的天花板本身就不在这个位置,我优化了半天的地方根本不是瓶颈。

这篇文章想讲的就是:怎么发现自己的优化打在了空气上,以及最后那个"正确的解"是什么样。


一、为什么非得自训

我在做一个美术风格的感受检索工具。用户输入「空灵、留白、冷」这种自然语言感受,系统把它映射到受控标签体系(27 个基础感受 + 260 个细词 + 598 个美术风格)。

难点在于:**用户不会用我的词表说话。**他说「潮乎乎的旧教堂」,我得知道那是"潮湿 + 怀旧 + 冷"。这必须靠语义嵌入,而纯关键词匹配完全不可行。

现成方案我都试过了,全死:

方案 体积 问题
Ternlight(现成英文小模型) 7 MB 中文能力不可用
bge-small-zh-v1.5 ~92 MB 效果可用,体积超标 9 倍
调云端 embedding API 0 违背产品前提:描述不上传、离线可用

我的硬约束是自己定的,因为产品承诺了"全部计算在浏览器本地完成":

  1. 权重 + 分词器 + 推理引擎 总计 ≤ 10MB
  2. 中文语义相似度必须可用(近义词显著高于无关词)
  3. 浏览器端推理(WASM / ONNX / ggml)
  4. 单次嵌入 ≤ 10ms(目标)

于是只能自训。路线是三步:砍词表 → 蒸馏 → 量化。


二、第一步:砍词表,砍掉一半死重

这里有一个我没想到的事实,是跑统计才发现的:

拿教师模型(bge)的 21128 词表,在 10 万句中文通用语料上统计词频,有 10922 个词从未出现过。

一半以上的词表在中文场景下是死重。而且 top-4000 就覆盖了 98.7%,top-8000 覆盖 99.9%。

这个数据直接支持了"另建 32k 中文 BPE"的决定。我没有在教师词表上做 trim,而是从零训了一个 32k 的 BPE:

erlang 复制代码
平均句长 12.8 个汉字 → 10.0 token
教师 WordPiece 同样句子 → 14.8 token
省约 30%

注意最后这个数字------它后面变成了真正的工程约束:**我的 max_length 是 64 token,按 1.25 token/字算,每条文本最多只能写 60 个汉字左右。**这个限制后来把我语料写作规范整个重塑了一遍(后面讲)。


三、第二步:蒸馏,指标达标了

学生模型:2 层 Transformer、隐藏 384、6 头、CLS 池化、32k BPE。

训练过程中两个关键数字:

第一轮: 学生 AUC 保住了教师的 96%,但 STSB 相关性只有 0.639 ------ 没到规格线(要求教师的 85%)。

分析下来是语料问题。补语料重建后再评:

erlang 复制代码
STSB 0.639 → 0.730
达到教师的 93.2%(规格要求 85%)

过了。这时候我以为核心工作完成了。


四、翻车:指标达标,线上还是搜不出来

把模型接进检索链路,跑真实查询。结果:

erlang 复制代码
场景类查询 top1 命中率:42%
游戏美术细分类查询 top1:20%

42% 意味着用户问两三次就有一次第一屏是错的。

我一开始的判断是"模型还是太弱,得再压一压",打算继续加层数、加语料。但加之前,我做了一件后来证明是整个项目最有价值的事:

我先跑了教师模型作为上限。

ini 复制代码
教师 bge-base-zh  top1 = 45%
我的学生模型       top1 = 42%

两者几乎持平。

这一刀直接砍掉了我的错误假设:瓶颈不在模型。我把学生做到和教师一样强,也只能到 45%。真实差距在 top3(学生 67 vs 教师 82)------这说明模型能力不是没有,但第一名的排序被某些东西盖住了。


五、然后我又发现:三个教师的数字可能是假的

既然是选型问题,我把当时最强的几个中文 embedding 都拉出来对比:

ini 复制代码
bge-base-zh    top1 = 45%
bge-large-zh   top1 = 62%
Qwen3-Embedding top1 = 8%   ← 不对劲

Qwen3-Embedding 在 MTEB 中文榜上是顶级水平,不可能只有 8%。我去查池化方式:

Qwen3-Embedding 用的是 last-token 池化(它是 CausalLM 架构),我按 CLS 池化取到的是 BOS 位置。整个对照组的数据是废的。

修正池化后重跑,17%。仍然低,但不是 8% 了。结论反而更清楚:Qwen3-Embedding 是为"检索"优化的(query-doc 非对称),而我的任务是"细粒度情感/审美区分",两者不是一回事。 榜单第一不等于你的任务第一。

这一段的教训:换池化方式这种低级错误,能让你的整个选型对比全部作废。 而且它不会报错,只会给你一个"看起来很低但好像也说得过去"的数字。

修正后的对照:

模型 体积 top1 备注
Qwen3-Embedding 数百 MB 17% 检索优化,不对口细粒度情感
bge-base-zh 92 MB 45%
自研(蒸馏) 8.4 MB 42% 保住教师 93% 的能力
bge-large-zh 1.3 GB 62% 真正的上限

我 8.4MB 的模型拿到了 1.3GB 模型 68% 的效果。 从投入产出看,这一步其实是合算的。

但 62% 也说明:这个任务本身就只有 62% 的上限。剩下 38% 不在模型里。


六、真正的瓶颈:索引锚点覆盖率

回头看错误样本,问题很直白:

每条标签我只写了一句话做锚点,然后 embed 存起来。用户查「雪地脚印」,锚点是「孤独、荒凉」------模型再强,也没法把"雪地脚印"和"孤独"在一句话里对齐,因为那条索引里根本没有雪地、没有脚印。

模型能给的是语义,给不了不存在的信息。你的索引覆盖了多少,检索就能到多少。

于是我把力气从"训模型"转到"写索引",然后立刻踩到第二个坑。


七、第二个坑:双路嵌入,不能拼接

我的语料每条有两个字段:

  • gloss:近义词 + 易混词串(词袋路)
  • explain:定义 / 视觉 / 触觉 / 情绪极性 / 搭配对象 / 近义边界(属性路)

直觉做法是把两者拼成一段,embed 一次。实测这是错的。

erlang 复制代码
只 embed explain:自命中 103 → 4(噪声塌缩,几乎全崩)
整体检索正确率:91% → 22%

原因:拼接后的文本超过了有效注意力范围,而且两路语义分布不同,混在一起会把向量拉到两个簇的中间------两边都不像。

正确做法是两路各自独立 embed,检索时分别打分取 max,绝不拼接:

erlang 复制代码
中文:91%(与单路最好的持平,但覆盖了两类查询)
英文:60% → 75%

代价是存储翻倍(每条两条向量),但对 8.4MB 的模型来说这个代价可以接受。


八、物理约束:为什么最后没上 WASM

计划里是 WASM。但实测纯 JS 实现:

复制代码
加载 103ms
单次嵌入 86--150ms
同级 WASM 实现:7.5ms

慢 12--20 倍。为什么没直接换 WASM?因为查到一个会翻车的兼容性问题:

中文内核是 SIMD 编译的,而 iPhone 上所有浏览器(都是 Safari 内核)在 16.4 之前不支持 WASM SIMD,编译会直接失败。

我的用户里有相当比例是手机上刷到就点开的。为了 7.5ms 的延迟,让一部分人打开就是白屏,不划算。最终的方案是:

实时的是词典层(同步、无延迟),语义层异步补正。 用户打字时索引立刻动,语义结果晚 100--150ms 补上。体感上完全没有延迟感,因为它不在关键路径上。


最终交付

erlang 复制代码
模型:2 层 / 384 维 / 6 头 / CLS 池化 / 32k 中文 BPE
权重:7.4MB(三值量化)
引擎 + 分词器:约 1MB
总计:8.4MB,max_length 64 token
STSB:教师的 93.2%
真实检索 top1:42%(教师同任务 45%)

能离线跑、能断网用、用户输入的任何描述都不出浏览器。


三条可以搬走的经验

1. 优化之前,先测你的上限。 我差点花两天去压模型,只因为没先跑一次教师。项目里最有价值的一次测量,是那个"教师也只有 45%"的数字------它省下的不是一点时间,是方向。

2. 换池化方式 / 换 tokenizer 这种"低级"差异,能让整组对照实验作废,而且不报错。 Qwen3 那个 8% 如果我没去查,我会得出"Qwen3 不适合情感任务"这个看起来很有道理的错结论,并且写进选型文档。

3. 模型从来不是瓶颈,索引覆盖率才是。 8.4MB 拿到了 1.3GB 的 68%。剩下 32% 不在权重里,在"你给每个标签写了多少真正能被搜到的描述"里。我后来的绝大部分时间都花在写语料上,而不是调模型。


作品在这里,桌面端打开效果最好:xiximm.cloud/

输入框直接打字就行,比如「空灵、留白、冷」「不要热闹的宁静」「潮湿的旧教堂」。全部计算在浏览器本地完成。


本文的每一个数字都来自项目开发过程中的实测记录。

#模力全开#

相关推荐
光锥智能1 小时前
海康机器人举行2026移动机器人新品发布会:箱式存拣升级,「明智」系列发布
java·前端·机器人
计算机魔术师1 小时前
Anthropic 上市文件最吓人的一幕,不是亏损 807 亿
前端
alsmile1 小时前
AI友好的可视化图形引擎自我革命:Meta2d.js 2.0 历时一年全新重写,性能与多人协作全面升级
前端·开源·数据可视化
行思理1 小时前
Nginx 跨站请求(CORS)配置完全指南:从原理到生产实践
java·前端·nginx
粥里有勺糖1 小时前
视野修炼-技术周刊第137期 | 编程已经面目全非
前端·github·ai编程
IT_陈寒2 小时前
Python的列表推导式用不好真的会爆内存你知道吗
前端·人工智能·后端
moshuying2 小时前
块扩散 + GDN 改造:Clover 把「并行生成」塞进了 Agent 的长上下文里
前端·算法
kyriewen2 小时前
我算了笔账:AI 编程工具一年烧掉两台 MacBook
前端·人工智能·程序员
moshuying3 小时前
扩散模型开始写代码、开网页了:350 亿参数,速度比 Jev 快一倍
前端·算法