為什麼 Skill Mode 下 keyword.md 會重複寫入?三個根本原因、三個修法,還有一些讓你少掉幾根頭髮的教訓。
📋 本文目錄
事情是這樣開始的
想像一下你打開了一個剛生成的 keyword.md,然後你看到這個:
# 遠端工作的真實面貌
遠端工作聽起來很美好,穿著睡衣開會、貓咪在旁邊陪...
(此處省略 1200 字精彩文章)
---
# 遠端工作的真實面貌
遠端工作聽起來很美好,穿著睡衣開會、貓咪在旁邊陪...
(此處又省略 1200 字一模一樣的文章)
---
# 遠端工作的真實面貌
(你沒看錯,還有第三份)
恭喜你,你的 AI 助理今天特別勤勞,把同一篇文章幫你寫了三遍。 這不是功能,這是 Bug。而且是那種「看起來好像在工作」的 Bug, 最讓人崩潰。
這是我們在開發 blog-pro-max(一套把 Skill 注入到 18 種 AI 平台的部落格寫作工具)時, 在 Gemini CLI 的 Skill Mode 上踩到的真實坑。這篇文章就是完整的偵錯過程紀錄, 希望你看完之後,可以省下我花掉的那幾個小時。
發生了什麼事:文章被複製三次
系統架構快速說明
blog-pro-max 的運作方式是這樣的:
- 用
blogpro init --ai gemini把一個 SKILL.md 注入到 Gemini CLI - Gemini CLI 讀取 SKILL.md,知道自己有「寫部落格文章」的能力
- 使用者輸入指令,Gemini CLI 依照 SKILL.md 的規格執行:生成文章、做分析、存檔
理論上,最終應該輸出三個乾淨的檔案:
| 檔案 | 內容 |
|---|---|
output/keyword.md | 文章本文(乾淨,只有文章) |
output/keyword_analysis.md | 所有分析報告(標題建議、審稿、趨勢⋯⋯) |
output/keyword.html | 兩者合併的完整 HTML,一頁看完 |
實際上,keyword.md 裡面長這樣:
🔥 實際輸出(節錄)
文章本文 × 1 → 邏輯審稿報告(包含再次輸出的文章本文)× 1 → 結構審稿報告(又包含文章本文)× 1 → …
最後 keyword.md 的大小:應有 3KB,實際有 27KB。
根本原因大揭秘(共三個,一個比一個妙)
找 Bug 的第一步,當然是把程式碼放在眼前盯著看,然後說「這不可能啊」。 讓我們來逐一拆解。
Bug #1
🤖 LLM 每產一段分析就 write_file 一次
SKILL.md 的 LLM-only 模式指示 Gemini CLI:「執行全科檢查、標題建議、封面提示詞、時事趨勢……」 沒有明確說什麼時候存檔。於是 Gemini CLI 很有 initiative 地選擇: 每產出一段分析,就立刻呼叫 write_file 存一次。
問題是:LLM 在呼叫 write_file 的時候,通常不是「附加模式(append)」, 而是重新生成整個檔案內容。它會把整個對話脈絡(包含剛才輸出的文章本文) 一起寫進去。然後下一段分析完成,又重新寫一次,文章本文又出現一次。
執行 12 段分析 = 文章本文出現 13 次(第一次正常存,後面 12 次每次都夾帶一份)。 恭喜你,獲得免費的 ×13 文章。
⚠️ 核心觀念
LLM 的 write_file 工具呼叫是覆寫(overwrite),不是 append。 每次呼叫都會寫入你在 prompt context 裡看到的「目前完整內容」。 你以為在附加,AI 其實在複製貼上。
Bug #2
🤯 LLM-only 區塊裡偷偷藏了腳本指令(邏輯矛盾)
這個 Bug 讓我看了三遍才看懂。在 SKILL.md 的「不具備執行腳本能力」區塊裡, 步驟 5 有這樣一行:
- 若 AI 具備執行腳本能力,執行:
python output_md2html.py output/keyword.md output/keyword.html \
--analysis output/keyword_analysis.md
等等……這在「不具備腳本能力」的區塊裡,說的是「若具備腳本能力」??
這是某次重構時的遺留程式碼(我知道,不要笑)。 Gemini CLI 解讀這個邏輯的方式是: 「我是 Gemini CLI,我可以執行腳本,所以我要走腳本路徑。 但我現在也在 LLM 模式裡,所以我也要走 LLM 路徑。」 於是兩條路都走了,存了兩次。
🔥 這是什麼感覺
就像告示牌上寫:「如果你會開車,請停車。如果你不會開車,請停車。如果你會開車,請繼續開。」 然後有個人在十字路口停了三次。
Bug #3
🪤 腳本執行失敗時靜默回退到 LLM 模式
content_research.py 需要 GITHUB_TOKEN 或 OPENAI_API_KEY 才能呼叫 LLM。如果這些 key 沒設定,腳本會直接 crash。
但 SKILL.md 沒有說腳本失敗時要怎麼辦。於是 Gemini CLI 很有 can-do 精神地說: 「腳本不行沒關係,我自己用 LLM 生成!」
然後就悲劇了:腳本已經把文章部分寫入了 keyword.md(或者創建了空檔案), Gemini CLI 的 LLM 又重新生成一遍,疊加進去。 最後你打開 keyword.md,看到的是腳本的殘骸 + LLM 的全文,拼在一起。
⚠️ 沉默的失敗是最可怕的
明確的錯誤訊息是你的朋友。靜默回退是你最危險的敵人。 永遠要明確指定「失敗時應該做什麼」,尤其是當執行結果會寫入檔案的時候。
怎麼修:三個外科手術等級的改法
找到病因之後,修法其實相當直接。以下是每個 Bug 對應的修法。
Fix #1
明確指定「全部完成後一次性寫入」
在 SKILL.md 的 LLM-only 存檔規則前,加入明確的防護說明:
❌ 舊的(含糊)
自動執行所有分析:全科檢查、標題建議……
儲存規則:
– output/keyword.md:僅存文章本文
– output/keyword_analysis.md:所有分析結果
– output/keyword.html:合併
✅ 新的(明確)
執行順序(所有分析在記憶體中累積):
全科檢查 → 標題 → 封面 → 時事趨勢 → …
⚠️ 存檔規則(必須嚴格遵守):
– keyword.md:只呼叫 write_file 一次,之後絕對不再修改
– keyword_analysis.md:全部完成後才 write_file 一次
– 禁止對同一個檔案呼叫多次 write_file
關鍵字是「記憶體中累積」和「一次性寫入」。 給 LLM 的指令越明確,它越不會自由發揮。
Fix #2
把錯誤嵌套的腳本指令移出 LLM-only 區塊
把那行混入 LLM-only 區塊的 output_md2html.py 指令直接刪除。 LLM-only 模式就讓 LLM 自己生成 HTML,不要夾帶腳本指令。
改完之後,兩個區塊變得清清楚楚、互不干擾:
/* 具備腳本能力 */
→ 執行 content_research.py(腳本自己搞定一切)
→ 回報結果
/* 不具備腳本能力(純 LLM) */
→ 生成文章 + 執行所有分析(全在 memory)
→ 一次性寫三個檔案
→ HTML 直接用 LLM 合併兩個 .md 的內容輸出
Fix #3
在腳本模式加「失敗就停,別偷偷改模式」
在「具備腳本能力」的步驟 5 後面,加上明確的失敗處理指令:
> ⚠️ 若腳本執行失敗(例如缺少 API 金鑰或環境問題),
> 請直接顯示錯誤訊息並停止。
> 不要改用 LLM 模式重新生成文章,
> 以避免與腳本已寫入的部分內容重疊。
就這一段話。十秒鐘加進去的文字,省了好幾次「為什麼文章又重複了」的困惑。
血淚教訓:寫 AI Prompt 規格時你一定要知道的事
📚 Lesson 1:LLM 的 write_file ≠ append
這是最根本的認知差距。你可能以為 LLM 在「附加分析到 .md 檔」, 但它實際上是「重新生成完整的 .md 內容後覆寫」。 在任何涉及分次寫入的場景,都必須明確指定「collect → write once」的順序。
📚 Lesson 2:Prompt 的分支邏輯要互斥且完整
if A → do X,if not A → do Y。這兩個分支要確保:
- 互斥(進了 A 就不能再進 not-A 的分支)
- 每個分支裡只有屬於自己的指令(不要偷渡對方的內容)
- 都有失敗處理(明確說失敗時要做什麼,不要讓 AI 自由心證)
人寫 if/else 都會 bug,更何況是自然語言寫給 LLM 的條件分支。
📚 Lesson 3:靜默回退(Silent Fallback)是所有 Bug 的老爸
在 AI agent 系統裡,「我做不到就換個方式做」聽起來很聰明, 但如果沒有明確說「換個方式前要先清除之前的影響」, 就會出現兩個執行路徑同時留下痕跡的混亂狀態。
規則很簡單:可以 fallback,但必須先 cleanup;或者根本禁止 fallback,直接 fail-fast。 後者在寫入檔案的場景通常更安全。
📚 Lesson 4:Prompt Spec 也要 Code Review
SKILL.md 裡的那個「LLM-only 區塊藏著腳本指令」的 Bug, 是重構時從其他地方複製過來的殘留程式碼。
普通程式碼有 linter、有 type checker、有 unit test。 Prompt 規格文件什麼都沒有。 所以在你覺得「這個 prompt 寫好了」的時候,最好把它當成 code 一樣做一次 邏輯審查:每個分支都走一遍,看看有沒有矛盾的指令。
📚 Lesson 5:「應該很直觀」是 Bug 最常見的藏身地
「LLM 應該知道只要寫一次吧?」 「腳本失敗了應該會跳錯吧?」 「分支應該不會混在一起吧?」
以上三句話,分別對應了本文的三個 Bug。 凡是「應該」的地方,就是你需要寫明確指令的地方。 在人機協作的系統裡,不要假設任何「顯而易見」的行為。
懶人包 TL;DR
| Bug | 根本原因 | 修法 |
|---|---|---|
| 文章本文重複 N 次 | LLM 每段分析後各自 write_file,覆寫時夾帶完整 context | 明確指定「全部在記憶體累積,最後一次性 write_file」 |
| 同一份檔案被存兩次 | LLM-only 區塊裡偷藏了腳本指令,兩條路都走 | 移除嵌套的錯誤指令,確保分支互斥 |
| 腳本 + LLM 輸出疊加 | 腳本失敗時靜默回退 LLM 模式,兩者輸出重疊 | 加入「腳本失敗停止,禁止靜默回退」明確指令 |
🎉 修完之後的感覺
打開 keyword.md,看到乾淨的文章本文,只有文章本文,沒有重複, 分析全部在 keyword_analysis.md,HTML 合併得漂漂亮亮。 這種「終於正常了」的快感,大概是所有開發者共同的語言。
如果你也在開發類似的 AI agent 系統,希望這篇文章能幫你少踩幾個坑。 如果你踩到的坑比我還奇特,歡迎留言分享,讓大家一起開心(或一起哭)。