CVS Radar:超商商品評價雷達¶
摘要¶
超商新品好不好吃,答案散在 PTT CVS 板的上千篇心得和推文裡。CVS Radar 把它整理成站在貨架前就用得上的推薦依據:搜尋、比較、看清楚大家為什麼推或不推,再一鍵回到原文。
站上收錄約 799 項商品,來自近一年、976 篇貼文與 13,985 則留言(資料基準 2026-08-11)。從爬蟲、語意判讀、統計評分到前端網站都是自己蓋的,由每日排程重算後發佈,線上一直跑著。
這篇除了介紹成品,也記錄開發過程中幾次讓我改掉做法的轉折——包括為什麼最後選擇讓四分之一的商品不顯示分數,以及哪些問題是一開始完全沒料到、後來被現實逼著換做法的。
線上體驗: https://cvs-radar.vercel.app/ · GitHub Pages 鏡像: https://yuhsunwang.github.io/cvs-radar/ · 原始碼: https://github.com/YuHsunWang/cvs-radar
目前狀態
爬取、LLM 語意標記、評分管線、Next.js 網站、CI 與每日自動更新都已上線。全家 417 項、7-11 322 項、萊爾富 54 項,其餘為其他通路。
產品長什麼樣¶
預設以「近期推薦」呈現最近值得買的商品,也能切換「討論熱度」看現在大家在聊什麼。收合的卡片就給齊判斷需要的東西:綜合評分、共識標籤、聲量、上架日期、價格,以及一句代表性的留言。
介面是為了「人在超商、單手操作、十秒內決定」設計的。展開任一張卡,才會出現比較完整的判斷材料:作者的完整心得、留言整理出的「大家喜歡的點」與「需要留意的點」,最後是原文連結——分數說服不了你的時候,可以自己去看。
要解決的問題¶
超商新品的討論散在不同文章與推文裡,而每一種讀法都有它的問題:
- 單看一篇心得,會被作者的口味帶著走。
- 直接翻推文,得自己排除反諷、離題回覆和重複附和。
- 兩三則極端評價,就能讓一個商品看起來比實際好很多或差很多。
- 一個分數如果沒有附上樣本數、共識和原文,其實不知道該不該信。
所以問題被拆成兩層:後端先把雜訊整理成商品層級的公平分數、信心度與證據;前端再把這些輸出轉成能快速比較的購買介面。
一篇心得,怎麼變成購買參考¶
- 收集公開心得——讀取 PTT CVS 板的商品文與留言。
- 整理成同一商品——辨識通路、品名與價格,把名稱寫法不同、實際是同一項的商品合併起來。
- 保留真正的使用感受——作者心得維持原句;留言逐則判斷正向、中立、負向,排除離題、純附和與只談其他通路的內容。同一篇講到多項商品時,排除鄰近商品的敘述。
- 彙整共識與時效——同一個人不會因為重複留言被放大;樣本不足就不給分。「近期推薦」兼顧評價與新鮮度,「討論熱度」回答現在大家在聊什麼。
- 回到證據查證——卡片先給結論,展開後仍能讀到作者評價、代表留言與原文連結。
值得一提的是使用者瀏覽時,網站其實什麼都沒有在算。所有語意判讀與統計都在發佈前的批次管線跑完,瀏覽器只讀一份靜態 JSON;載入之後的搜尋、篩選與排序全在本地完成,沒有後端。
開發歷程¶
第一次改寫:詞典看得懂字,看不懂語氣¶
最早的情緒判讀是規則式的:一份中文情感詞典,加上 PTT 特有的「推/噓」標籤當先驗——有人按推,就先給一點正分。
這在多數情況下堪用,但踩到的坑很有代表性:
- 詞典裡「香」是正向詞,於是「香精味有夠重」被判成好評。
- 「推」標籤的正分先驗,套在炎上文底下的酸推上會全面失準——「詐騙壽司推」「超爛推」「難喝推」全都被算成正向。
換句話說,規則能看懂字,看不懂語氣。反諷、比較式否定、省略主詞的抱怨,這些正好是 PTT 留言最常見的形式。
解法是把逐則情緒判讀交給 LLM。但這帶來新問題:LLM 的輸出不穩定、要花錢,而且每次重算結果都可能不一樣——一個每天重跑的管線不能是這樣。
所以做法是把 LLM 的判斷結果快取進版本控制:每一則留言以「內容指紋」(文章網址、推噓標籤、正規化後的文字,加上 prompt 版本一起做雜湊)當 key,判斷結果存成 CSV 進 repo。相同輸入不會再呼叫模型一次,也一定產生完全相同的分數。CI 因此跑得動,成本可控,改動也可以前後比對。
回頭把歷史留言整批重標之後,影響比我預期大得多:全目錄 93% 的商品分數有變動,581 項下降、145 項上升,平均掉 10 分。我抽驗了掉最多的幾項逐則核對,方向幾乎全對——那些分數本來就不該那麼高。目前快取裡有 15,419 則情緒判斷,另外還有商品名、心得摘句與代表留言三層標記。
常見誤解/釐清
誤解: 加了 LLM,專案就變成「不可重現的黑盒子」。 釐清: 不可重現的是「每次都即時呼叫模型」這個做法,不是 LLM 本身。把判斷結果以內容指紋快取進版控之後,管線對相同輸入是確定性的。 實際後果: 驗證方式因此可以是「改動前後各跑一次完整管線、比對整份結果集」,而不是只看單元測試綠燈。
第二次改寫:三個人的意見,不該長得像大家的共識¶
情緒判準了之後,下一個問題是怎麼把一堆「正/中/負」變成一個分數。單純平均會出事:只有三個人講過的商品,平均下來可能是 95 分,看起來比一百個人討論、平均 82 分的商品還好。
這裡用的是貝氏收斂。講白話一點:
先假設每個新商品都是「普通」(50 分),再讓實際評價把它往上或往下拉。講的人越多,拉得越用力;只有兩三個人講,就拉不太動,分數會停在中間附近。
這不是保守,而是誠實——樣本少的時候,我們是真的不知道。多數人的直覺是「資料越少越要小心解讀」,貝氏做的只是把這件事寫進公式,而不是留給讀者自己判斷。
同一條線上還有三個決定,都是為了同一件事:讓分數反映「多少人真的這樣覺得」,而不是「誰講得比較大聲」。
- 每人每商品只算一票。 同一個帳號在同一串連續留言,不會線性放大影響。
- 排除作者自推,也排除離題與純附和的留言。
- 舊討論逐漸淡出。 權重會隨時間衰減,半衰期約 139 天。三年前的評價不該和上個月的評價一樣重——尤其超商商品的配方和供應商本來就會換。
而最花時間討論的,是要不要在資料不夠時直接不給分。
有個具體案例:改版前有 575 項商品被標成「聲量充足」,我去看了一下,其中 457 項其實只有一篇貼文——只是那篇底下留言很多。但同一串底下的留言會互相影響,從眾效應很明顯,不能當成獨立的討論。所以規則改成至少要兩篇不同貼文才算「充足」,信心度也跟著調整。
最後的版本是:樣本太少的商品,不顯示綜合評分,也不顯示正/中/負百分比,只明確標示「資料不足」。目前有 194 項商品(24.3%)因此留白。
在產品上留白比湊出一個好看的排行難——留白會被質疑「你的資料是不是不夠」,而一個看起來很精確、其實只有三個人講過的數字不會。但那個數字會讓人買錯東西。
第三個取捨:降權,還是點名?¶
管線裡有一層帳號可信度分析:品牌偏好是不是異常集中、貼文是不是樣板化、評價是不是極端、有沒有短時間爆量。這些訊號合起來是一個「可疑分」。
這個分數有兩種用法。一種是做成公開標籤——「CVS Radar 標出 N 個疑似業配帳號」,示範性最強。另一種是只讓它在計分時降低權重,對外什麼都不說。
我選了後者,因為公開標籤要求的證據強度,是內部降權的好幾倍。降權判斷錯了,是某個商品的分數偏了一兩分;點名判斷錯了,是一個真實帳號被掛上洗評價的標籤,而代價完全由他承擔。同樣的弱訊號,撐得起降權,撐不起點名。
這條界線往外延伸成整套隱私設計:公開資料只有商品層級欄位,不輸出帳號檔案或個別帳號分數;原始貼文(含 PTT 帳號)永遠不進 repo,由測試守著。原文連結留給使用者自己查證,但這裡不會生出一份公開的個人評價檔案。
最後一段路:讓它每天自己動¶
一個「跑過一次的分析」和「每天自己更新的產品」,中間的距離比想像中遠。
現在的樣子是:排程每天抓新貼文、跑 LLM 標記、重算分數、產出前端資料,然後 commit 回 main 觸發部署。但這條路上的東西都會壞,所以配套是後來一件一件補上的:
- 排程剛寫好的那幾天其實一次都沒成功跑過——一個是環境變數少了一段路徑找不到執行檔,一個是時區算錯、排在半夜而不是早上。所以每次成功都會留下時間戳記,另有每小時檢查一次的監測,太久沒動就發通知給我。
- 資料新鮮度訂成明確的門檻:發布出去的資料最多 14 天。檢查讀的是資料快照的時間,不是網站建置的時間——建置每天都會成功,那不能證明資料有更新。
- 這支檢查刻意做成在哪裡都能跑,不綁管線主機。主機整台掛掉的時候,它還是測得出來。
這些沒有一項是「功能」,但少了它們,網站會在某個沒人注意的時候安靜地停在三週前。
還有幾個是事後才知道的¶
上面三段是我主動改掉的設計,下面這四件不是——是原本的假設在某個時間點突然不成立,只好換做法。
本來想讓網站每天自己更新,最後還是得靠我自己的電腦。 每天的更新裡有一段工作是「請 AI 逐則讀留言」。我用的 AI 工具是訂閱制的,要用我的帳號登入才能使用,而每天自動執行的雲端環境登不了我的帳號。另一個選項是改用按次計費的 API,程式裡也留著這個開關,但那要把付款用的金鑰放進一個公開的專案裡,而且每天都要付錢。所以最後的安排是:更新排在我自己的電腦上執行,雲端只留一個手動按鈕當備用。備用那條路跑出來的資料會缺少 AI 判讀的那一層,所以我在文件裡特別註明它不算正式更新。
備份漏想了一件事:出事的時候,備份也剛好不在。 爬回來的原始資料含有 PTT 帳號,不能放進公開的專案,所以我把它存在雲端服務的暫存空間。後來才知道這個空間有規則:七天沒被使用就自動清除。問題在於,它會連續七天沒被使用,通常正是因為每日更新壞掉了——也就是我最需要這份資料的時候,它剛好過期消失。現在改成每週備份一份到自己的硬碟,另外寫了重建的工具,以及一份「資料不見了怎麼辦」的操作手冊。
我電腦上的版本,永遠是舊的。 每天的自動更新會把結果寫回專案,所以只要我沒有主動同步,手上那份就是落後的。有一次我在查某個商品的分數為什麼偏低,查了半天找出兩個問題並修好,後來才發現那兩個問題早就被修過了——我只是在一份舊檔案上又重做了一次。現在的規矩是:任何跟資料或分數有關的調查,動手前先把最新版本抓下來,一律以線上那份為準。
請 AI 幫忙標註,要先確認它真的有在標註。 有一批留言需要一則一則判斷情緒,我把這份工作交給 AI 代理去跑。它交回來的檔案格式完全正確,但仔細看會發現它其實是寫了一套關鍵字規則去分類,並沒有真的逐則讀過——那批結果全部作廢。之後改成:要求它一邊判斷一邊寫檔,讓我看得到進度;另外產出一份「為什麼這樣判」的說明檔,我檢查這些理由有沒有整段重複套用,再挑出反諷、比較句這類容易判錯的句子抽驗,確認過才收進資料裡。
這個專案想證明的事¶
- 讓 LLM 參與,但不讓結果變成不可重現。 四個語意層都由 LLM 判讀,結果全部以內容指紋快取進版控。
- 統計上不逞強。 貝氏收斂、單人設限、時間衰減,加上樣本不足就不給分。
- 端到端自己蓋完,而且真的在跑。 從爬蟲、標記管線、評分到靜態前端,資料由每日排程重算後發佈,不是一次性的 demo。
- 驗收方式要對得上想驗的東西。 管線對相同輸入是確定性的,所以改動的驗證方式是前後各跑一次完整管線、比對結果集。
專案採 PRD 先行:先定義產品情境、資料契約、評分決策、隱私界線與驗收方式,再分模組實作。開發過程使用 Claude Code 與 Codex 輔助,但需求定義、架構、演算法取捨、程式審查與最終驗證由我負責。
技術棧¶
| 層 | 技術 |
|---|---|
| 資料收集 | Python、Requests、Beautiful Soup |
| 語意標記 | LLM 判讀四層(情緒、商品名、心得摘句、代表留言),各以內容指紋快取 |
| 評分 | 規則式情緒、SnowNLP adapter、貝氏收斂、時間衰減 |
| 服務層 | 框架無關的查詢 API,附 FastAPI adapter |
| 前端 | Next.js 15、React 19、TypeScript、Tailwind CSS、Lucide |
| 品質 | Pytest、Vitest、Ruff、TypeScript/Next.js production build |
| 部署 | Vercel(主要)+ GitHub Actions/GitHub Pages 靜態鏡像 |
驗證¶
三百多個自動化測試涵蓋解析邊界情況、商品正規化、情緒歸屬、單人設限、共識分布、作者摘句抽取、分數校準,以及前端的搜尋、分類與品牌篩選、日期邊界、四種排序模式與靜態匯出。GitHub CI 分開跑前後端檢查,production build 會同時驗證 TypeScript、Next.js 靜態匯出與 GitHub Pages base path。
已知限制與後續¶
- 語意判讀仍可能誤讀反諷、迷因與省略主詞的比較句,因此保留人工覆寫與原文查證。
- 商品分群依賴正規化後的名稱,遇到罕見命名變體仍可能拆開或誤合。
- 公開站台是預先計算的快照,不是即時串流。
- 後續:更大的人工標註 gold set、校準過的模型比較、更多合法公開來源。
延伸閱讀(本站)¶
- Bayesian MCMC 瀏覽偏好模型 — 同樣以貝氏方法建模行為資料的專案
- 常見 ML Case Study — 文本分類與模型評估的解題骨架
CVS Radar 是獨立作品集專案,與 PTT 或任何便利商店品牌無關。公開內容來自使用者生成資料,應視為購買決策輔助,而非客觀品質認證。
來源¶
- CVS Radar repository
- 站上數據為
web/public/data.json公開快照,資料基準 2026-08-11





