Dashboard

📐 分析方法 · Methods

這頁解釋 seo_etherwan 各份報表背後用了什麼資料、用了什麼演算法、為什麼這樣選。 目前涵蓋兩塊:Link Equity(連結權重 / Tier 分級 / 建議連入連出) 與 LLM Edit-brief(Gemini 3 Pro 改稿 brief 的 prompt / pipeline / 輸入輸出契約)。 切換下方 tab。

1. 一句話定位

把站內每一頁當作節點、把站內所有內鏈當作邊、把 Google Search Console 90 天曝光當作流量證據; 三者疊合後回答一個問題:「下一個內鏈該補在哪、為什麼。」

Link Equity 不告訴你「該寫什麼新文章」,只告訴你「現有頁面之間該怎麼連結傳遞權重」。 新文章選題是另一份報表(content-gap)的事。

2. 為什麼有這份報表

傳統 SEO 工具看「孤兒清單」(沒有 inbound link 的頁),但「沒人連到」≠「值得補連結」。 孤兒清單裡實際混雜三種根本不同的問題:

  • 真高需求 + 站內冷宮:客戶在 Google 找得到(有 imp),站內卻沒導流 → 必須立刻補
  • 真冷門 + 真孤兒:沒人找、站內也沒導 → 應該砍或合併,不是補連結
  • 假孤兒:URL 改名後其他文章還引用舊 slug → 不是補連結,是修 alias

只看 inbound 數字無法分辨這三類。把流量證據(GSC)疊上來,才能把優先序對齊到真實業務價值。

3. Tier 分級邏輯

規則式(rule-based),不是 ML score。閾值從本站 187/191 個 page_meta_all 內 GSC 命中頁的曝光分佈拍定(p87 / p64 / p20), 未來資料量改變時可在 data/producer/lib/link_equity.py 調 T_HIGH / T_MID / T_LOW

A 急救站內冷宮、外部熱
inbound = 0 AND imp_90d ≥ 5,000
站內 0 條 inbound,但 Google 90 天曝光超過 5000 — 客戶在 SERP 看得到、站內卻沒幫它導流,最浪費權重的狀況。
B 助攻位置 6-15 + 中曝光
avg_pos ∈ [6, 15] AND imp_90d ≥ 500
距離前 5 名很近,補幾條內鏈或重寫 title/description 很可能推進。投報率最高的批次。
C 已有但弱連結密度不足
inbound ∈ [1, 2] AND imp_90d ≥ 500
已被站內看到、但連結密度不夠分散權重。補多 1-2 條 inbound 即可。
D 真冷沒人找 + 站內無連
imp_90d < 50 AND inbound = 0
90 天曝光不到 50 + 完全沒站內導流。大概率是過時、競爭不過、或主題太細。考慮砍或合併到主題頁。
X 健康不需要動
其他
連結密度與流量已經平衡。不在優化清單裡。
Tier 是排他的 + 有優先序:A > B > C > D > X。一頁同時符合多個條件時取最高 Tier,避免重複出現。

4. 建議連入 / 建議連出 演算法(slug-token-overlap-v1)

每篇文章「建議連入 top 5」+「建議連出 top 5」兩欄背後的邏輯。 純本地計算,不打 API、不用 LLM、不用 embedding。 程式碼:data/producer/lib/link_suggestions.py

4.1 公式(兩個方向對稱)

# 對每篇文章 X(要產生建議的對象): X_tokens = X.slug.split("-_") # 拆 lowercase alphanum tokens if len(X_tokens) < 3: skip # slug 太短沒辨識度 # 對每個其他文章 Y(候選 counterpart): overlap = |X_tokens ∩ Y_tokens| if overlap < 3: skip if 兩者間已存在連結(按方向判讀): skip overlap_ratio = overlap / max(|X_tokens|, |Y_tokens|) impr_factor = min(1.0, log10(Y.imp_90d + 1) / 6) score = 0.7 × overlap_ratio + 0.3 × impr_factor # 取 top 5 by score 寫入 article_link_suggestions 表, # direction 欄為 'inbound' 或 'outbound'

4.2 兩個方向的解讀

direction對 X 的意義UI 顯示誰的 imp 進公式
inbound Y 應該連到 X(Y 把權重灌給 X) X 的 article-brief「建議連入」欄 Y.imp(source 越熱越好)
outbound X 應該連去 Y(X 推薦讀者去 Y) X 的 article-brief「建議連出」欄 Y.imp(target 越熱越值得推薦)

公式對兩個方向完全對稱:兩種情況下 Y.imp 都是「希望對方越熱越好」。 不一樣的只是「兩者是否已連結」要用對方向判讀(inbound 看 Y → X 是否存在;outbound 看 X → Y 是否存在)。

4.3 範例(eth-0145)

eth-0145(slug = understanding-the-difference-between-layer-2-and-layer-3-switches-explained):

