我合作的民宿老闆不是行銷人員。他們是蓋了幾間客房、學會替陌生人做早餐的人,然後某天發現如果要有訂房就得跑 Google Ads,於是就跑了。廣告繼續投,但他們其實不太清楚哪些關鍵字有效,只是停下來感覺更危險。
這就是我做這套系統想解決的問題。
我幫台灣兩間小型家庭民宿管理 Google Ads——那種沒有 IT 部門、沒有行銷團隊、沒有人知道什麼是品質分數的地方。以前每週的流程是:拉關鍵字報表、盯著數字看、靠直覺調整。大概有效,但「大概有效」的意思是我變成了瓶頸。我一忙起來,關鍵字就沒人審,哪個關鍵字在燒錢也沒人發現,直到月底帳單來才知道。
所以我做了 bnb-ads-manager。核心想法很簡單:自動化資料收集,丟給 Claude 做推理,但真正的決定還是由人來下。
每週流程
系統每週做這幾件事:
- 從 Google Ads API 拉關鍵字和活動績效
- 從 Google Sheets 讀取庫存資訊(房型、定價、空房備註)
- 連同業務背景一起送給 Claude
- Claude 回傳結構化 JSON 建議
- 系統發一封 Gmail 通知,附上審核連結
- 人類確認(或修改)
- 執行確認後的變更,呼叫
adGroupCriteria:mutate
第 6 步是我最在意的部分。在這套系統存在之前,關鍵字的決策靠感覺,或者根本沒人在做決策。現在每週都有一份報告:哪個關鍵字在賺、哪個在燒、哪個繼續觀察——而且每個建議都有具體理由。民宿老闆不會自己執行變更(那是我的工作,確認之後才動),但他們可以真正看懂發生了什麼事。從「我相信 Wayne」到「我理解這在做什麼」,對一個小型業者來說這個轉變很重要。
「人在迴圈中」這個說法很常被當成裝飾,我希望它在這個系統裡是真實存在的關卡。
Claude 實際看到什麼
Prompt 裡不只有績效數字。我還帶入住宿類型、地點、典型客群、季節性備註。大概長這樣:
Business context:
- Property: Mountain cabin, sleeps 6, near hiking trails
- Location: Nantou, Taiwan
- Seasonality: High season Oct-Feb (cool weather hikers), low season Jun-Aug
- Current inventory: 3 rooms available next 2 weeks
Keyword performance (last 30 days):
[...CSV data...]
Return JSON in this format:
{
"mvp": {"keyword": "...", "stats": "..."},
"monitor": {"keyword": "...", "stats": "..."},
"add_suggestions": [{"keyword": "...", "reason": "..."}],
"remove_suggestions": [{"keywordText": "...", "adGroupId": "...", "criterionId": "...", "reason": "..."}]
}
mvp 欄位是跑了幾週後加上去的,記錄當前最有價值的那一個關鍵字,讓客戶清楚看到預算花在哪裡最有效。在這個欄位出現之前,報告就是一堆建議異動的清單,沒有「好消息先說」的錨點,讀起來很混亂。monitor 則是還在新人保護期(加入未滿 14 天)的關鍵字,LLM 會說明狀態而不會建議刪除。真正的異動建議放在 add_suggestions(新增)和 remove_suggestions(刪除)裡。
14 天保護規則
早期遇到一個問題:這週建議移除某個關鍵字,暫停之後,下週又建議加回來。來回震盪,很煩,對品質分數可能也有影響。
根本原因是剛加入的關鍵字資料不夠。跑三天的曝光量根本說明不了什麼。根據這點資料建議移除是雜訊,不是訊號——對預算有限的民宿老闆來說,根據雜訊做決策就是在浪費錢。
我加了一條簡單規則:14 天內有異動過的關鍵字,不碰。
from datetime import date
def is_protected(row: dict) -> bool:
date_added = row.get("Date_Added", "")
if not date_added:
return False
try:
added = date.fromisoformat(date_added)
return (date.today() - added).days <= 14
except ValueError:
return False
不算複雜,但有效止住了來回震盪。Claude 的輸出裡還是會看到這些關鍵字,但執行層會直接跳過它們。
可替換的 LLM 提供商
我不想永遠鎖在 Anthropic,所以用 Python 的 Protocol 做了一層薄薄的抽象:
from typing import Any, Protocol
class LLMClient(Protocol):
def analyze(self, system_prompt: str, user_payload: dict[str, Any]) -> dict[str, Any]:
...
def build_llm_client(cfg: Config) -> LLMClient:
if cfg.llm_provider == "anthropic":
return AnthropicLLMClient(cfg.anthropic_api_key, cfg.llm_model)
if cfg.llm_provider == "openai":
return OpenAILLMClient(cfg.openai_api_key, cfg.llm_model)
raise RuntimeError(f"unknown LLM_PROVIDER: {cfg.llm_provider}")
實際上到目前為止只用過 Claude(當時是 Opus)。但有這個 Protocol 介面,之後只要改 LLM_PROVIDER 環境變數就能換掉整個 LLM,不用動其他程式碼。
意外的收穫
給了業務背景之後,Claude 的季節性推理能力蠻讓我驚訝的。沒有脈絡的話,它就是看 CTR 和 CPC 做通用建議。有了背景之後,它會說出這種話:「這個『十月登山』關鍵字現在流量低,但依照季節規律再過六週會到高峰——建議觀察而非移除。」
這種判斷是民宿老闆自己做不到的——不是因為不聰明,而是沒有時間把關鍵字資料、自己的訂房紀錄、還有當地的登山季節行事曆放在一起比對。把這些脈絡餵給 Claude,彌補的就是這段距離。
這不是什麼魔法,就是對我提供的文字做模式比對,但它幫我避開了幾次短視的決定,更重要的是,它讓客戶每週有一份真的能讀懂的報告——不是看不懂的數字 dashboard,而是一段說「這個關鍵字值回票價、那個不值」的白話文。
Google Sheets 當稽核紀錄這個設計也選對了。客戶可以打開試算表,看 AI 建議了什麼、實際執行了什麼,不用學新的介面,也不用另外登入。他們本來就活在 Sheets 裡。
如果重來的話,我會多留時間在 prompt 工程上。讓 Claude 穩定回傳結構化 JSON——尤其在資料稀疏或有點奇怪的時候——需要多輪驗證和 prompt 調整,花的時間比我預期的多。