为什么 Reso 里没有"未分类"

"其他"不是你的笔记的事实,而是系统承认它从来没做决定。我们用十个稳定的门、封闭列表分类、噪声池和可以在下面自由涌现的簇取代了它。

Engineering

每一个带自动整理功能的笔记工具,最后都会长出一个叫"其他"、"杂项"或"未分类"的抽屉。我们的也长了。某天早上打开 Nebula,36 条记忆躺在一个真的叫"未归类"的主题里,旁边还有一个叫"其他"的兜底。同一个失败,两个抽屉。

我们把这当成 bug 处理,而不是当成数据的属性。这篇讲 bug 最后是什么,以及我们最终的设计。

"未分类"里到底装的是什么

追踪那 36 条记录,没有一条是真的无法归类。它们来自两个机械的缺口:本该接住低置信记录的兜底主题,因为一个谁也够不到的阈值而不可达;一个从大簇里拆出来的小簇在路上丢了父级标签,到可视化的时候就没有主题了。

这就是普遍规律。"未分类"很少是内容模糊,它是管线停止做决定的地方,然后把"没有决定"包装成了一个类别。

十个门,为了稳定而定

修复从一个听起来显然、其实不显然的决定开始:分类的顶层是固定的。十个门,手写,目标是对一个人多年对着麦克风说的东西做到不重不漏:

  • Building(构建)
  • Work & People(工作与同事)
  • Business & Growth(业务与增长)
  • Money & Investing(金钱与投资)
  • Learning & Ideas(学习与想法)
  • Creating & Expression(创作与表达)
  • People & Relationships(人与关系)
  • Body & Mind(身心)
  • Life Admin(生活事务)
  • Direction & Meaning(方向与意义)

涌现式主题在 demo 里令人兴奋,在日常里让人疲惫。顶层一旦允许漂移,"career"和"work"这个月是两个本子,下个月合成一个,所有依赖这些名字的界面都得跟着改。顶层的稳定,是下面一切可以流动的前提。

每个门下面有一小组"槽",总共四十来个,同样手写。真正的归属发生在槽这一层。

一个必须做选择的分类器

归属由一个小型托管语言模型按批完成,对着封闭列表做。它看到门、门下的槽、每个槽一句描述,以及十几条笔记;必须以 JSON 返回每条笔记一个槽 id,别的什么都不能返回。不在列表里的 id 会被拒绝,那一行单独重试。

比模型更重要的是两个细节:

  1. 没有"置信度低于多少就送去兜底"的阈值。每条记录都拿到当前最合适的槽。模型不确定,就标成低置信,界面可以展示,但记录仍然有家。
  2. 有一个单独的池子,装那些根本不是记忆的东西:测试语句、一句光秃秃的"好的"、语音引擎从静音里幻听出来的碎片。这些是被隐藏,不是被上架。隐藏是一个有明确规则的明确决定,而一个叫"其他"的抽屉两者都不是。

标签直接存在记录上,旁边放着生成它的那版分类文本的哈希。改了某个槽的描述,只有旧版本打的标签会被重新排队。用户亲手挪过的记录,机器永远不再碰。

让第三层自己长出来

槽的下面,结构被允许自己生长。记录会被向量化,簇在每个槽内部按几何关系形成。簇只在长到很大(大约 150 条)时才拆,而且只在两半真的不一样时才拆——我们把拆分跑三次,候选看起来像重复就拒绝。保留原始锚点的那一半保留名字,另一半起新名字。

这一层就是"音乐创作"和"音乐表演"可以作为两个簇出现、却不需要任何人预先定义的地方,同时两者仍然在同一个槽、同一个门里。在犯错便宜的地方涌现,在犯错昂贵的地方稳定。

为什么配图不是生成的

每个门、每个槽都有配图,没有一张是图像模型按需生成的。它是一套人工策划的图库,每个门、每个槽一张,同一种画风。涌现出来的簇继承它所在槽的那张图。

另一条路我们试过。生成的封面惊艳一周,然后书架看起来像十个插画师吵了一架。一段你会看很多年的记忆,不该每次模型重训就换一张脸。

仍然会出错的地方

向量空间不是各向同性的。在一份真实语料上,一个门吞掉了大部分体积,只因为模型对工作类文本的表示更密。让语言模型读字来分类,远好于在那个空间里量距离——这也是为什么门的判断不用最近质心。

而且分类器是判断,不是测量。它会把一条关于副业定价的笔记放进 Business & Growth,而当事人觉得那是 Money。没关系:人可以挪,机器记住不挪回去,分类名称本地化成八种语言,纠正的感觉像整理书架,不像跟模型较劲。

系统再也不会做的一件事是耸肩。没有一个抽屉是留给它懒得决定的东西的。

这不是营销,是我们自己踩过的坑。

下载 macOS 版 Reso

阅读更多 Reso 构建日志