建議連入 top 5 — 該被誰連到本頁: what-difference-between-layer-2-managed-switches-and-layer-3-managed-switches(共享 6 token)等。 編輯動作:去那些 source 文章正文加 anchor 連到 eth-0145。

建議連出 top 5 — 本頁該連去哪些文章: eth-0102 (Layer 2 vs 3 FAQ) / eth-0134 (Managed vs Unmanaged) / eth-0119 (Configuring Layer 3 Routing) 等。編輯動作:在 eth-0145 原文加 anchor 推薦這些延伸閱讀。

4.4 為什麼這個 weighting

  • 0.7 overlap_ratio:主導因素 — 兩篇是不是「在講同一件事」
  • 0.3 imp_factor:對方曝光大、推薦/被推薦的價值才高;但不應該蓋過主題相關性,所以權重小
  • log10 ÷ 6:把曝光從 0 到 100 萬 normalize 到 [0, 1],避免少數超高流量頁吃掉所有 score

4.5 為什麼兩個方向都要跑

圖論上「X 連到 Y」就等於「Y 被 X 連入」— 同一條邊。但編輯成本不同

  • 補 X 的 inbound:要打開 N 篇別的文章,每篇找段落、加 anchor — 跨多篇、改別人
  • 補 X 的 outbound:打開 X 一篇,補 5 條 outbound 一次處理完 — 改自己、單篇 batch

所以實務上「持續部署建議連出」是更可持續的工作流: 編輯每天/週打開幾篇 → 看「建議連出 top 5」→ 採納 1–2 條 → 下一篇。 每一條 outbound 採納,自動就是某頁的 inbound +1,全站連結圖隨時間累積。

5. 為什麼不用 Gemini / LLM

2026-04-28 V1 ship 時的決策(記在 link_suggestions.py 開頭 + article-internal-link-suggestions-exploration openspec change):

因素純本地(V1)Gemini / LLM
成本 0 716 條建議 × API 費用
速度 producer 一次跑 ~10 秒 慢 + rate limit
可解釋 「共享 4 個 token + 來源頁 imp 12k」可審計 「LLM 說 relevant」黑盒
Producer 自動化 隨 build_report 自動跑 需 LLM job 流程 + retry
準確度 字面相似(slug-level) semantic-level 更準
V1 刻意選最簡單的方案 ship 完看效果,再決定是否升級。「先有,再準。」

6. 已知限制(誠實說)

  • 語意盲點:「VLAN 設定」跟「網路分段方法」對的 SEO 連結,但 slug token 沒重疊 → 演算法找不到
  • 依賴 slug 品質:如果 slug 是 eth-0145 這類沒語意的 ID → 演算法廢
  • 沒考慮主題深度:兩篇都有 switch token 不代表是同主題(一篇講硬體、一篇講配置)
  • 沒考慮已有 anchor 重複:如果 source 已用 anchor "Layer 3" 連去某篇,再建議它連去 eth-0145 的 anchor 也是 "Layer 3" 會冗餘

7. 升級路徑

下面三條路按複雜度遞增。料齊但沒做 — 先看 V1 在實際使用上多不準,再決定升哪條。

V2 — Embedding (cosine similarity)

你已有 dashboard-report-seo-embedding-1 容器(Python FastAPI + pgvector + GPU)。 把 191 篇文章內容跑 embedding 灌進 pgvector,演算法改成「target embedding 跟所有 source embedding 算 cosine sim」取 top 5。 解語意盲點,仍是純本地(不連雲端)。

  • 準度:明顯升 — 抓得到「VLAN 設定 ↔ 網路分段方法」這類 slug 不像但內容像的
  • 成本:embed 一次 ~$0(GPU 本地跑);之後 build_report 是 lookup,無 inference
  • 新增依賴:embedding 服務必須 up;migration 加 vector 欄

V3 — LLM rerank (Gemini)

Embedding 找 top 30 → Gemini 讀文章開頭/headings 對 30 條 rerank 取 top 5。準度最高但慢 + 花錢。

  • 準度:最高 — Gemini 能判讀「這篇是初學者向、那篇是進階」
  • 成本:每篇 ~$0.001 × 191 篇 ≈ $0.2 per build_report
  • 建議只跑 Tier A/B(最值錢的 ~90 篇),不跑全站

V4 — cluster_id 主題分群

探索 change article-internal-link-suggestions-exploration 的 D2 阻塞決策:cluster_id 0/186 沒填。 要先跑分群(k-means on embeddings 或 LLM 標籤),再用 cluster 為 candidate 過濾。 搭 V2 或 V3 都更準,但工程量大。

8. 5-Stage Pipeline

整份 link equity 報表是這條 pipeline 的最終輸出。一輪 build_report 跑完所有 stage:

┌───────────────────────────────────────────────────────────┐
│  Step 0  Data discovery (人工 + 一次性)                   │
│  讀 page_meta_all.json + GSC _p.csv                       │
│  → 確認 schema / 拍 Tier 閾值 / 列補爬候選 / 確認 prefix  │
└────────────────┬──────────────────────────────────────────┘
                 │
       ┌─────────┴─────────┐
       ▼                   ▼
