觸發場景:Flutter 書籍管理 App 的書目比對——判斷 API 查回來的書跟館藏裡的書是不是同一本。測試給了兩本 ISBN 完全相同的書,相似度算出 0.67、低於 0.8 的匹配閾值:系統認為「不確定是同一本」 疑問來源:ISBN 相同幾乎就是同一本書的定義,演算法哪裡把這個確定性弄丟了? 整理目的:記下加權平均對異質訊號的稀釋機制、訊號分級的修法、以及 identity 比對前的正規化前置 本文邊界:素材是該專案 v0.6.1 的重構記錄;「identity 訊號 vs fuzzy 訊號」的分級思路通用於任何比對 / 評分系統


稀釋機制:平均假設訊號同質、但訊號不同質

原始的相似度是一條加權和:ISBN、標題、作者、出版社各占一個權重、各算一個相似度、加權平均出總分。這個結構對同質訊號成立——四個都是「有點像」的模糊證據時,加權平均是合理的融合。

但 ISBN 不是模糊證據。它是 identity 級訊號:兩本書 ISBN 相同、是同一本書的先驗機率接近 1——這是這個編號存在的目的。把它丟進平均池的後果:ISBN 完全匹配貢獻它權重上限的分數、然後被「標題寫法不同」「出版社欄位缺失」這些 fuzzy 訊號的低分往下拉——確定性被稀釋成 0.67。這個數字的含義很難堪:系統對「ISBN 相同的兩本書」的信心、只比「標題有點像的兩本書」高一點。

調閾值救不了它:降到 0.65 會讓真匹配過關、也讓一批「標題很像但不是同一本」的假陽性一起過關——問題不在線畫哪裡、在兩種確定性被壓進了同一個刻度

修法:identity 訊號短路、fuzzy 訊號才進池

重構把計分拆成兩層、兩個函式各司其職:

  • _calculateIsbnPriorityScore——identity 層:ISBN 完全匹配(含等價形式)時直接給 0.9 以上的基礎分、不進加權池。確定的事實用確定的分數表達
  • _calculateWeightedSimilarity——fuzzy 層:ISBN 不可比(缺失、不匹配)時才落到這裡,標題、作者、出版社的加權平均在同質訊號之間做它擅長的事

結構化之後、分數重新有了語意:0.9 以上讀作「identity 證據確認」、中間段讀作「fuzzy 證據的綜合強度」——下游要對兩種情況做不同處置(自動合併 vs 請使用者確認)時,分數本身就攜帶了依據。這個「強訊號短路、弱訊號加權」的形狀在別處反覆出現:規則引擎的 hard rule 先於 scoring、搜尋的 exact match 先於 ranking——凡是訊號有等級的地方,先分級再融合。

前置:identity 訊號自己要先正規化

第二個修正堵住 identity 層自己的漏接:同一本書的 ISBN 有兩種合法表示——ISBN-10 跟 ISBN-13(978 前綴加重算校驗碼)。等價檢查沒實作時(重構前它是一條 TODO 註解),一邊是 10 碼一邊是 13 碼的同一本書、在 identity 層比對失敗、跌進 fuzzy 層被當成「標題很像的兩本書」。

原則跟 Money 與 VO 的相等性相通:同一身分的多種表示、比對前先化到正規形式。identity 訊號的價值建立在「相等判斷可靠」上,表示層的多樣性沒被吸收掉、identity 層就會系統性漏接——而且漏得很安靜,每一筆都降級成 fuzzy 比對、只是分數低一點。

附帶的第三個修正也值得一行:原本自製的「簡化版 Jaro-Winkler」字串相似度被記錄為「過於簡化、無法準確計算」——fuzzy 層的訊號品質同樣要顧,自製知名演算法的簡化版、通常是拿演算法的名字背書一個不是它的東西。

判讀徵兆

  • 「明明是同一個東西、比對分數卻不高」——查有沒有 identity 級欄位被丟進加權平均
  • 評分公式裡確定性訊號(唯一編號、hash、精確鍵)跟模糊訊號(字串相似、日期接近)共用權重——先分級、讓確定訊號短路
  • identity 欄位有多種合法表示(含連字號 / 不含、10 碼 / 13 碼、大小寫)而比對是裸字串相等——正規化缺席、identity 層在漏接
  • 調閾值的討論反覆出現——通常是訊號結構的問題假扮成閾值問題

相關閱讀