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 會被拒绝,那一行單独重試。
比模型更重要的是两個细節:
- 沒有"置信度低于多少就送去兜底"的阈值。每條记錄都拿到當前最合適的槽。模型不確定,就標成低置信,界面可以展示,但记錄仍然有家。
- 有一個單独的池子,裝那些根本不是记憶的东西:測試語句、一句光秃秃的"好的"、語音引擎從静音裡幻聽出來的碎片。這些是被隱藏,不是被上架。隱藏是一個有明確规則的明確決定,而一個叫"其他"的抽屉两者都不是。
標簽直接存在记錄上,旁邊放著生成它的那版分類文本的哈希。改了某個槽的描述,只有旧版本打的標簽會被重新排隊。用戶亲手挪过的记錄,机器永遠不再碰。
让第三層自己長出來
槽的下面,結構被允许自己生長。记錄會被向量化,簇在每個槽內部按几何關系形成。簇只在長到很大(大约 150 條)時才拆,而且只在两半真的不一樣時才拆——我们把拆分跑三次,候選看起來像重复就拒绝。保留原始锚點的那一半保留名字,另一半起新名字。
這一層就是"音乐創作"和"音乐表演"可以作為两個簇出現、却不需要任何人预先定義的地方,同時两者仍然在同一個槽、同一個門裡。在犯錯便宜的地方涌現,在犯錯昂贵的地方穩定。
為什麼配圖不是生成的
每個門、每個槽都有配圖,沒有一张是圖像模型按需生成的。它是一套人工策划的圖库,每個門、每個槽一张,同一種畫风。涌現出來的簇繼承它所在槽的那张圖。
另一條路我们試过。生成的封面惊艳一周,然後書架看起來像十個插畫师吵了一架。一段你會看很多年的记憶,不該每次模型重训就換一张臉。
仍然會出錯的地方
向量空間不是各向同性的。在一份真實語料上,一個門吞掉了大部分體積,只因為模型對工作類文本的表示更密。让語言模型讀字來分類,遠好于在那個空間裡量距離——這也是為什麼門的判斷不用最近質心。
而且分類器是判斷,不是測量。它會把一條關于副業定價的筆记放進 Business & Growth,而當事人觉得那是 Money。沒關系:人可以挪,机器记住不挪回去,分類名稱本地化成八種語言,纠正的感觉像整理書架,不像跟模型較劲。
系統再也不會做的一件事是耸肩。沒有一個抽屉是留給它懒得決定的东西的。
這不是行銷,是我們自己踩過的坑。