恆春民宿業者跑 Google Ads,面對的是一個結構性問題,不只是預算問題。OTA 的搜尋優勢來自三件事疊加:多年帳戶信用換來的 Quality Score、佣金遠比外界想像高——高到足以補貼業者跟不上的廣告預算——加上全職 PPC 團隊一年五十二週在跑實驗。一個民宿業者自己管廣告帳戶,對面是這些。
問題不是「專業的關鍵字管理有沒有幫助」——有。問題是「沒有全職行銷人員,這件事能不能做」。
之前跑的是什麼
原本有一套 n8n 自動化已經找到方向。每週把 Google Ads 的關鍵字資料拉下來,送進 LLM 用民宿專屬的脈絡分析,把結果整理成業者看得懂的報告,附加新增和移除建議,業者點郵件裡的按鈕批准就執行。
跑這套系統需要四個 Railway 容器:n8n Main、Worker、Redis、Postgres,全天候待命。兩次週跑之間,CPU 使用率接近零。每月費用約 $10 美元。這個數字絕對值不大,但它是固定的基礎建設支出,在業者的帳戶沒有任何動作的時候也在計費。這種費用結構,遲早會有人問「它值這些錢嗎」——問久了,工具就被關掉了。
為什麼要搬
搬到 Python + GitHub Actions,主要目的不是省 $10 美元。目的是讓這個工具兩年後還在跑。
GitHub Actions 免費額度完全夠一週一次的 cron job,有很大的餘裕。LLM API 一個月四次分析,費用在美分等級。零常駐容器代表零閒置費用。工具可以無限期存在,不需要每個月重新回答「它值不值得繼續付錢」。
Python 在這個情境下比 TypeScript 更合適。google-ads Python SDK 對 GAQL 的支援比 TypeScript 版本成熟,proto-plus enum 解析有乾淨的處理路徑。更關鍵的是,整個 Ads + Sheets + Gmail 的 OAuth 表面現在共用一個 refresh token——一次授權流程,一個 secret 管理。n8n 版本的每個 API 各自有一套憑證,透過 n8n 的 Credentials UI 管理。
兩個 workflow,一個人工關卡
analyzer.yml 每週一台北時間上午九點觸發,也可以手動觸發。對每個設定好的帳戶,它用 GAQL 從 Google Ads API 拉下啟用中的關鍵字,讀 Google Sheets 上的關鍵字庫存歷史,比對兩者找出自上次跑以來新增、移除、或重新出現的項目,把合併後的資料送給 Claude Opus 用帳戶對應的系統提示詞分析。模型回傳一個 JSON:mvp(本週最佳關鍵字與理由)、monitor(觀察名單)、add_suggestions(建議新增的關鍵字)、remove_suggestions(建議下架的候選詞)。結果寫成一筆 task row 進 Sheets,同時寄出 HTML 分析郵件。
executor.yml 只能手動觸發。業者讀完郵件,到 GitHub Actions UI 填入兩個欄位:從郵件複製的 task_id,和要執行的 action(add 或 remove)。Task row 裡的新增操作和移除操作存在不同的 key——業者可以這週批准新增、下個月再決定移除,不需要一次全批。
沒有手動觸發,帳戶不會有任何動作。這個設計對謹慎的客戶很重要:分析和執行之間有一個人工審查窗口,不會混在一起。
14 天保護規則為什麼要在兩個地方
每個系統提示詞裡有一條新人保護規則:Date_Added 距今不滿 14 天的關鍵字,不論表現多差,都不得進入移除建議。新上架的關鍵字還沒有時間積累資料——四天後 CTR 是零,這是雜訊,不是訊號。
問題是系統提示詞在資料壓力下會漂移。當關鍵字清單夠長、JSON payload 夠密,模型可能在前 11 筆記得規則,到第 12 筆時忘了。這類失敗難以在測試中發現:不會拋錯,只是輸出一個看起來正確、實際上有一筆問題的結果。
所以 decisions.py 在 LLM 回傳輸出之後,重新把同一條規則跑一遍,用 Python 硬過濾。filter_protected_removals() 逐筆檢查模型的移除建議,用 criterion_id 對照庫存裡的 date_added,把 14 天內的全部擋掉。date_added 格式錯誤或缺失,一律視為受保護——寧可保守,不能誤殺。
被擋下來的建議不會靜默消失。郵件裡有一個獨立區塊列出「模型建議移除、但系統擋下的關鍵字」。業者看到的是完整的過程,不只是過濾後的輸出。
這個雙重設計——規則寫在提示詞、同一條規則又在程式碼裡強制執行——對應的是對失敗模式的判斷。提示詞失效,程式碼接住。兩層都沒接住,業者還要手動觸發 executor workflow 才能執行。三道關卡,對應一個難以輕易還原的操作。
每個帳戶有自己的禁忌知識
每個 B&B 帳戶在 config.py 裡有一個 topic_facts 欄位,在 runtime 注入系統提示詞。其中一家的設定明確列出:不許主張大型泳池、不許提海景或山景;強調戲水池(安全、水淺)、BBQ 烤肉設施、KTV 房間、以及走路到超市和夜市的距離。這些是事實和禁忌,不是文風指引。
為什麼需要這些禁忌?沒有這層知識的分析提示詞,模型可能從搜尋詞資料推導出「這家業者應該主打泳池假期關鍵字」——但那個戲水池跟旅客想像的泳池是完全不同的體驗。業者按這個方向去跑廣告,廣告承諾和到達現場的落差會傷害 Quality Score。topic_facts 的作用是讓模型在下判斷之前就知道這個物件實際是什麼、不是什麼。
這個欄位是 per-account 的常數,不在提示詞模板裡。同一個分析迴圈、同一段程式碼,在 build time 注入不同的脈絡知識。新增帳戶只需要一個 config entry 和確認帳戶 ID,不需要動程式碼。
現在能做什麼
四個 Railway 容器退役。基礎建設費用從每月約 $10 美元降到 Claude API 四次呼叫的成本——一個不會影響到工具能不能繼續存在的數字。
目前兩個帳戶,由 analyzer.py 裡的 config loop 驅動,擴展帳戶不需要改程式碼。n8n 前身為單一帳戶硬編碼,加第二個帳戶是一次基礎建設工程。
每週自動分析、14 天保護規則在程式碼裡強制執行、每個 B&B 的禁忌知識注入提示詞、人工批准才能執行——這套流程跑在幾乎零成本的基礎建設上。OTA 的結構性優勢不會消失,但一個關鍵字衛生做得紮實、投放有針對性的帳戶,能縮短部分差距。這是這個系統想解決的問題,也是基礎建設成本必須低到「不需要人去想它」的原因。