┌──────────────┐    ┌────────────────────────┐
│ Step 1       │    │ Step 3 (並行)          │
│ Path Alias   │    │ Openspec 文件化        │
│ 修復 URL     │    │ proposal/design/tasks  │
│ rename       │    │ 本介紹頁就是這層延伸   │
└──────┬───────┘    └────────────────────────┘
       │
       ▼
┌──────────────────────────────────┐
│ Step 2  GSC-driven 補爬 ✓        │
│ 高曝光缺頁 → crawler seed → 重爬│
│ Phase 1 完成: 28 頁,產品頁/     │
│ about/applications/support       │
│ (page_meta_all 191 → 219)        │
└──────┬───────────────────────────┘
       │
       ▼
┌──────────────────────────────────┐
│ Step 4  Alias 量產 (Phase 2) ✓   │
│ scripts/audit_orphan_targets.py  │
│ 12 候選 → 5 條 high-confidence   │
│ 自動加 path_aliases.json,剩餘   │
│ 8 條 jaccard 0.75(不同產品分類,│
│ 非 rename)defer                 │
└──────┬───────────────────────────┘
       │
       ▼
┌──────────────────────────────────┐
│ Step 5  Link Equity Report 實作  │
│ producer S9 + Go handler + UI    │
│ 包含本頁 + /links-report 主表   │
└──────────────────────────────────┘
    

9. 資料源

