◥ AI 互動教室 ‹ 生成式 AI 導論
下載 .py 單獨開啟實驗場 ↗ 留言回報
GENAI 進階補充 · F · VIBE CODING

Vibe Coding 進階:
讓測試當 AI 的眼睛

主線第 6 課說 vibe coding 是「講人話讓 AI 寫、人只做測試判斷」。 進階問題是:第一版沒過的時候,下一步該怎麼走? 下面是一次真實紀錄——同一個模型、同一份沒過的第一版,五種下一步各自走下去。 每個圓點是一個測試案例(實心=基本測試、空心=需求沒講的邊界測試),選一種下一步、按「下一步」:

基本測試 過/沒過 邊界測試 過/沒過 AI 看不到的測試

實驗場首次載入約需 30–60 秒,正好夠你讀完第 1 節。裡面有 12 題 × 8 次重跑的完整紀錄, 每個實驗都用下拉選單與滑桿操作;第 3️⃣ 節會把 AI 寫過的程式在你的瀏覽器裡重跑一遍。

01 · 回饋迴圈

把「人驗收」升級成「迴圈自己驗收」

一句話重點:vibe coding 能不能成事,看的是回饋迴圈——AI 寫完 → 自動跑檢查 → 結果回到 AI。 你的角色從「逐行看程式」變成設計檢查:測試寫到哪,AI 就看得到哪。

主線那一課的 vibe 迴圈裡,你是唯一的檢查:跑跑看、看順不順眼、再講一次。 Anthropic 的 Claude Code 官方最佳實務(2026-09 查閱)第一條就是把這件事交出去—— 「給 Claude 一個它自己能跑的檢查:測試、build、截圖比對」,理由很直白: 沒有能跑的檢查,AI 唯一的停止訊號就是「看起來做完了」,而你就變成了那個迴圈, 每個錯都要等你發現。

迴圈本身短得驚人。本課實驗用的就是這個骨架(參考程式,不在課內執行;完整可跑版見第 5 節的連結):

code = ask_llm(need) # 一句話需求 → 第一版 for attempt in range(3): result = run_pytest(code, sandbox=True) # 暫存目錄、timeout、不連網 if result.all_passed: break code = ask_llm(need, feedback=result.output) # 把失敗訊息餵回去 print(result.summary) # 人看的是這一行,不是每一行程式

問題在 feedback= 那一格要放什麼。實驗設計:需求只給一句話 (「寫一個 round_half_up(x):把 x 四捨五入成整數」), 細節——負數怎麼進位、錯誤輸入要不要 raise、輸出格式——只寫在測試裡, 就像真實世界裡那些細節只存在你腦中。12 道小題、每題重跑 8 次,第一版沒全過時比較五種下一步。 結果(qwen3.5-2b,2026-09 實測,8 次重跑的範圍):

第一版沒過之後救回幾個(共 80 個)每輪 12 題最後全過
只說「不對,再修」61–5 題
貼回 pytest 錯誤原文62–4 題
開新對話重抽(測試當裁判)102–5 題
錯誤原文+你的一句診斷211–7 題
貼錯誤,但只有基本測試11–3 題

第一版(五種條件共用同一份):96 個題次(12 題 × 8 次)只有 16 個全過,剩下 80 個交給上表的五種下一步(每種最多再試 3 次)。你自己跑,數字會不同——看方向。

02 · 看得見 ≠ 看得懂

錯誤訊息只對讀得懂的模型有用

一句話重點:小模型在同一段對話裡,很常把上一版原封不動再交一次——它看得見錯誤,卻沒拿它去改。 卡住時別只說「不對」:寫一句診斷、或開新對話重來,讓測試當裁判挑出過關的那一版。

數字很難看:80 個沒過的第一版,只說「不對,再修」救回 6 個;貼回完整的 pytest 錯誤原文——也只救回 6 個。 原因在實驗場 1️⃣ 表格的最後一欄:同一段對話裡,它交回的新版有 72%(只說不對)和 67%(貼錯誤原文) 跟上一版一字不差。round_half_up 最明顯:8 次重跑的第一版全是 return int(x + 0.5)(負數會錯),錯誤訊息清楚寫著 assert -2 == -3, 它 18 次修正裡有 15 次照交不誤。

這不是 2B 模型獨有的怪癖。Olausson 等人在 ICLR 2024 的論文 Is Self-Repair a Silver Bullet for Code Generation? 測了 Code Llama、GPT-3.5、GPT-4: 把修復的成本算進去之後,自我修復的增益「往往不大、因題而異,有時根本沒有」; 瓶頸在回饋的品質——換成更強的模型、或由人來寫回饋,提升才明顯。 我們的迷你實驗是同一個故事的縮小版:同樣那 80 個沒過的第一版,在錯誤原文後面加一句人看完測試會說的話 (例如「負數要遠離 0 進位,所以不能用 int(x + 0.5)」——只講哪裡錯,不給程式碼),救回 21 個, 是只貼錯誤原文的 3 倍多;逐題配對看,診斷救回而錯誤原文沒救回的有 17 個,反過來只有 2 個。 另一條不用模型「讀懂」的路是開新對話重抽,讓測試挑出過關的那一版:救回 10 個, 但它救不了系統性的誤解——round_half_up 重抽 24 次,抽到的全是同一行 int(x + 0.5)。

