家人說「資料好多,不知道怎麼選」——我拆掉權重滑桿,換上八個品項籤


家人朋友實際用過我的優惠券網站之後,回饋集中在一句話:

資料好多,不知道怎麼選。

還補了一句「像是漢堡,炸雞,薯條,蛋塔」。

而網站當時對「幫我選」這件事的答案,是一支權重滑桿。它問的是「你覺得配餐值幾成價」——你拉動它,網站重新計算每張券的划算程度,然後重新排序。至於畫面上的券有幾張?一張也不會少。

那支滑桿是怎麼來的

前一輪做 CP 排序引擎的時候,Claude 憑著回歸算式推測「需要權重調整」,就做了這支滑桿,收在 ▸ 進階 摺疊裡。做之前沒特別跟我提,我看到網頁才知道。

這件事我不追究。本來真的做出來之後就有實測階段,我會親自看過——而我也確實看了,還說出了那句後來變成轉向起點的話。只是當下我沒有把「看不出差別」昇級成「該拆掉」,要等家人的回饋把同一個感覺說成一句具體的抱怨,它才變成重建的指令。

我說的話

改權重看不出差別,不如做成包含選項。比方說選了漢堡和薯條,出現的優惠券就必須同時有這兩項。

轉向的起點就是這句。回頭看,「選了漢堡和薯條、就必須兩項都有」——篩選的 AND 語意在我這句話裡就已經定下來了,後面整輪設計只是把它做對。

我當時還開過一個折衷條件:如果真想保留權重的話,可以在背後偷算,拿選項當開關。Claude 說這樣沒意義,我對此無所謂,就同意不做。它沒意義的理由,放在後面講設計決定時一起說。

權重從機制上就治不了這句回饋

Claude 把我的直覺展開成機制層的診斷:score.ts 的 getImpliedTotal() 只被 value 與 ratio 兩個排序維度呼叫——權重一張券都不會濾掉。拉完滑桿,畫面上還是 915 列。回饋抱怨的是「資料好多」,那是篩選問題;權重是排序工具,一開始就不在同一條路上。

還有三個附帶問題,每一個都讓這支滑桿離使用者更遠:它問的是估價師的問題(「你覺得配餐值幾成價」),不是顧客的問題(「你想吃什麼」);它收在 ▸ 進階 摺疊裡,會抱怨的人多半沒展開過;而當時真正在減少資料量的,只有搜尋(要先知道打什麼)和份量分區籤(solo/duo/family/dessert——那是份量,不是品項)。

沒有任何控制項讓使用者說「我要吃薯條」。

這筆帳兩邊都有份。滑桿是 Claude 憑回歸算式推測做的,回答了一個沒人問它的問題;但實測階段我親自看過,也感覺到「改權重看不出差別」,卻沒有動手拆。做了一個沒被要求的東西的,是 Claude;把這樣的東西留下來的,是我。

顆粒度:唯一的設計決定

家人說「漢堡」,但 menu.json 裡根本沒有漢堡——堡類拆成 8 個品名(卡啦雞腿堡、脆雞堡、花生熔岩雞腿堡……),薯條拆小、中、大。籤要做在哪一層?Claude 沒有憑感覺挑,而是用 917 張真實券的語料量了三個層級:

層級籤數兩兩 AND 空集合
原始品名36630 組中 391 組空(62%)
品項家族(採用)828 組中 0 組
既有 item_types5換皮不換骨,side 把薯條和鱈魚圈混在一起

原始品名為什麼會壞成這樣:一張券很少同時含兩種堡——917 張裡只有 25 張——而同時含兩種薯的,是 0 張。想吃漢堡的人面對原始品名籤,得按 8 次還不能一起按。

所以整個設計的承重牆是這一句:「預設 AND」能成立的前提,是每個籤本身已經是一次 OR。 8 個家族籤、跨家族 AND、家族內 OR——不是附帶細節,是這個篩選能動起來的全部前提。日後誰想「讓籤更精細一點」,那正是會把它推倒的改動。

