這次把停更多年的 Hexo 部落格搬到 Astro 6,我確實讓 AI Agent 參與了大量工作。但我沒有只給一句「幫我把 Hexo 改成 Astro,畫面做漂亮一點」。
舊專案最危險的地方通常不是新功能做不出來,而是 Agent 很快做出一個能 build 的新網站,同時讓 139 個舊網址、搜尋引擎 canonical、Disqus identity 和一張拿不到的圖片悄悄消失。
先把任務改寫成可以失敗的規格
如果規格只有「完成遷移」,Agent 很容易把「新首頁能打開」當成成功。這次的目標文件反而先寫出大量會讓任務判定失敗的條件:
scope:
source: Hexo 3.8
target: Astro 6.x
published_posts: 35
local_posts: 36
legacy_html_paths: 139
must_preserve:
- URL 大小寫
- canonical 與 og:url
- Disqus identifier
- 原始發布與更新時間
- 一張失效圖片的真實狀態
must_not_do:
- 公開本機獨有 draft
- 捏造失效圖片
- 搬移失效 UA Analytics
- 未經同意 push 或部署
這段規格的功能不是把 Prompt 寫得很長,而是替後面的 checker 提供輸入。若 legacy_html_paths 寫成 139,最後就必須得到 139 / 139;少一個不能用「大部分正常」帶過。
把權限拆成 gate,不要一次交出去
程式能不能修改、分支能不能建立,以及網站能不能正式上線,是三種不同權限。
這次的流程分成兩個明確 gate:
- 第一次批准只允許建立
migration/astro-6,並在該分支完成 13 個可建置提交。 - 第二次批准才可能包含 push、merge、切換 GitHub Pages Source 與部署。
第一個 gate 通過,不代表第二個也自動通過。即使 workflow 已經寫好,本機驗證全部成功,Agent 仍必須停在 cutover 報告,不能因為「剩最後一步」就自行部署。
這種設計適合所有有外部副作用的 Agent 工作。可以把權限拆成:
讀取與盤點 → 本機修改 → 建立隔離分支 → 外部寫入 → 正式切換
每一段都需要不同的驗收證據。Git commit 是可回看的本機狀態;push、雲端設定與 production deploy 則會影響其他人,不能混成同一個「請幫我完成」。
先做 routing spike,再大量搬內容
Astro 支援 build.format: 'preserve',看起來正好能保留 .html 文章與目錄型 index.html。但「文件說支援」和「這個舊站的所有路徑都能保留」仍然是兩件事。
正式搬移 36 篇 Markdown 前,Agent 先建立最小 routing spike,實際 build 出:
/Hexo/why-choose-hexo.html/about/index.html/tags/Hexo-searchdb/index.html/tags/hexo-searchdb/index.html
這裡很快遇到一個容易漏掉的問題:macOS 常見的大小寫不敏感檔案系統,會把後兩個路徑視為同一個位置。Astro 顯示 141 條 route 都成功生成,但實體目錄只剩 139 個 HTML,Pagefind 也少索引一頁。
如果只看 build exit code,這次測試會被判定成功。最後是改到大小寫敏感的 APFS volume 重建,才同時看到 141 個 HTML 與 36 個 Pagefind 索引頁。
這個例子說明 Agent 的驗收不能只問「指令有沒有成功」,還要問「成功後的世界是不是我們要的狀態」。
把大任務切成每一步都能 build 的狀態
遷移依照下面順序拆成 13 個 commit:
- 凍結 Hexo inventory。
- 建立 Astro 6 基礎。
- 定義 Content Collection schema。
- 匯入 Markdown。
- 保留文章路由。
- 重建 archive 與 taxonomy。
- 加入可替換的設計基礎。
- 搬移圖片。
- 恢復 Disqus 與中文搜尋。
- 補上 SEO、feeds 與相容端點。
- 讓舊 service worker 安全退役。
- 加入 parity tests。
- 準備仍未啟用的 Pages workflow。
每個 commit 都必須能 build,不能先提交已知失敗狀態,再期待下一個 commit 修好。這項限制會迫使 Agent 在每一步留下可理解的邊界,也讓人工審閱時不用跨越三、四個 commit 才看懂某個錯誤為什麼暫時存在。
Prompt 不能取代 checker
我可以在目標裡寫「不要弄壞舊網址」,但真正有用的是 check-legacy-urls.mjs。我也可以寫「文章內容要完整」,但最後仍要逐篇比對:
- 標題、日期、分類與標籤。
- 純文字長度與 heading。
- link、image、iframe 與 code block 數量。
- canonical、
og:url與 Disqus identity。
最後的遷移基準包含:
| 驗收 | 結果 |
|---|---|
| Legacy HTML | 139 / 139 exact case |
| 公開文章結構 | 35 / 35 |
| Internal references | 4,543 |
| Fragment references | 275 |
| 圖片 variants | 152 / 152 |
| 靜態 accessibility | 141 / 141 |
| Secret findings | 0 |
| Playwright | 桌機與手機 8 種頁面 |
Prompt 負責說明意圖,checker 負責拒絕不符合意圖的產物。兩者缺一不可。
不確定的事情要留下,不要補成完整故事
Agent 很擅長把缺口補成讀起來合理的內容,但 migration 有幾種缺口不能補:
- 一張 Facebook CDN 圖片已經失效,沒有可驗證備份。
- SlideShare 在 headless browser 可能顯示供應商 fallback。
- Astro 6 的 dependency audit 有一組 moderate 問題,但修復版本屬於 Astro 7 major upgrade。
- 本機獨有的 2021 文章沒有公開紀錄。
這些項目最後分別被標成來源失效、外部服務限制、dependency exception 與 draft,而不是生成一張替代圖片、宣稱 iframe 完全正常、強制 major upgrade,或推測那篇文章原本就應該公開。
對 Agent 來說,「不知道」也必須是一種合法輸出。否則規格寫得越完整,它越可能為了填滿所有欄位而開始猜測。
我現在會怎麼設計 Agent 任務
經過這次遷移,我會先替較大的 Agent 工作準備六項內容:
- Source of truth:哪些檔案、線上狀態或 API 回應可以當成事實。
- Non-goals:哪些相鄰問題本次明確不處理。
- Permission gates:本機修改、外部寫入與 production 變更分別何時允許。
- Acceptance checks:每一項要求如何產生 pass/fail。
- Reversible path:失敗時保留哪個 branch、artifact 或備份。
- Unknowns report:無法取得與需要人工決定的內容放在哪裡。
AI Agent 能加快盤點、產生腳本、搬移內容與重複驗證,但它不應替我們決定什麼可以消失,也不應因為測試大多數通過,就把剩下的差異當成雜訊。
讓 Agent 接手舊專案之前,我現在最先寫的不是「請使用哪個框架」,而是「什麼證據能證明它沒有把舊世界弄丟」。
Discussion
文章留言
留言服務會連線到 Disqus;只有在你選擇載入後才會建立外部連線。