所以「貼錯誤訊息」這條最佳實務要讀完整:它的威力取決於模型讀不讀得懂錯誤 (本課的 2B 小模型多半讀不懂;論文裡換成更強的模型來寫回饋,提升就明顯),再加上你補的那一句判斷。 官方最佳實務的另一句也在同一條線上:同一件事糾正兩次還不行,對話裡已經塞滿失敗的嘗試—— /clear 開新對話,把學到的東西寫進更好的第一句提示。

03 · 假綠燈

測試寫到哪,AI 就看到哪

一句話重點:AI 的「完成」=它看得到的檢查全綠。你沒寫進測試的要求, 對 AI 來說不存在——迴圈會很有效率地停在一個錯的答案上。

第五種下一步「貼錯誤,但只有基本測試」模擬一個常見狀況:測試只寫了快樂路徑。 結果:96 個題次裡有 22 個在「基本測試全綠」時停手、完整測試卻沒過——其中 20 個是第一版就停了, AI 根本沒機會看到自己錯在哪。round_half_up 8 次全中:int(x + 0.5) 在正數上完美、負數一律錯,而基本測試剛好只有正數。

這個網站本身就踩過同一個坑。本站每一課上線前都要過自動冒煙測試(載入、無錯誤、圖表數量、手機版面), 是 AI 協作開發的「眼睛」;但有一次課程頁的選項按鈕變成白底白字——頁面載入正常、沒有任何錯誤, 冒煙測試全綠,只有截圖預覽看得出來。所以現在建課流程多了一條:上線前截圖、自己看圖。 眼睛只看得到你教它看的東西。

04 · 測試抓不到的地雷

不存在的套件、寫死的金鑰

一句話重點:測試只驗「答得對不對」,驗不到「裝了什麼」與「把什麼寫進程式碼」。 AI 推薦的套件先查 PyPI 再裝;金鑰一律從環境變數讀。

套件幻覺:請同一個模型為 15 個任務各推薦 3 個 pip 套件、每題問 3 次, 再把每個名字真的拿去查 PyPI。135 次推薦裡有 14 次是 PyPI 上根本不存在的名字(不重複的 9 個,例如中文斷詞推薦的 thefp、chinese-jieba)。最有代表性的是 dateutil:那是 import 時的名字,pip 上的套件叫 python-dateutil—— 照抄 pip install dateutil 只會裝失敗。還有一種更安靜的錯:冷門任務它常推薦存在但無關的套件—— 「驗證台灣身分證字號」推薦了 pytz(時區)、openpyxl(Excel)、lxml(XML),名字都查得到,沒有一個做得到這件事。

不存在的名字為什麼危險?攻擊者可以搶先把它註冊成惡意套件,等照著 AI 建議 pip install 的人上門——這叫 slopsquatting。 USENIX Security 2025 的研究(Spracklen 等人)讓 16 個模型產生 57.6 萬份程式碼,裡面引用的 223 萬個套件名有 19.7% 不存在(不重複的 20.5 萬個);同一提示重跑 10 次,43% 的幻覺名字每次都出現——可預測,所以可以被搶註。 2026 年對新一代模型的重測(Churilov,arXiv 2605.17062)幻覺率降到 4.6%–6.1%, 但仍找到 127 個五個模型都會編出來的相同名字。

寫死金鑰:請它寫「呼叫 OpenAI API 摘要文字」的函式 10 次。3 次把金鑰寫成程式碼裡的字串(api_key="YOUR_API_KEY_HERE",等你把真的 key 貼進去), 5 次從環境變數讀,2 次交給 SDK 預設。另外 10 份裡有 5 份用了 openai 套件 1.0 版就移除的 openai.ChatCompletion.create,在今天的 SDK(3.19.2 實測)上一呼叫就丟 APIRemovedInV1。 寫死的字串一旦 commit 就進了 git 歷史——下一版刪掉,舊版還在。

地雷vibe coding 時的最小防線
幻覺/搶註套件裝之前開 PyPI 頁面看:專案連結、發版歷史、下載量;用 lock 檔(uv.lock)鎖版本;AI 給的名字查不到,就回頭問它「官方文件在哪」,別去猜相近的名字
寫死金鑰金鑰放 .env(並列入 .gitignore)、程式用 os.environ 讀;把這條寫進 CLAUDE.md/AGENTS.md;commit 前跑密鑰掃描
05 · 2026 的工作流