出籤的 8 個家族命中券數:炸雞 604、蛋塔 495、漢堡 290、薯條 271、雞塊 194、甜點 138、其他炸物 118、飯食 46。有兩個家族有 family 標籤但不出籤:drink 命中 842/917(92%),當篩選籤只能濾掉 8%,純粹佔行動版一格;sauce 只有 2 張券。兩者在 brand.json 標 chip: false——分類留著(新增品項才不會靜默漏掉),籤不渲染。

四個設計決定,各否決一個替代案

  1. 家族清單放每列的 data-families 屬性,不放 cp-data JSON。 JSON 只在 CP 引擎開啟時存在,走 JSON 會讓品項篩選跟 CP 排序綁死。屬性版讓不做排序的清單品牌也能用同一套篩選——實測把品牌暫切成 cp_ranking: false build,8 個籤照常渲染、篩選照常運作。
  2. 籤上的數字是動態 facet 計數——按下去會剩幾筆有效券,為 0 的變灰不可按。否決建置期靜態計數:籤上寫 271、畫面剩 94 的過時數字會誤導。動態版讓「按下去變空白」在設計上不可能發生。
  3. 篩選狀態不寫入 localStorage。 篩選是當次意圖,回訪者看到一個已被篩過的頁面,會誤以為那就是全部。排序偏好照舊持久化。
  4. 不做權重偷算。 這就是我開過的那個折衷條件,Claude 否掉它的理由:現有排序結果本來就被評為「不盡人意」,把一個不被信任的算式從可見改成不可見,是把問題藏起來。而且篩到剩幾十筆之後,排序的重要性本就下降——等回饋從「不知道怎麼選」變成「選出來的順序不對」,那時才是排序問題。

另外把 per_item(單品均價)排序與品項下拉一併移除。失去「每顆蛋塔均價最低」這種排序,是已知且核准的取捨。

review 判 PASS 之後,文件還在描述滑桿

實作交給 Gemini 3.1 Pro。in-run review 由 qwen 跑(grok 當天不可用),第一輪就判 FAIL——render() 沒有在啟動時呼叫一次,籤上的數字要等使用者第一次互動才出現。實作者自我修正一輪後才 PASS;那個修正,是這一輪唯一的功能缺陷。qwen 這次驗得很紮實:headless Chrome 實測、CDP 量行動版 sticky 高度、APFS clone 跑 cp_ranking: false build。

PASS 之後 Claude 另外複驗(不引用 report 自述數字):語料核對 14 項全中;變異測試把 families() 改成兩種壞寫法(對不到就 throw、不去重),測試都轉紅,還原後 55/55 綠。測試數從 56 降為 55 是對的:移除了 2 個測「被移除機制」的案例、新增 1 個。

然後是這輪最後一個發現:Claude 再讀一次 diff,抓到三處實作者留下的過時文字,全部是這次改動造成的孤兒,全部不在驗收條件裡。content.config.ts 的 item_types 註解仍寫「driving the weight sliders…weight fixed at 1.0」——滑桿已經不存在了;cp_ranking 的註解仍寫「no weights」;而最重的一處,/about/ 的隱私權政策仍寫著:

您在網站上調整的排序或權重等偏好設定

一個公開頁面,在描述一個已不存在的功能。三處都在同一個 commit 修正。

自動 review 問的是「驗收條件有沒有達成」,達成就 PASS;它不會問「達成的方式有沒有讓別的東西變差」。這次的變體是文件腐爛——比型別退步更難被任何工具發現,因為沒有任何測試會去讀註解和隱私權政策。

先問分類,再問工具

回頭看,這句回饋最大的成本,是它一開始就被分錯了類。「資料好多,不知道怎麼選」是篩選問題,而網站手上最像「幫你選」的工具是排序——用排序工具回答篩選問題,做得再好也是 0 分,因為拉完滑桿,那 915 列一列都不會少。

所以現在留給自己的判準是三條。回饋進來,先問分類對不對,再問工具好不好。要讓「選了 A 又選了 B」這種 AND 語意成立,先確認每個籤本身已經是一次 OR,不然 62% 的組合按下去都是空白。移除一個功能的時候,grep 它的名字——含中文,不只是識別字——因為防腐爛這件事,沒有測試可以代勞。