data/crawl/etherwan.com/page_meta_all.json
所有節點(222 條,去 dup 後 219,2026-04-29 補爬後)。每筆含 url / category / slug / h1 / content_links / 等 17 個欄位。原 Crawler 產出 191 article 頁;scripts/crawl_supplement.py(cloudscraper + bs4)2026-04-29 補爬 28 個高曝光產品 / about / applications / support 頁。後續補爬批次也會用同腳本。
data/seo_etherwan/internal_links.json
所有邊(target → [{source_page, anchor_text, ...}])。Producer 跑 build_internal_links.py 從 page_meta_all 反轉產出,套用 path_aliases + path_filters 後得 246 target paths / 1145 edges。
data/seo_etherwan/gsc/202601-202603/*_p.csv
3 個月 page-level GSC:page, clicks, impressions, ctr, position。共 9,490 rows / 3,609 unique paths。Robert 從 GSC export 手動更新,每月一次。
data/seo_etherwan/path_aliases.json
URL rename 修復表。命中者代表「站內仍引用舊 slug、應視為連到新 slug」。第 1 條已 ship(eth-0145);Phase 2 量產 ~80 條。
data/seo_etherwan/path_filters.json
永久排除 prefix(cdn-cgi / print/pdf / sites/default/files / node / us / tw / jp)。命中者既不收進連結圖也不收進報表。
reports.db.articles + article_link_suggestions + link_equity_pages
SQLite。articles 是 advisor brief 的子集(186 條,page_meta_all 的子集);article_link_suggestions 存 V1 演算法建議(716 條);link_equity_pages 存本報表結果(191 行 × Tier)。

10. 哪些頁不在報表(為什麼)

path_filters.json 過濾的 prefix。理由:報表的單元應該是「能 take action 的頁」。 不能優化的列進來只會是噪音。

Prefix原因怎麼處理
/sites/default/files/PDF 文件 (95k 90d imp)無法加 internal link / 改內容做 SEO,源頭排除
/us/ /tw/ /jp/locale 變體 (1939 頁 / 843k imp)不獨立列出,但 GSC 流量已合併到 root(root 行的 imp 是合併數)
/cdn-cgi/Cloudflare 基礎設施不是真實頁面(44 outbound refs 全部來自 email-protection link)
/print/pdf/Drupal print plugin (125 GSC pages)跟原頁同內容,不重複收
/node/<id>Drupal raw ID URL (21 頁)網站 alias 沒做好的殘餘,本期統一過濾

11. FAQ

Q:為什麼 eth-0145 的 imp_90d 是 375k 不是 GSC 上看到的 329k?
A:報表把 /us/ /tw/ /jp/ 三個 locale 變體的 imp 合併到 global root。 eth-0145 root 自己 ~329k + tw 19k + jp 28k + us 0 = ~376k。 理由:locale 是同一篇文章的多語版本(hreflang 規範下),分開看反而扭曲主題流量。
Q:為什麼孤兒清單只有 104 篇不是 105 篇?昨天我看到 105。
A:因為 alias 修復生效了 — eth-0145 過去 inbound = 0(其他文章引用舊 slug 沒被 attribute 給它); path_aliases.json 加了第一條 mapping 後,那 2 條 inbound 正確 attribute 給新 slug,於是它從孤兒變「有 inbound 的頁」。
Q:建議連入 top 5 怎麼跟 alias 互動?
A:不互動。建議連入演算法是看 slug token overlap,本身不依賴連結圖;alias 只影響 inbound count 的計算。 所以「演算法說該被連入但 inbound = 0」可能是真的還沒人連、也可能是 alias 還沒補。可同時看 article-brief 的 outbound 動線確認。
Q:為什麼有的頁 has_gsc_match = N?
A:4/191 篇沒命中 GSC。原因可能:(1) 頁太新還沒被 Google 索引(lastmod 看一下); (2) 像 /support/faq/generic 那種佔位頁本來就沒人搜; (3) URL 在 GSC 紀錄是別的 path,需要查 GSC raw 比對。
Q:報表多久更新一次?
A:跑一次 python -m data.producer.build_report 更新一次。本機開發即跑即看;prod 透過 Git+CI/CD 部署 + scp reports.db。目前不是 cron 自動。
Q:為什麼用 SQLite 不用 Postgres?
A:Producer 產出的 reports.db 走檔案、易 scp、prod 沒 Postgres。 Postgres 留給 LLM job pipeline(dashboard-report-seo-postgres-1),那邊才需要併發寫。
← 回 Link Equity 報表

1. 一句話定位

把 article-brief 的「現況分析」(GSC 指標 / AI-friendliness 子分數 / rule-based 建議 / 原文片段)打包成 prompt, 丟給 Gemini 3 Pro 產出一份 markdown 格式的「edit brief」— 4-6 個按 impact × effort 排序的 Change,每個 Change 明示「改哪裡 / Current 怎樣 / New 怎樣」。

LLM 不取代人,只壓縮人工分析時間。每個 brief 仍要編輯人工 review (accept_brief) 才會進客戶看的 article-brief 頁。模型瞎掰風險靠 prompt 規則 + rule-based baseline 雙重 dampening。

2. Pipeline 全圖

從一篇文章被選中到「客戶在 article-brief 頁看到 LLM 建議」的完整路徑:

┌──────────────────────────────────────────────────────────────────┐
│ Step 1  選文 (CLI flags)                                          │
│   --top-n-impressions / --pool / --article-ids / --only-unmatched │
│   → 從 static/data/briefs/*.json 篩出 N 篇 candidate              │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Step 2  Build prompt (per article)                                │
│   prompts/edit_brief_v1_2.py::build(brief)                        │
│   ├─ SYSTEM (固定 ~95 行) ──────────────► 角色 / 規則 / 骨架 / 自檢 │
│   └─ USER (動態 ~7000 字) ─────────────► page_data_lite + rules + │
│                                          原文截斷 + 結尾指令      │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Step 3  Call Gemini API                                           │
│   client_gemini.py + key from keys/gemini_api_key.json            │
│   model = gemini-3.1-pro-preview (default) / 3.1-flash-lite       │
│   → markdown 字串(# Edit Brief …)                               │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Step 4  Persist (3 個地方)                                        │
│   ├─ 檔:data/seo_etherwan/content-drafts/auto-generated/.md │
│   ├─ Postgres: llm_generation_items (job audit + content_md)     │
│   └─ JSONL audit: data/seo_etherwan/auto-brief-runs/<run_id>/    │
└────────────────┬─────────────────────────────────────────────────┘
                 │  ← 人工 review gate
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Step 5  accept_brief.py (rename only, no delete)                  │
│   auto-generated/.md  →  group-a-edits/.md  (promoted) │
│   auto-generated/.md  →  retired/<ts>-.md  (歸檔)      │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Step 6  build_report.py S7                                        │
│   掃 group-a-edits/*.md → match article_id → upsert advisor_drafts│
│   sqlite: reports.db.advisor_drafts (draft_type='group-a-edit')  │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Step 7  Go API serve                                              │
│   GET /api/article/:project/:aid → JOIN advisor_drafts.content_md │
│   → suggestions[].content                                         │
│   GET /api/article/:project/_index → has_llm_draft / has_advisor  │
│        _edit flags 給 sidebar 圖示 + filter 用                    │
└────────────────┬─────────────────────────────────────────────────┘
                 │
                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ Step 8  Frontend render                                           │
│   article-brief.js 拉 brief JSON → renderBrief() 把 LLM markdown │
│   渲染進「LLM 建議」section + Current/New diff 兩欄顯示          │
└──────────────────────────────────────────────────────────────────┘
    

3. CLI 入口 — generate_briefs.py

Source: data/producer/generate_briefs.pyDefault 是 --dry-run,沒打 --confirm 永遠不會打 API。

3.1 文章選法(必須擇一)

Flag用法適合場景
--article-id eth-XXXX 單篇 針對特定文章一次性產 brief
--article-ids id1,id2,id3 逗號清單 編輯給定的一批 article-id
--pool hot|winner|cold|... 按既有分桶 批量處理某一桶(producer S7 wave)
--top-n-impressions N 按 GSC 90d impressions 取前 N 高曝光優先 — ROI 最高

可疊加 --only-unmatched 跳過已有 LLM brief 的文章(避免重複跑)。

3.2 模型 / prompt / 成本控制

FlagDefault說明
--modelgemini-2.5-flash實際 production 都改用 gemini-3.1-pro-preview
--prompt-versionv1.2v1.0 / v1.1 / v1.2,見「版本歷史」section
--max-cost-usd1.00單次 run 總成本上限。Pro 跑 20 篇要設 5.00 否則會在 ~5 篇後 stop
--max-output-tokens8192worst-case 估算用;實際 output 通常只 ~1100 tok / 篇

3.3 執行模式

  • --dry-run (default):印出完整 prompt(截斷顯示)+ 估算 token 數 + 估算 cost。不打 API、不寫 DB、不寫檔。
  • --confirm:真打 API。需 google-genai SDK + keys/gemini_api_key.json + Postgres 連線(job_id 記到 llm_generation_items)。
  • --no-db:跳過 Postgres,只寫 .md 檔(debug 用)。
  • --show-full-prompt:dry-run 時印完整 SYSTEM+USER;省略時 USER 截斷顯示。

3.4 典型批次指令

.venv/bin/python -m data.producer.generate_briefs \
  --top-n-impressions 20 \
  --only-unmatched \
  --model gemini-3.1-pro-preview \
  --prompt-version v1.2 \
  --max-cost-usd 5.00 \
  --confirm \
  -v

4. 輸入:USER prompt 怎麼組裝

edit_brief_v1_2.build(brief, numbering) 接一份 brief JSON(從 static/data/briefs/<article_id>.json 讀),輸出 (SYSTEM, USER) 兩段字串給 Gemini。 USER 段組三塊:頁面資料 JSON + rule-based 建議 + 原文截斷 2000 字

4.1 page_data_lite — 從完整 brief 抽哪幾欄

不是把整份 brief JSON 都送進 prompt(會太貴 + 塞滿 context window)。實際只送這幾欄:

欄位來源為什麼送
article_id, url, path, h1, title_tag, meta_descriptionbrief.json 頂層定位用 — LLM 才知道「在改哪一頁」
category, pool, word_countbrief.json 頂層內容類型語氣判斷
metrics.{gsc_impressions_90d, gsc_avg_position, ctr_actual, aio_brand_primary, target_keyword, ...}brief.metrics讓 LLM 知道「這頁排名 / 競爭狀態 / AIO 品牌」
ai_friendliness.{total, signals.{definition / comparison_table / faq / h2_entity_coverage / ...}}brief.ai_friendliness各子分數 + evidence 字串 — LLM 才知道哪個訊號弱要補
headings, outbound_linksbrief.headings / outbound_links結構訊息 — LLM 才能說「在 ## Applications 之前插入」
how_to_readbrief.how_to_read該頁的 reader profile — 讓 LLM 對齊讀者

4.2 rule_suggestions — Producer S6 的 baseline

在 LLM 跑之前,build_report.py S6 stage 已經依規則跑出一批 rule-based 建議 (low-AIO / low-CTR / no-FAQ / position-near-top-10 等),存進 article_suggestions 表。 組 prompt 時用 _rule_bullets() 把這些 baseline 列成 markdown bullets,丟給 LLM 當參考:

- [P1] **Add comparison table for managed vs unmanaged**(30 min)
  - why: ai_friendliness.comparison_table = 0
  - how: 在 ## Applications 之前插入 markdown table

LLM 被允許「採用、擴充、批判替換」這些 baseline,但不可完全忽略。 理由:rule-based 抓得到 deterministic signal(schema gap / AIO miss),LLM 補的是 nuance。

4.3 content_md_truncated — 為什麼截 2000 字

  • 大多數 EtherWAN 頁是 500-3000 字,2000 字含蓋 ~80% 文章 + 截斷標記 …(已截斷,實際長度 N 字)
  • 留 token budget 給 SYSTEM (~95 行) + page_data JSON (~3000 字) + headings/outbound (~500 字) — 加總約 7000 字 USER + SYSTEM ≈ 9000 字
  • Gemini 3 Pro context window 1M tok 綽綽有餘,但每篇 ~$0.02 actual cost 對於 Y 篇批次需要控制
  • 實測 LLM 看 2000 字頭 + headings 已能寫出可用 brief;超過 2000 字邊際效益遞減

5. SYSTEM prompt 全文(v1.2)

Source: data/producer/lib/llm/prompts/edit_brief_v1_2.py。 ~95 行,每篇文章都會送同樣這段(fixed prefix,給 cache 命中)。

你是 EtherWAN 的 SEO 改寫顧問,幫 B2B 工業網路設備廠商產出可執行的 edit brief markdown。

**輸出規則(不可違反)**

- Output 第一行必須是 `# Edit Brief <編號>: <target_path>`;不要開場白、不要「好的」「以下是」。
- 每份 brief 含 4-6 個 Changes,依 impact × effort 排序;最省力最高效的先。
- 每個 Change 必須有下列四個欄位:
    **Location:** 明示改哪裡(section anchor / 段落編號 / H2 標題之前或之後)
    <段落解釋> 30-80 字正文,直接寫;不要用 `{描述…}` 或 `<…>` 這類 meta placeholder
    **Current:** <貼出現況的段落或元素;新增段落寫「(新段落,建議插入 XX 處)」>
    **New:** <貼出建議替換後的完整 markdown 片段>
- 禁止瞎掰產品規格、溫度範圍、認證編號。若資料缺,寫「需 RD 確認具體規格」。

**輸出骨架**

```
# Edit Brief <編號>: <target_path>

**Target keyword:** <主查詢字>
**Current AIO:** <aio_brand_primary,若空寫 "none">
**EtherWAN rank:** <gsc_avg_position 四捨五入 #N>
**Problem:** <一句話點出為什麼這頁需要改,不超過 40 字>

---

## Change 1: <具體動作,如「改寫 H1」/「新增 FAQ 段」/「補內部連結」>

**Location:** <改哪裡 — 例:「H1」/「`## Applications` 之前」/「第一段後」/「頁末 meta description tag」>

<30-80 字正文直接寫,解釋為什麼這改動能解 Problem 的某個面向。>

**Current:**
```
<現況段落、元素、或字串>
```

**New:**
```
<建議替換後的完整文字或 markdown 片段>
```

## Change 2: <...>

## Change 3: <...>
...
```

**質量要求**

1. **EtherWAN 工業語氣**:客戶是 engineering procurement / network engineer,不是消費者。避免 `revolutionary` / `cutting-edge` / `best-in-class` / `game-changing`。
2. **具體可執行**:不要「增加內容」「補充資訊」「優化 H1」;必須指名段落位置、具體字串、schema type、錨文字。
3. **術語白名單不翻譯**:PoE switch / managed switch / industrial / DIN-rail / IEC 61850-3 / EN 50155 / hardened / fanless — 英文原字形。
4. **rule-based 建議是 baseline**:可採用、擴充、批判後替換;不要完全複製。
5. 不要 wrap 整段 markdown 在 code fence 裡;Changes 區直接用 heading + markdown。

**不可犯的錯(output 之前自檢)**

- ❌ output 第一行 "好的" / "以下是" / "根據您的資料"
- ❌ 保留 `{具體動作}` / `<...>` meta placeholder 在輸出
- ❌ 「Change 1: 優化 H1」沒給 Location、沒給 current / new
- ❌ 瞎掰規格 ("operating temp -40°C") 若 page_data 無此資訊
- ❌ 用 "我建議" / "您可以考慮" 這種軟勸說;直接命令式
- ❌ 行銷花俏詞彙

6. USER 模板(v1.0 沿用)

Source: data/producer/lib/llm/prompts/edit_brief_v1_0.py::USER_TEMPLATE。 v1.1 / v1.2 改 SYSTEM 沒改 USER。紅色變數build() 動態填入的:

# Edit Brief {numbering}: {target_path}

# 頁面資料

```json
{page_data_json}
```

# 系統已有的 rule-based 建議(可參考、擴充或批判替換)

{rule_suggestions_bullets}

# 原文前 2000 字截斷(content.md 完整長度 {content_md_length} 字)

{content_md_truncated}

---

請輸出完整的 edit brief markdown。直接寫,無需開場白或解釋。

6.1 變數對照

變數填入內容大小
{numbering}固定 AUTO-0001(每篇都一樣,不影響輸出品質)9 chars
{target_path}brief.path 或 brief.url~50 chars
{page_data_json}page_data_lite 序列化(見 §4.1)~3000 chars
{rule_suggestions_bullets}_rule_bullets() 把 article_suggestions 表的 row 渲染成 markdown bullets~500 chars
{content_md_length}文章原始 word_count(資訊用,告訴 LLM 截斷比例)~5 chars
{content_md_truncated}原文前 2000 字 + 截斷標記≤ 2050 chars

7. 預期輸出

7.1 骨架(每份 brief 必有)

# Edit Brief AUTO-0001: /support/featured-articles/-slug

**Target keyword:** <主查詢字>
**Current AIO:** <品牌名 or "none">
**EtherWAN rank:** #N
**Problem:** <不超過 40 字一句話>

---

## Change 1: <動作>
**Location:** <改哪裡>

<30-80 字解釋 why>

**Current:**
```
<現況>
```

**New:**
```
<建議>
```

## Change 2: ...
## Change 3: ...
(4-6 個)

7.2 4 個必要欄位(每個 Change)

欄位用途什麼算合格
Location編輯打開檔案後該滑到哪「H1」/「`## Applications` 之前」/「第一段後」/「頁末 meta description tag」— 不可寫「在文章裡」
段落解釋為什麼這改動能解 Problem 的某個面向30-80 字、命令式、無 placeholder(不留 {…}<…>
Current原文現況片段貼真實字串;新增段落時寫「(新段落,建議插入 XX 處)」
New建議替換後完整 markdown編輯可直接 copy-paste 進原文的可執行字串

7.3 實際範例(從 eth-0129 跑出的真檔擷取)

實際輸出存在 data/seo_etherwan/content-drafts/group-a-edits/eth-0129.md。 開頭幾行:

# Edit Brief AUTO-0001: /support/featured-articles/implementing-quality-service-prioritizing-network-traffic

**Target keyword:** quality of service network traffic
**Current AIO:** none
**EtherWAN rank:** #11
**Problem:** 雖然有 11 名排名與 7000+ 月曝光,但無 FAQ / 比較表 / 定義段,
AI Search 引擎無法擷取,CTR 低於 1%。

---

## Change 1: 改寫 H1 加 entity + 主查詢字

**Location:** H1

H1 目前 `Implementing Quality of Service` 沒帶 brand / 設備類型 entity,
搜尋 "QoS switch industrial" 時無法被視為 strong topic match。

**Current:**
```
Implementing Quality of Service: Prioritizing Network Traffic
```

**New:**
```
Implementing QoS on Industrial Ethernet Switches: Prioritizing Network Traffic
```

## Change 2: ...

7.4 自檢清單(SYSTEM 強制)

  • ❌ 第一行不可是「好的」「以下是」
  • ❌ 不可保留 {具體動作}<...> 在 output
  • ❌ Change heading 沒給 Location / Current / New 任一
  • ❌ 瞎掰規格(operating temp / 認證編號)若 page_data 沒給
  • ❌ 行銷詞彙(revolutionary / best-in-class / cutting-edge)
  • ❌ 軟勸說語氣(「我建議」「您可以考慮」)

8. 人工 review · accept_brief.py

LLM 寫完不會直接上線。auto-generated/ 是 staging 區,編輯人工掃過再 promote 到 group-a-edits/, build_report 才會撿。理由:避免 LLM 幻覺直接出現在客戶看的頁面。

8.1 這支腳本做什麼

  • Copyauto-generated/<aid>.mdgroup-a-edits/<aid>.md
  • Retireauto-generated/<aid>.mdretired/<ts>-<aid>.md
  • group-a-edits/ 已有同名 .md:原檔 rename 為 .pre-<ts>.md 保留
  • 不刪檔(per CLAUDE.md File preservation rule)— 永遠是 rename,可隨時翻 retired/ 找回

8.2 Flag

Flag用法
--article-id eth-XXXX單篇 promote
--article-ids id1,id2逗號清單
--allauto-generated/ 內全部 .md 一次 promote
--dry-run只印 plan,不動檔
--force覆蓋 group-a-edits/ 已存在的同名檔(仍會 rename 舊檔)

9. build_report.py 整合(S7)

Producer 第 7 stage 掃 content-drafts/{auto-generated, group-a-edits}/,把每個 .md 檔 match 到 article_id,upsert 進 reports.db.advisor_drafts 表。

9.1 advisor_drafts schema

CREATE TABLE advisor_drafts (
  draft_id    INTEGER PRIMARY KEY,
  article_id  TEXT,
  draft_type  TEXT NOT NULL,    -- 'auto-generated' | 'group-a-edit'
  filename    TEXT,
  content_md  TEXT NOT NULL,    -- 完整 LLM 輸出 markdown
  accepted    INTEGER NOT NULL DEFAULT 0,
  created_at  TEXT NOT NULL
);

draft_type 區分兩階段:auto-generated(剛跑完還沒 accept)vs group-a-edit(已 accept,可上 prod)。Frontend 用這兩個欄位畫 ⚡ (LLM draft) 和 👤 (advisor edit) 兩種 sidebar 圖示。

9.2 已知 import 行為

build_report S7 的 import 邏輯目前只認得 article_briefs 已 match 的 article_id。 如果 LLM 跑的文章在 article_briefs 表沒有 brief row,drafts_imported 會列入 skipped_no_match。 已知 edge case:直接 INSERT advisor_drafts 補資料庫 row 是 workaround;root cause fix 待後續 session。

10. 頁面渲染

10.1 API 端

internal/service/reports_db.go::Article(article_id) 從 advisor_drafts 撈 content_md → inject 進 brief JSON 的 suggestions[0].content。同檔 linkSuggestionsFor()article_link_suggestions 表撈 inbound/outbound 建議 → 進 internal_link_flow.inbound_suggested

_index endpoint JOIN 一次 advisor_drafts 拿到每個 article_id 的 has_llm_draft(draft_type='auto-generated')和 has_advisor_edit(draft_type='group-a-edit')兩個 boolean — 給 sidebar 圖示 + 「⚡ 只看已有建議」filter 用。

10.2 Frontend

static/js/article-brief.js::renderBrief()suggestions[0].content 直接餵進 markdown-it render。Brief 還有「Current/New diff renderer」把 **Current:** ... **New:** ... pattern 自動切兩欄並排顯示,方便 reviewer 比對前後差異。

11. 模型 / 成本

11.1 已 register 的 Gemini 3 模型

ModelInput $/1M tokOutput $/1M tok適合
gemini-3.1-pro-preview~$2.50~$15.00旗艦品質、單次重要
gemini-3.1-flash-lite-preview$0.075$0.300批次最划算
gemini-3-flash-preview中間中間折衷

11.2 Dry-run 估算 vs 實際

--dry-run--max-output-tokens 8192(worst case)算成本。 實際 output 通常只 ~1100 tok / 篇(ratio ~0.13),所以實際是估算的 ~20%:

ModelDry-run 估 / 篇實際 / 篇20 篇實際總
gemini-3.1-pro-preview~$0.10~$0.02~$0.40
gemini-3.1-flash-lite-preview~$0.025~$0.005~$0.10
--max-cost-usd default $1.00 是 per-run cap,不是 per-article。Pro 跑 20 篇要設 5.00(按 dry-run worst case 估),否則 ~5 篇後會被 cap 擋。

11.3 撞過的兩個 bug(已修)

  • worker 不讀 job.model:subprocess 讀 args.model 而非 DB job_row 的 override,已改 main() 讀 job_row。
  • 3.1-pro 拒絕 thinking_budget=0:Pro 必須開 thinking。加 THINKING_REQUIRED 白名單,這幾個 model 不送 thinking_budget=0。

12. 版本歷史

所有 prompt source 在 data/producer/lib/llm/prompts/edit_brief_vX_Y.py--prompt-version CLI flag 切換。

版本主要變動還在用?
v1.0 初版。SYSTEM 7 條質量要求 + USER_TEMPLATE 三段組裝(page_data / rule baseline / 截斷原文)。 不建議。撞到 placeholder 字串外洩 bug
v1.1 修 v1.0 placeholder leakage(LLM 把 {描述為什麼這改動…} 字面照抄出來)。SYSTEM 範本 placeholder 改成 <角括號> + 顯式說明「不要保留 label」+ 補 good vs bad example。 過渡版
v1.2 Default。加 Location 欄位(每 Change 必須明示改哪裡,避免「在文章裡」這種模糊描述); 禁止任何 preamble(output 第一行直接 #); EtherWAN 工業語氣強化(明列禁用詞); 「不可犯的錯」自檢清單。 ✅ Production default

USER_TEMPLATE / build() / estimate_prompt_tokens 三個 v1.0 → v1.2 都沒動, v1.1 / v1.2 透過 from edit_brief_v1_0 import USER_TEMPLATE, _truncate, _rule_bullets 共用基礎建設。

13. FAQ

Q:為什麼 Pro 跑 20 篇實際只花 $0.17 不是 dry-run 估的 $4?
A:dry-run 用 --max-output-tokens 8192 算 worst case;實際 LLM 寫一份 brief 通常只 ~1100 tok。 所以實際是估算的 ~20%。但 --max-cost-usd 是按 dry-run worst case 估算扣,要把 cap 放寬否則跑到一半會 stop。
Q:LLM 會不會瞎掰產品規格?
A:SYSTEM 明禁。撞到時 fallback 寫「需 RD 確認具體規格」。但不能 100% 防— 所以 accept_brief 的人工 review gate 必走,且 page_data_lite 不送競品比較數據(讓 LLM 沒料可瞎掰)。
Q:為什麼 Pro 不是 Flash-lite?
A:Pro 寫的 brief「Location」精度更高、Change 排序更貼 impact × effort, 且 EtherWAN 工業語氣判斷比較準(Flash-lite 偶爾會冒出 marketing 詞彙)。 20 篇 ~$0.40 vs $0.10 — 差 4x 但絕對值都低,買 Pro 品質。
Q:能不能換成 Claude / GPT?
A:架構支援。data/producer/lib/llm/client_*.py 是 provider abstraction,已有 client_gemini.py,要加 client_anthropic.py 是同 interface。 但 prompt v1.2 是針對 Gemini 3 Pro 的 thinking-mode tuned,換 model 要重新 verify。
Q:跑完一篇 brief 大概要等多久?
A:Pro thinking-mode 約 25-40 秒/篇(含 thinking time);Flash-lite 約 5-10 秒。 20 篇 Pro 串連跑 ~10-15 分鐘。
Q:跑完的檔案在哪?
A:data/seo_etherwan/content-drafts/auto-generated/<article_id>.md。 Accept 後移到 group-a-edits/,原檔留在 retired/。 DB 並行寫進 reports.db.advisor_drafts(sqlite,prod 也有這份)+ llm_generation_items(postgres,job audit / cost log,prod 沒這個 DB)。
← 回 Article Brief