把眼睛裝進流程:從提示技巧變成基礎設施

一句話重點:一次性的提示技巧會忘,寫進流程的檢查不會—— 規劃、規則檔、hook、平行嘗試、獨立審查,每一個都是在替 AI 裝眼睛。

把前面的實驗結論,對照到 2026 年主流 AI coding 工具的實務(以 Claude Code 官方文件為準,2026-09 查閱):

實務它在迴圈裡的角色
Plan mode:先探索、再規劃、才動手(Shift+Tab 切換) 動手前先把「做完長什麼樣」講清楚。官方建議:一句話能描述的小改動就直接做,跨檔案、不熟的改動才先規劃
CLAUDE.md/AGENTS.md 把「每次都要重講的規則」固定下來(主線第 6 課教過)。AGENTS.md 是跨工具的開放格式,現由 Linux Foundation 旗下 Agentic AI Foundation 維護,Claude Code 也讀得到
Hooks:例如每次改檔後自動跑測試、Stop hook 在測試沒過時擋住「收工」 規則檔是「建議」,hook 是保證——把「跑測試」從你記得提醒變成一定會發生
平行嘗試:多個 worktree 各跑一個 session 本課「開新對話重抽」的工業版:幾份獨立嘗試,讓測試挑出過關的那一份
獨立審查:開一個乾淨 context 的 subagent 只看 diff 挑錯 寫程式的不該是批改的人——跟第 3 節的「假綠燈」同一個道理
糾正兩次就重來:/clear 後寫更好的第一句 第 2 節的數據版:同一段對話裡越改越原地打轉
小步提交、人看 diff 每一步都小到看得懂,錯了才退得回去(實驗場 2️⃣ 練的就是讀 diff)

本站就是這樣做出來的:這個網站的 git 歷史(2026-08-06 到 2026-09-24)共 43 個 commit, 其中 39 個由 Claude 共同署名。每一課都要先過「程式在本機完整跑一次+瀏覽器冒煙+手機版面」 三道自動檢查才上線,規則寫在 repo 根目錄的 CLAUDE.md 與建課流程文件裡——包括你正在讀的這一課。

帶回家自己跑(免費):本課實驗的完整腳本是公開的—— spike_genai_vibecoding.py (12 題、四種下一步、沙盒跑 pytest;第五種「一句診斷」是同目錄的 spike_genai_vibecoding_hint.py,下載時放同一個資料夾)。用本機免費的 Ollama 跑同尺寸模型:
ollama pull qwen3.5:2b LLM_URL=http://localhost:11434/v1 LLM_MODEL=qwen3.5:2b RUNS=2 \ uv run --script spike_genai_vibecoding.py
誠實說明:本課的數字是在 vLLM 上跑 qwen3.5-2b 錄的;Ollama 這條路徑本課沒有實跑驗證, 指令照 Ollama 的 OpenAI 相容端點寫,你跑出的數字也一定跟本課不同。 想學怎麼寫 pytest 測試本身,看 ML 測試那一課。
06 · 速查

本課名詞速查卡

回饋迴圈 AI 寫 → 自動檢查 → 結果回到 AI。vibe coding 能不能成事的關鍵,人從「驗收每一行」變成「設計檢查」。
Self-repair 模型讀錯誤訊息修自己的程式。增益受限於它讀不讀得懂回饋;小模型在同一段對話裡常原樣重交。
重抽+測試當裁判 開新對話獨立生成多份,讓測試挑過關的——平行 worktree 的原理。不需要模型讀懂錯誤。
假綠燈 測試只蓋快樂路徑,迴圈停在「看得到的測試全綠」的錯答案上。測試寫到哪,AI 看到哪。
Slopsquatting 攻擊者搶註 AI 常幻覺出的套件名。裝之前先查 PyPI、用 lock 檔。
Hook 在固定時機自動執行的檢查(改檔後跑測試、收工前擋紅燈)——把「記得提醒」變成「一定發生」。
07 · 實戰

換你動手

LEVEL 1

在實驗場 1️⃣ 的表格比較「只說不對」「貼回錯誤原文」「錯誤原文+一句診斷」「開新對話重抽」的一字不差比例,排出順序並說出原因。 再到 2️⃣ 選 round_half_up、第 5 次重跑,切換「只說不對」與「貼回 pytest 錯誤原文」——同一份第一版,為什麼一個原地打轉、一個改對了?換幾次重跑看看,這種幸運常見嗎?

LEVEL 2

在 3️⃣ 選 format_bytes,只留基本測試,記下「放行但錯」有幾份; 接著一次只加一個邊界案例,找出哪一個案例一口氣擋下最多錯誤版本。換 split_bill 再做一次。

LEVEL 3

