廣告自動化的基礎建設問題有個固定形狀:一個每週只跑幾分鐘的任務,背後掛著四個隨時醒著的容器。Railway 上的 n8n 系統是這個形狀——n8n Main、n8n Worker、Redis、PostgreSQL 一起跑,一個月執行四次,每次不到 15 分鐘。每月費用大約 10 美元,99% 的時間在閒置。
費用本身不是主要問題。更核心的問題是:這套 n8n workflow 只服務一個帳戶,而且是刻意設計成這樣的。廣告主的 customer ID、ad group ID、Google Sheets document ID——全部硬編碼在 workflow JSON 裡。第二個民宿業者進來的時候,唯一的擴展路徑是複製整個 workflow,換掉裡面的 ID:獨立的 n8n 實例、獨立的 secret、獨立的維護面。第三個客戶就是第三份複製。
遷移同時解決兩個問題。
為什麼這個問題值得解決
OTA 對每筆訂單的抽成遠比外界想像的高。對恆春的小型民宿來說,把一部分訂單從平台拉回直客管道,差額是真實的利潤。但把 Google Ads 廣告管好需要的工作量——關鍵字分析、比對類型策略、持續清理爛字——不會隨著房間數縮小。六間房的業者和六十間房的連鎖飯店,面對的分析工作量是一樣的。在 AI 工具成熟之前,這類服務在這個規模的業者身上根本不存在:人工成本和業主能收到的價值不對稱。
n8n 是第一個答案,讓分析跑起來了。但它只服務一個帳戶,而且承接第二個客戶的成本是線性的——每多一個帳戶,就多一份要獨立維護的 workflow。
多帳戶怎麼設計進去的
每間民宿在新架構裡是一個 Account dataclass:customer ID、ad group ID、它在 Google Sheets 裡擁有的分頁名稱、email 主旨前綴,以及一個注入進 LLM 系統提示的 topic_facts 字串。帳戶在 analyzer 啟動時透過 env var 動態發現:ADS_<CLIENT>_CUSTOMER_ID 和 ADS_<CLIENT>_AD_GROUP_ID 存在就加入這個帳戶,不存在就略過。多一個客戶是加兩個 env var,不動程式碼。
topic_facts 欄位是這個架構在帳戶層面最重要的設計。其中一間客戶民宿有具體的禁忌清單:不能聲稱有海景或山景、不能強調大型泳池——這間民宿沒有這些,AI 在廣告文案裡的幻覺不是品質問題,是退款問題。這些約束以明文寫在 topic_facts 裡,每次分析都注入進系統提示。
Analyzer 以 for 迴圈跑過所有帳戶,每個帳戶獨立執行、獨立 exception 捕捉。一個帳戶出錯,其他的繼續跑。
人工關卡的設計
LLM 對每個帳戶回傳結構化 JSON:mvp(當前最佳關鍵字)、monitor(觀察名單)、add_suggestions、remove_suggestions。這份輸出被渲染成 HTML email,附上一個 TASK-XXXX 格式的任務 ID,和 executor workflow 的連結。
沒有任何操作會自動異動帳戶。Analyzer 只寫 Google Sheets、寄 email。異動帳戶的 executor.yml workflow 是獨立的,而且需要明確輸入——任務 ID 加上 action(add 或 remove)——才能觸發。Wayne 看完 email 之後,決定要執行哪個方向,打開 GitHub Actions,貼入任務 ID,手動觸發。Executor 從 Sheets 讀取對應的 task row,取出操作 payload,然後才真的動帳戶。
這替換掉的是 n8n 的 webhook 按鈕——點一下打到 Railway 上的 endpoint。新機制的 UX 退步剛好在那個地方:從一鍵變成幾個步驟。換來的是消除最後一個需要常駐的基礎建設依賴——webhook 按鈕需要一個隨時醒著的服務來接收和路由請求。workflow_dispatch 只需要 GitHub,本來就在那裡。
14 天保護的兩道防線
新進的關鍵字需要至少 14 天的曝光資料,才有足夠的依據做移除決策。LLM 系統提示裡明確寫了這條規則。但在資料壓力大的情況下——一批新關鍵字的早期數據同時看起來都很差——模型偶爾還是會建議在保護期內移除。
保護規則在兩個地方執行,不只靠提示文字。decisions.py 裡的 filter_protected_removals 從 inventory 取出每個被建議移除關鍵字的 date_added,計算距今天數,把保護期內的全部過濾掉,剩下才打包進 task row。日期格式不合解析失敗的,預設視為受保護。這個設計的邏輯是:把 LLM 當成唯一的執行點本身就是脆弱的,程式碼的過濾是獨立的一道防線。
Email 同時顯示兩份清單——LLM 建議了什麼,以及哪些被保護規則攔下來。被攔的建議從來不會走到執行階段,但業主看得到 AI 的判斷。
遷移過程找到的東西
n8n workflow 的 JSON export 裡,Google Ads developer token 以明文存在——和其他所有硬編碼常數放在一起。這是沒有程式碼的平台在序列化 workflow 成可匯出 JSON 時的結構性問題:匯出格式不區分設定和 secret,能存進 JSON 的東西就全部進去了。Python 版本接上線上帳戶之前,必須先換掉這個 token。
PostgreSQL 一開始在遷移計畫裡是保留的——Railway 上最便宜的元件,估計每月 2 美元,假設裡面存了重要的業務狀態。反向工程 n8n workflow JSON 之後才知道:Postgres 只被 LangChain 的 memoryPostgresChat 用到,是 AI 的跨次執行記憶,和業務邏輯完全脫鉤。每週的分析是獨立的,不依賴上一次跑的結果。Postgres 直接拔掉。狀態存在 Google Sheets,就是原本 n8n 一直在寫的那份試算表。
成本對比是具體的:Railway 四個容器每月約 10 美元,換成 GitHub Actions cron 之後費用是零——四次執行,每次不到 15 分鐘,完全在免費額度內。節省本身不是這個工作的目的,但它精確地衡量了一個從未設計成多帳戶的系統,累積了多少不必要的閒置基礎建設。
還沒做完的部分
Analyzer 和 executor 的主管線已搭好骨架,帶著明確的 TODO 標記;LLM 分析核心和各個 client builder(Anthropic、Google Ads、Google Sheets、Gmail)都能獨立運作。剩下的是在 analyzer pipeline 裡把它們串起來,然後等換過的 token 就位之後,對線上帳戶跑 smoke test。第二間民宿的 customer ID 和 ad group ID 還沒確認,topic_facts 裡放著一個佔位符,讓 LLM 在知道真實事實之前不會自由發揮。
第三個客戶進來的時候,改的是設定,不是程式碼。