週一早上打開廣告報表,發現上週的 ROAS 掉了一截,第一個念頭通常是哪支廣告壞了?接著就是一連串的猜測:是不是競爭對手在搶關鍵字、是不是素材看膩了、是不是網站又改版把什麼弄壞了,每一種猜測的處理方式都不一樣,猜錯了就會把預算砍在不該砍的地方。
麻煩的是這幾種狀況在報表上看起來都一樣,都是「成效變差」,要分辨它們得把點擊成本、點擊率、網站追蹤到的購買、後台的訂單一項一項對照著看,資料量一大,光是找出哪裡變了就要花掉一個早上。
今天要做的事是讓 SQL 先把哪裡變了整理成一張很短的表,再把這張表直接在 BigQuery 裡交給 Gemini 判斷原因,Day 05 產生合成資料時先寫好了一份答案表,在資料裡刻意藏了幾個狀況,其中三種在報表上看起來都像成效下跌,剛好拿來考 AI:
| 代號 | 藏在哪裡 | 做了什麼手腳 | 報表上會看到 | 真正的原因 |
|---|---|---|---|---|
| S1 | meta 重訓襪專案的開發新客廣告群組 meta-trn-prospecting |
8/12 起每次點擊的價格變成兩倍,點擊率和下單機率都不變 | 花費變多,訂單沒有跟著變多 | 競價變貴 |
| S2 | 全站,8/27 一整天 | 網站的購買事件整天沒送出,後台的訂單照常成立 | 網站追蹤到的購買歸零,後台訂單卻照常 | 追蹤碼壞掉 |
| S3 | meta 常態廣告的一支素材 cr-meta-evg-p1 |
從 6/19 起點擊率每週掉約 8% | 點擊一週比一週少,每週只差一點點 | 素材疲乏 |
答案表只用來對答案不會洩漏給 AI 看,另外 S7(9/1–9/16 秋日棉織專案):預期專案商品銷售佔比上升,這題無需 AI 分析,後續在 3.1 階段直接用 SQL 驗證即可。
今日核心目標:
AI.GENERATE_TEXT 讓 AI 逐列判讀原因,結果存成一張表整個流程分成四步,只有第三步會花到 Gemini 的錢:
| 步驟 | 做什麼 | 產出 |
|---|---|---|
| 找異常 | 用 SQL 把最近的數字和之前比,超過門檻才留下來 | diag_summary,今天是 4 列 |
| 寫題目 | 把每一列的數字寫成一段白話說明,附上候選原因 | diag_prompt 檢視表 |
| 問 AI | 透過遠端模型呼叫 Gemini,每一列問一次 | mart_diagnosis,每列都有原因、證據、信心分數 |
| 驗答案 | 檢查回答格式是否完整,再和答案表比對 | 檢查報表 |
💡 核心工程理念:
找異常用了三個角度,每個角度抓的東西不一樣:
| 角度 | 比較方式 | 門檻 |
|---|---|---|
| 廣告群組每週 | 這週和前四週比點擊成本與點擊率 | 點擊成本變動 30% 以上(漲跌都算),或點擊率掉 20% 以上 |
| 素材每週 | 這週和素材在資料裡最早 14 天的點擊率比 | 點擊率掉 25% 以上 |
| 全站每天 | 網站追蹤到的購買和後台訂單,各自和前七天平均比 | 任一項掉到一半以下 |
素材要和自己剛上線時比而不是和上週比,因為疲乏是一週掉一點慢慢累積,每週只差幾個百分點,和上週比永遠不會超過門檻。
一開始我也想在廣告群組層級直接算 ROAS,但每個群組每週追蹤到的購買只有 2 到 13 筆,多一筆少一筆 ROAS 就差很多,實際算出來在 0.1 到 2.5 之間跳動沒辦法拿來比。
點擊成本和點擊率就穩定多了,每個群組每週有數百到一千多次點擊撐著,所以群組和素材改看這兩個數字,購買有沒有追蹤到則拉到全站每天來對帳。
廣告群組和素材只留第一次超過門檻的那一週,全站則是每一天各自判斷,跑完的結果只有 4 列:
| 對象 | 期間 | 發現 |
|---|---|---|
| 廣告群組 meta-trn-prospecting | 8/10 那週 | 點擊成本 7.55 元變 13.42 元(+78%),點擊率 2.28% 到 2.39% 幾乎沒變 |
| 全站 | 8/27 | 網站追蹤到的購買 0 筆(前七天平均 43.9 筆),後台訂單照常 37 筆 |
| 素材 cr-meta-evg-p1 | 7/20 那週 | 點擊率從最早 14 天的 2.32% 掉到 1.6%(−31%) |
| 廣告群組 meta-evg-prospecting | 9/14 那週 | 點擊率掉 21%,這週只有 3 天資料 |
前三列正好是 S1、S2、S3,第四列是 S3 那支素材所在的群組,推測是素材越來越差把整個群組的點擊率也拉下來了,沒有多抓到不相干的東西。
另外補上 Day 08 留下來的 S7 秋日專案,這題用 SQL 就算得出來不需要 AI:專案期間(9/1 到 9/16)專案商品的件數占比從前三週的 53.8% 升到 66.6%,營收占比從 47.7% 升到 56.8%。
BigQuery 要呼叫 Gemini 得先建一個「遠端模型」,它其實只是一個指向 Gemini 的捷徑,Day 03 用 Terraform 建好的連線 vertex_ai_conn 這時派上用場:
CREATE OR REPLACE MODEL martech_dw.gemini_flash_lite
REMOTE WITH CONNECTION `us.vertex_ai_conn`
OPTIONS (ENDPOINT = 'gemini-3.5-flash-lite');
建模型本身不收費,之後在 SQL 裡用 AI.GENERATE_TEXT 把題目交給它,下面是節錄,{...} 的完整內容在 diagnose.sql:
SELECT anomaly_id, result, statistics
FROM AI.GENERATE_TEXT(
MODEL martech_dw.gemini_flash_lite,
(SELECT anomaly_id, prompt FROM martech_dw.diag_prompt),
STRUCT('''{"generation_config": {...}}''' AS model_params));
有三個規定要注意:
prompt,其他欄位會原封不動跟著帶出來,方便對回是哪一筆異常result,Token 用量放在 statistics,status 空白只代表 Gemini 有回應,不代表回答是完整的model_params 裡用 response_schema 把回答鎖成四個欄位:原因、證據、信心分數、下一步要查什麼,其中原因只能是六個選項之一每一題長這樣,只放數字和候選原因,沒有告訴 AI「點擊成本變高就是競價變貴」這種判斷規則,這樣才測得出它自己會不會判斷:
對象:廣告群組 meta-trn-prospecting(通路 meta)
期間:2026-08-10 到 2026-08-16,這週有資料 7 天,對照前四週
- 每天平均點擊:246.7 次(前四週 230.1 次)
- 點擊率:2.39%(前四週 2.28%)
- 平均每次點擊花費:13.42 元(前四週 7.55 元),變化 +78%
- 這週花費:23169 元
- 同通路的網站追蹤 ROAS:0.62(前四週 0.56)
請判斷最可能的原因,只能從這六個選一個:競價變貴、追蹤碼失效、素材疲乏、需求或季節變化、其他、資料不足
完整的題目模板在儲存庫的 diagnosis/prompt.sql,參數說明放在同一個目錄的 README.md。
同一份摘要交給兩個模型,一個是最便宜的 gemini-3.5-flash-lite,一個是 gemini-3.6-flash:
| 對象 | 答案表 | flash-lite | 3.6-flash |
|---|---|---|---|
| meta-trn-prospecting 8/10 | 競價變貴 | 競價變貴(0.90) | 競價變貴(0.90) |
| 全站 8/27 | 追蹤碼失效 | 追蹤碼失效(0.95) | 追蹤碼失效(0.95) |
| cr-meta-evg-p1 7/20 | 素材疲乏 | 素材疲乏(0.85) | 素材疲乏(0.85) |
| meta-evg-prospecting 9/14 | 素材所在的群組(我的判讀) | 資料不足(0.90) | 素材疲乏(0.80) |
三個植入的狀況兩個模型的答案都和答案表一致,給的理由也引用了對的數字,例如 8/27 那一筆 flash-lite 寫的是「網站追蹤到的購買為 0 筆但後台實際成立了 37 筆訂單,且廣告點擊數 1386 次與前七天平均相當」,3.6-flash 寫的是「網站追蹤到的購買降至 0 筆(前七天平均 43.9 筆),但後台仍有 37 筆實際成立訂單」,這正是分辨追蹤碼壞掉和真的沒生意的關鍵。
差別出在第四筆,flash-lite 認為只有 3 天資料、花費也少,選了資料不足,3.6-flash 則根據點擊率從 1.34% 掉到 1.06% 而點擊成本只差 2% 選了素材疲乏,但這一題是廣告群組的數字,裡面沒有任何一支素材的資料,所以 3.6-flash 的答案帶有推測成分,flash-lite 選資料不足反而比較守題目的規則。
這個結果要保留兩個前提來看:一是合成資料的訊號是刻意植入的,比真實世界乾淨很多,二是每個模型只跑了一次,AI 的回答每次可能略有不同,拿到真實資料上用之前要多跑幾次、多看幾個案例。
為了看 AI 在資料不足時的反應,我把同樣四筆異常只留下老闆最常看的那幾個數字,例如廣告群組只給花費和 ROAS,素材只給這週的曝光和點擊,全站只給追蹤到的購買,拿掉點擊成本、點擊率、素材上線初期的比較和後台訂單,再分兩組問 flash-lite,A 組只能五選一,B 組多了「資料不足」這個選項,也多了「數字不夠就不要硬猜」的規則:
| 對象 | A 組:五選一 | B 組:可選資料不足+不要硬猜 |
|---|---|---|
| meta-trn-prospecting | 其他(0.50) | 資料不足(0.90) |
| meta-evg-prospecting | 其他(0.50) | 資料不足(0.90) |
| cr-meta-evg-p1 | 素材疲乏(0.85) | 資料不足(0.95) |
| 全站 8/27 | 追蹤碼失效(0.98) | 追蹤碼失效(0.90) |
A 組最值得注意的是素材那一筆,題目只有這週的曝光和點擊,沒有可以比較的基期,看不出點擊率有沒有下降,它的理由卻寫了「點擊率與互動狀況下降」,這是自己補上去的,答案雖然對,但這次猜對不代表下次也會猜對。
B 組有了出口,三筆都老實說資料不夠,但全站那一筆兩組都選了追蹤碼失效,只憑購買歸零就下結論,沒看到後台訂單其實分不出是追蹤壞掉還是網站整個掛掉沒人下單,這也提醒我們 3.3 裡 S2 答對,不一定代表它真的有拿後台訂單來比,所以摘要一定要同時放追蹤數字和後台數字,給 AI 足夠的對照組比什麼都重要。
ENDPOINT 只寫模型名稱時 BigQuery 會送到非 global 的端點,單價比 global 高一成,這裡用的是非 global 的單價(發表時誤用 global 單價,9/29 更正)AI.COUNT_TOKENS 數題目長度,4 題共 1,178 個 Token,實際計費的輸入是 1,779 個,每題多出約 150 個,是規定回答格式的那段 response_schema,這段設定也會算錢,run.sh 會先印出兩個模型各跑一輪的最壞情況費用,要輸入 yes 才會真的呼叫 Geminimartech_dw 裡有 fct_ad_daily、fct_events、fct_orders
us.vertex_ai_conn 存在,而且連線的服務帳號有 Vertex AI User 角色gcloud auth list,帳號前面要有星號cd ~/ai-driven-martech-pipeline && git pull && bash diagnosis/run.sh
畫面印出預估費用後輸入 yes,看到「16 項通過、0 項不通過」與兩段診斷報表就完成了。
cd ~/ai-driven-martech-pipeline
git pull
ls diagnosis
會看到 summary.sql、prompt.sql、cost.sql、diagnose.sql、check.sql、report.sql、experiment.sql、promo.sql、run.sh 與說明文件 README.md。
bq query --nouse_legacy_sql < diagnosis/summary.sql
bq query --nouse_legacy_sql < diagnosis/prompt.sql
bq query --nouse_legacy_sql --format=pretty "SELECT anomaly_id FROM martech_dw.diag_prompt"
最後一行會列出 4 筆異常,想調整門檻就改 summary.sql WHERE 條件裡的 0.30、-0.20、-0.25、0.5 再跑一次。
bq query --nouse_legacy_sql --format=pretty < diagnosis/cost.sql
worst_case_usd 是每一題都把輸出上限用滿的最壞情況,今天兩個模型合計估出 US$0.0162,實際花了約 US$0.0053,大約是三分之一。
bq query --nouse_legacy_sql < diagnosis/diagnose.sql
這一步會用 flash-lite 和 3.6-flash 各跑一輪,結果寫進 mart_diagnosis,約一分鐘內完成。
bq query --nouse_legacy_sql --format=pretty --max_rows=100 < diagnosis/check.sql
sed -n '/-- Day 09 ①/,/;/p' diagnosis/report.sql | bq query --nouse_legacy_sql --format=pretty
sed -n '/-- Day 09 ②/,/;/p' diagnosis/report.sql | bq query --nouse_legacy_sql --format=pretty
檢查項目包括兩個模型都有跑、每一列都有回答、API 沒有回報錯誤、JSON 解析得出來、原因在六個選項內、信心分數在 0 到 1 之間、理由不是空白、沒有用到思考 Token,ok 欄位全部是 OK 就代表回答可以直接使用,後面兩行分別印出兩個模型的對照表,以及 flash-lite 的理由與下一步建議。
diag_summary 共 4 列,對應 S1、S2、S3 與 S3 所在的廣告群組check.sql 16 項全部通過bq rm -f -t martech_dw.diag_prompt
bq rm -f -t martech_dw.diag_summary
bq rm -f -m martech_dw.gemini_flash_lite
bq rm -f -m martech_dw.gemini_flash
# 有跑 3.4 小實驗(experiment.sql)才需要這一行
bq rm -f -t martech_dw.diag_experiment
mart_diagnosis 在 Day 21 讓 AI 查資料庫時會用到,建議保留,真的不需要時再用 bq rm -f -t martech_dw.mart_diagnosis 刪除,這幾張表都很小,遠端模型只是一個捷徑,放著不會產生費用。
gemini-3.6-flash 預設會先想一遍再回答,想的部分也算在 max_output_tokens 裡,第一次跑時每題光想就用掉約 488 個 Token,真正的答案只寫出 5 到 10 個就被切斷,但 status 還是顯示成功,解法是把 thinking_budget 設成 0,另外 thinking_level 目前會被 BigQuery 的參數檢查擋下check.sql 會確認 JSON 解析得出來、原因在選項內、信心分數有值AI.GENERATE_TEXT 每執行一次就呼叫一次 Gemini,結果先存成表再拿來查,不要每次看報表都重跑今天先用 SQL 把 52 萬筆事件壓成 4 列異常摘要,再透過遠端模型在 BigQuery 裡請 Gemini 逐列判讀,在這份合成資料上,三個植入的狀況 flash-lite 和 3.6-flash 的答案都和答案表一致,理由也引用了對的數字,4 題的正式診斷用 flash-lite 跑一輪約 US$0.0017,約新台幣 0.05 元。
回頭看前言的問題,ROAS 掉了不一定是廣告不好,這次的三種狀況一個是競價變貴、一個是追蹤碼壞掉、一個是素材看膩了,處理方式完全不同,AI 能幫忙分辨的前提是 SQL 先把對的數字擺在它面前,資料給不夠它就會開始猜,所以摘要要設計好,也要留一個讓它說「資料不足」的出口。
不過今天還有一件事需要提醒,3.3 提到拿到真實資料上之前要多跑幾次,但每多跑一次就多付一次錢,今天只有 4 題、跑一輪不到 0.35 新台幣,真實的廣告帳戶一天可能有幾十筆異常要判讀,同一批異常為了確認答案穩不穩還要各跑好幾次,費用就會跟著題數和次數一起往上乘。
仔細看今天正式診斷的題目會發現,每一題真正不一樣的只有中間那幾行數字,前面的角色說明、後面的六個候選原因和四條規則每一題都一模一樣,另外每次呼叫還會多出約 150 個 Token 的回答格式設定,這些內容每次都重新計費,今天的題目很短,重複的部分還不多,但之後只要附上商品清單、過去幾個月的成效這類背景資料,每一題都要重送一大段一樣的內容,這才是明天要解決的痛點。
明日預告:Day 10《同一份資料問 AI 十次,不用付十次錢的快取機制》,我們將把每次都一樣的說明與背景資料集中放到題目最前面,先存進 Gemini 的快取,之後每一題只送出會變的部分,實測 Context Caching 能把重複的輸入費用省下多少,以及資料要多長才划得來!