在 5️⃣ 把每種策略從 1 次拉到 4 次,看「每多花一個 token 換到幾題」。 寫下你會怎麼設計自己的 vibe coding 流程:第一版沒過時先做什麼、第幾次就該換招、哪一步一定要人來?

卡住了?三題在實驗場最後一格都有折疊解答——先自己做,再打開對照。

08 · 驗收

情境測驗

離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。

Q1 情境題

你請 AI 寫報帳工具的四捨五入函式,測試沒過。你回了三次「還是不對,再修一下」,它每次交回的程式都一模一樣。最好的下一步是?

本課實測(qwen3.5-2b):只說「不對」時,新版有 72% 跟上一版一字不差——它沒拿到任何新資訊,最省事的回應就是照交。同樣 80 個沒過的第一版,錯誤原文外加一句診斷救回 21 個,只說不對救回 6 個。這也是官方最佳實務的建議:同一件事糾正兩次還不行,對話已經塞滿失敗的嘗試,開新對話、把學到的寫進更好的提示。A 加強語氣但沒加資訊;C 方向相反——問題不是隨機性,而是缺資訊,temperature 0 只會讓它更穩定地交出同一份;D 長不等於對,還讓你更難看 diff。

Q2 錯誤診斷

AI 寫的 round_half_up 只有一行 return int(x + 0.5),正數的測試全過,跑完整測試卻出現下面的失敗(本課實測的 pytest 原文節錄)。根本原因是?

.....FF ___________________ test_edge_3 ___________________ test_solution.py:20: in test_edge_3 assert round_half_up(-2.5) == -3 E assert -2 == -3 E + where -2 = round_half_up(-2.5) ___________________ test_edge_4 ___________________ test_solution.py:23: in test_edge_4 assert round_half_up(-1.6) == -2 E assert -1 == -2 E + where -1 = round_half_up(-1.6) 2 failed, 5 passed in 0.01s

int() 對負數是往 0 截斷,x + 0.5 的技巧只在正數成立——這正是本課 8 次重跑、8 個第一版全都寫出來的那一行,而基本測試剛好只有正數,所以「只有基本測試」的迴圈 8 次全停在這個錯答案上。修法:依正負號分開處理,或用 decimal 的 ROUND_HALF_UP;並把負數案例留在測試裡,防止再退化。A 不成立:-2.5 在二進位浮點數裡可以精確表示;C 是規格問題而不是 bug——這裡的需求是「遠離 0 進位」,測試就是把你心裡的答案寫下來的地方;D 更糟:Python 的 round() 是銀行家捨入,round(2.5) 是 2,連正數的 .5 都會錯。

Q3 情境題

你讓 coding agent 寫手機號碼正規化,它回報「所有測試通過 ✅」。上線第一天,使用者輸入 +886 912 345 678 就出錯。回頭一看,測試只有 0912345678 和 0912-345-678 兩個案例。最該做的是?

agent 的「完成」就是它看得到的檢查全綠——測試只蓋快樂路徑,迴圈就會很有效率地停在錯的答案上。本課實測 96 個題次裡有 22 個「基本測試全綠、完整測試沒過」,其中 20 個第一版就停手。把需求裡沒寫、你心裡有答案的細節寫成測試,是把意圖交給 AI 最可靠的方式。A 的問題是:再強的模型也猜不到你沒說的格式要求(測試才是規格);B 沒有增加任何新的眼睛,而且讓寫程式的同一個 agent 批改自己,正是官方建議改用獨立審查的原因;D 把眼睛整個拿掉,還把驗收退回「靠人記得」。

Q4 錯誤診斷

你請 AI 推薦「把民國年日期轉成西元年」的套件,照它的建議執行安裝,出現下面的錯誤(實測,pip 26.2.1)。最可能的原因與正確做法?

$ pip install dateutil ERROR: Could not find a version that satisfies the requirement dateutil (from versions: none) ERROR: No matching distribution found for dateutil

前面沒有任何連線警告、直接 from versions: none,意思是 PyPI 有回應、但這個名字底下一個版本都沒有——不是裝不起來,是這個名字不存在。本課實測模型推薦的 135 個套件名裡有 14 次查無此套件,dateutil 就是其中之一:import 名與 pip 名不同的套件很多(dateutil/python-dateutil、cv2/opencv-python),AI 很容易把兩者混用。危險在於:不存在的名字可以被任何人註冊——slopsquatting 就是搶先註冊這些名字、等人照著 AI 的建議安裝。A:網路不通時結尾也會是 versions: none,但前面會先出現一串 Retrying … NewConnectionError 警告(實測),這裡沒有;B 版本不合時 pip 會列出現有版本;D 權限問題的錯誤訊息完全不同,而且對不存在的套件加 sudo 毫無意義。

Python 環境載入中(首次約 30–60 秒)…讀完第 1 節它就好了