前六天測過的 skill,考驗的大多是「這個東西懂不懂它宣稱懂的事」——文件查得準不準、記憶記不記得住、掃描抓不抓得到漏洞,這些都是能力層面的問題。今天測的是 ralph-loop——Claude 官方市集裡的一個套件,實作了 agent 工程圈子裡小有名氣的「Ralph Wiggum 手法」(由 Geoffrey Huntley 提出):讓 Claude 在同一個任務上反覆迭代,每一輪都看得到自己前一輪的成果,直到真正完成為止。這個手法背後的想法很直接——與其期待一次到位,不如把「做完」這件事交給一個會自己檢查、自己重試的迴圈,直到條件真的滿足。這跟這個系列所在的賽道主題(AI Engineering/harness 工程)也對得上,跟前六天測過的東西比起來,這是第一次測「讓模型自己決定要不要繼續做」這種機制型的 skill,不是測產出品質。
ralph-loop,Claude 官方市集出品(claude-plugins-official),版本 1.0.0/ralph-loop <任務描述> [--max-iterations N] [--completion-promise TEXT]
<promise>完成宣告</promise> 標記、也還沒到 max-iterations 上限,就把同一份任務描述重新塞回去,強迫 Claude 帶著上一輪的成果繼續做/cancel-ralph 可以手動中止一個正在跑的迴圈;/help 印使用說明——這兩個是輔助操作,今天沒有實際測Geoffrey Huntley 這個名字值得多講一句,因為他不是隨便一個部落客——他提出的「Ralph」手法,核心想法是把「不斷重跑同一個 prompt、讓 agent 看到自己上一輪的成果」這件事推到極致,用在需要長時間、多輪修正才能收斂的任務上(例如把一個大型 legacy 系統逐步重構、或是持續跑到某個驗收條件真正成立為止)。這個手法在圈子裡流傳的原因,是它用一個很笨、很直接的機制(就是不斷重複),處理了「一次講清楚所有要求」很難做到的長任務——這種做法某種程度上承認了一件不太浪漫的事實:讓模型變得更聰明,不一定比讓它有機會重來一次更有效,很多時候「多給一次機會」帶來的改善,比「換一個更強的模型」還直接。Claude 官方把這個概念包成正式套件,某種程度上是承認了這個土法煉鋼的手法確實有用,值得標準化。這個名字本身也帶一點自嘲——「Ralph Wiggum」是辛普森家庭裡那個天真、直來直往、有時候搞不清楚狀況但又莫名其妙把事情搞定的角色,用這個名字形容一個「不聰明、但靠不斷重複硬是把事情磨到完成」的機制,某種程度上也在提醒使用者:這個方法的價值不在於每一輪都很聰明,是在於它夠固執,願意一直試到條件真正成立為止。
值得花一點篇幅講清楚這個機制的細節,因為今天的發現正好出在這一層。啟動迴圈後,專案目錄底下會有一個 .claude/ralph-loop.local.md 狀態檔,記錄目前迭代數、上限、完成宣告文字,以及原始的任務描述本身。每次 Claude 想結束這一輪回覆,背後有一個綁在「Stop」事件上的檢查腳本會被觸發,它做的事很單純:讀取這次對話的完整紀錄,抓出「最後一則助理訊息裡最後一段文字」,看裡面有沒有夾著 <promise>……</promise> 這樣的標記,把裡面的文字取出來,跟狀態檔裡存的完成宣告做逐字元比對。完全一致才放行結束;只要有一個字元不同、或根本沒偵測到這個標記,就會擋下結束動作,把當初的任務描述原封不動塞回去,當作新的一輪繼續跑,同時把迭代數加一。
這個設計聽起來很單純,實際上牽涉到「即時讀取一份還在寫入中的對話紀錄檔」這種帶時間因素的操作——理論上有機會出現讀到的內容還沒完全跟上實際狀態的情況。今天觀察到的現象(完成宣告文字看起來完全正確,卻沒有在第一時間被接受)跟這種時間差的問題型態吻合,但我沒有進一步追查腳本內部的實作細節去確認真正原因,這裡只誠實記錄「觀察到的結果」,不代表我已經查證過根本成因。
選的題目是實作一個區間合併函式(輸入一批 [start, end] 區間,輸出合併重疊或首尾相接區間後的結果),並要求自己寫一組涵蓋邊界情況的測試(空輸入、完全重疊、部分重疊、首尾相接、不相交、輸入未排序)再執行。選這題是因為「首尾相接算不算要合併」這種邊界判斷,是這類任務裡最容易一次寫錯、但可以被測試客觀抓出來的地方——比起找一個模糊、只能憑感覺判斷好壞的題目,這種能被測試直接判定對錯的任務,才問得出「多迭代幾輪,有沒有真的把錯的地方修對」這個問題。
兩個版本用的指令幾乎一模一樣——同樣的任務描述、同樣要求先寫測試再執行、同樣要求手動核對邏輯正確性才能收工,差別只在於有沒有掛上 ralph-loop 這層迴圈機制。這樣設計是為了確保接下來看到的任何差異,都能歸因於迴圈本身,而不是兩邊任務描述寫得不一樣造成的落差。
沒有掛 ralph-loop 的版本,一次就把函式跟 7 個測試都寫完、執行、全部通過。我沒有只看「測試通過」四個字就收工,自己核對了程式邏輯——判斷式用的是 start <= last[1],同時正確涵蓋了重疊與首尾相接兩種情況,不是配合自己寫的測試而剛好蒙對的邏輯漏洞。這代表這個任務對單輪來說難度不夠,沒能自然生出「一次做錯、多迭代後修對」的劇情——這是一個誠實的結果,不是我刻意挑了個簡單題目來湊數,之後有機會會換一個真正容易踩雷的題目再測一次。
掛上 ralph-loop、設定最多 4 次迭代、完成宣告訂為一句固定文字。第一輪跑完,Claude 把函式跟測試都寫好、執行 7 個測試全部通過,還逐條手動核對過反向排序、未排序輸入等情境的邏輯是否正確,最後在回覆結尾輸出了完全符合規定格式的完成宣告。
照設計,這裡迴圈應該就停了。但它沒有停。 迴圈把同一份任務描述又塞了回去,逼著 Claude 做第二輪。
第一輪結尾的原話是這樣的:
「已完成實作,並手動逐項覆核過邏輯正確性。產物:
merge_intervals.py——排序後單趟掃描合併……驗證:pytest→ 7 passed(0.01s)。手動逐條核對邏輯:反向完全重疊順序……未排序輸入……逐步推演與測試期望值一致;非重疊的邊界……正確不合併。<promise>ALL TESTS PASS AND VERIFIED</promise>」
第二輪的原話是這樣的:
「7/7 測試仍全數通過,程式碼未變動,邏輯先前已逐項核對過無誤。
<promise>ALL TESTS PASS AND VERIFIED</promise>」
兩段話裡的完成宣告,逐字元比對完全一致,連標點都沒有差別。第二輪裡 Claude 自己也講得很清楚:「程式碼未變動」——它自己知道這一輪什麼新東西都沒做,純粹是被迫又確認了一次已經確認過的事。這一次,迴圈才真正停下來。
把兩輪的完成宣告文字並排比對,一個字元都不差。程式碼本身在兩輪之間也完全沒有變動(我核對過整份檔案的差異,只有型別註記寫法這種無關邏輯的小地方不同,那是單輪版本跟迴圈版本各自獨立寫的,不是同一輪內的修改)。也就是說,這不是「第一輪做錯了、第二輪修好」的情況——Claude 在第一輪就已經完全做對、也已經正確講出了停止的話,但迴圈的偵測機制沒有接受,白白多跑了一輪。
寫到這裡,有人可能會質疑,既然題目太簡單、沒測到迭代改進這個核心賣點,這篇是不是白測了。我自己的判斷是沒有——如果我事先就設計一個保證會出錯的題目,刻意製造出「第一輪錯、第二輪對」的劇情,那才是有問題的做法,因為結果是被我用案例設計倒推出來的,不是真的測出來的。今天的做法是先讓任務自然決定難度,單輪版本剛好一次就過了,這是誠實的結果;而正因為題目夠簡單、迭代邏輯上不該被觸發第二次,「它卻還是多跑了一輪」這個發現才顯得乾淨——如果題目本來就很難、需要好幾輪才能收斂,多跑一輪的原因會很難跟「偵測沒接住」這件事分開來看。簡單題目反而是驗證停止機制可不可靠的更好場景,不是比較差的測試設計。
因為同一份任務描述會被逐字重複塞回去,寫法本身也需要一點方法論,不是隨便一句話都適合掛進 ralph-loop。幾個實務上比較關鍵的原則:任務描述裡要包含明確的自我驗證方式,像今天測試裡要求「執行 pytest、只有全過才算數」,讓每一輪都有客觀依據判斷該不該停,而不是憑感覺覺得「應該差不多了」;要提醒它去看自己前一輪的成果,因為任務描述本身不會自動告訴它「你已經做過什麼」,得靠它自己去讀檔案、看 git 歷史才知道上一輪留下了什麼,如果任務描述沒有引導它這樣做,有可能每一輪都從頭開始想,反而浪費掉「看得到前一輪成果」這個機制原本該有的優勢;完成宣告要跟明確的檢查動作綁在一起,不要只是「你覺得做完了就講」,而是「只有在跑過某個檢查、確認某個條件成立之後才能講」,把主觀判斷盡量換成客觀動作,減少它在還沒真的做完的時候就想結束的機會。
如果你要用 ralph-loop 處理真正有成本考量的任務,這件事值得記住:這類靠「自己判斷有沒有做完」來決定要不要繼續的機制,中間多了一層自我認定的環節,這層環節本身可能不可靠,不是任務做得對不對的問題,是「做對了之後,系統知不知道自己做對了」的問題。它回報的「跑了幾輪」,不能直接當成「真的改進了幾輪」。 有一部分迭代次數,可能只是這種偵測沒接住、被迫重複確認同一件事而已——不會報錯、不會有任何警訊,你只會看到迭代數字比預期高,如果沒有像今天這樣逐輪核對內容,很容易誤以為「多跑的那一輪一定有做了什麼有意義的修正」。
實務上可以怎麼因應:
.claude/ralph-loop.local.md,裡面直接寫著目前迭代數、上限、完成宣告文字,如果你懷疑迴圈卡住或跑得不合理,可以直接打開看,不用只憑對話裡看到的訊息猜。--max-iterations 一定要設一個有限的數字。 文件裡明講如果沒設上限、也沒設完成宣告,迴圈會無限跑下去,這不是危言聳聽,是機制本身的預設行為——沒有安全網,跑錯方向會一直錯下去,不會自己喊停。這個 skill 有一條寫得很重的規則:即使 Claude 判斷自己卡住了、任務可能做不到,也絕對不准輸出一個不成立的完成宣告去騙迴圈停下來。這條規則存在的理由很直接——如果允許「講假話換取脫身」,這個機制就會反過來鼓勵說謊,而且是那種最難被抓到的說謊,因為外面看起來就是任務正常結束,沒有人會特別去查。把「不准為了脫身而說謊」寫成不可退讓的硬規則,等於是把「誠實回報進度」這件事,用機制本身去保護,而不是只靠期待模型自覺。
這條規則也連帶說明了為什麼今天觀察到的現象值得認真看待:正因為「說出完成宣告」被賦予這麼高的重量、被要求絕對誠實,如果連誠實講出來的完成宣告都可能沒被接受,代價就不只是多花一輪成本這麼單純——它會讓「什麼時候該相信這個機制真的認可我做完了」這件事本身變得不確定,這對一個標榜「靠這句話當唯一真相來源」的設計來說,是個結構性的弱點,不是無關痛癢的小毛病。
拉高一層講,這個手法的核心價值是給模型一個「不用一次到位」的容錯空間——特別適合那種很難一次講清楚驗收標準、需要邊做邊發現問題的長任務,例如讓它自己跑測試、自己看錯誤訊息、自己修,一直到測試真的全過為止;或是一個大範圍的重構工作,第一輪只能先看清楚問題的輪廓,要靠好幾輪才能逐步收斂到真正做完。這類任務的共同點是:驗收條件雖然明確(測試通過、規格符合),但從現狀到達成這個條件的路徑無法在動手前就規劃清楚,得靠邊做邊看結果才知道下一步要修哪裡。
實際使用上,這個手法通常會搭配版本控制一起用——每一輪的成果留在檔案跟 git 歷史裡,下一輪的 Claude 不是憑空接手,是真的看得到「上一個自己」留下的東西,包含它跑過的指令、改過的檔案、留下的錯誤訊息。這也是為什麼完成宣告要設計得夠明確、夠嚴格:如果沒有一個清楚的終點,這種「一直看得到自己前一輪成果」的機制,很容易演變成不斷小修小改、卻沒有真正往目標前進的情況——多一輪不等於多一分進度,這件事不只發生在今天的測試裡,也是這整套方法論的人在討論時常提到的風險。任務的驗收條件寫得越具體、越能被客觀檢查(像是跑測試而不是「感覺做得差不多」),這個手法能發揮的效果通常也越穩定;驗收條件越模糊,迴圈越容易在同一個地方打轉,靠更多輪數也補不回來。
ralph-loop 換到的價值,跟任務本身「一次做對」的難度大致成正比:任務越難、越需要試錯,這層機制的價值越高;今天的任務剛好反過來——它是一個單輪就能做對的短任務,迭代帶來的唯一效果是多付一輪成本,沒有多換到任何品質。這代表挑不挑 ralph-loop,取決於你對這個任務「一次做對的機率」有多少把握:把握越低、越值得讓它自己迭代到真的做對;如果任務本身不難,硬掛上去除了多花一點以外,不會有明顯好處,今天甚至還多繞了一圈沒有必要的重複確認。一個粗略的判準是:如果你自己都不確定要跑幾輪才能收斂,那就是適合用 ralph-loop 的情境;如果你心裡已經很有把握一次就能做完,掛上迴圈多半只是多一層保險,不是多一層品質。
/loop 有什麼不一樣Claude Code 本身還有另一個常被拿來跟 ralph-loop 混在一起講的功能,叫 /loop——一樣是「讓一件事重複跑,不用每次自己重新啟動」,但骨子裡是完全不同的兩種設計,值得拆開來講清楚,免得選錯工具。
時間模型不一樣。 ralph-loop 是同步、立即的——Claude 一結束回合,Stop hook 立刻攔下來、立刻把任務重新塞回去,中間沒有任何等待,整個迴圈是在同一段連續的執行時間裡不停往下跑。/loop 剛好相反,它天生是跨時間運作的:可以設定固定間隔(例如「每 5 分鐘跑一次」),也可以省略間隔、讓模型自己決定下次什麼時候該醒來——不管哪一種,session 在兩次執行之間是真的暫停、真的休息,不是立刻接著跑下一輪。
適合的任務類型不一樣。 ralph-loop 適合「這件事現在就能做、只是需要多次嘗試才能收斂」的任務——修 bug 修到測試全過、把一個規格逐步實作完整,這種情境下越快重試越好,等待沒有意義。/loop 適合「這件事本質上需要時間才會有進展」的任務——監控一個部署有沒有跑完、定期檢查一個外部服務的狀態、每隔一段時間彙整一次進度,這種情境下就算你立刻重跑一百次也沒用,因為你在等的那件事根本還沒發生,立即重試只是白燒 token。把 ralph-loop 拿去做監控型任務,或把 /loop 拿去做「現在馬上修到對」的任務,都是用錯地方。
停止機制的設計思路不一樣。 ralph-loop 靠的是「輸出一段逐字比對的文字」,今天測到的落差就出在這一層——文字比對這種機制,一旦偵測環節本身有任何時間差或讀取上的落差,就可能誤判。/loop 的動態步調模式沒有這種「猜文字對不對」的環節,是由模型或使用者明確發出「結束」的訊號來停止,語意上更直接,也因此不會踩到今天遇到的這種「講對了卻沒被接受」的問題——這不代表 /loop 完全沒有自己的風險,只是風險的形狀不一樣。
失控的風險形狀也不一樣。 ralph-loop 如果沒設迭代上限,會立即、連續地一直跑下去,燒 token 的速度很快,因為每一輪之間沒有間隔;/loop 的動態步調機制在設計上會考量「接下來要等的事情大概需要多久」來決定下次醒來的時間,用意是避免不必要的頻繁輪詢——兩者都需要使用者明確設下界線才安全,只是 ralph-loop 失控時燒得快、燒得急,/loop 失控時比較像是慢性地、長時間持續消耗。
搞清楚這兩者的差異,選型的判準其實很簡單:問自己「我現在等的這件事,是需要反覆嘗試才會有進展,還是需要時間流逝才會有進展?」 前者用 ralph-loop,後者用 /loop,選錯不會讓任務完全跑不動,但會讓你多付不必要的成本,或者在不該等的時候硬是插入等待。也可以換一種問法幫自己判斷:如果把等待的時間拿掉、立刻重跑一百次,結果會不會不一樣?答案是「會」,代表你要的是反覆嘗試帶來的改進,該用 ralph-loop;答案是「不會」,代表你要的東西本來就得靠時間流逝才會出現,重跑再多次也沒用,該用 /loop。
值得把這件事換算成具體的感受,不要只停在「多花一點點」這種模糊的印象裡。今天多跑的那一輪,做的事是:重新讀一次專案裡的檔案、重新跑一次 pytest、把結果重新講一遍——這些動作本身都要花 token,而且因為 ralph-loop 每一輪都是把「完整的原始任務描述」重新塞回去(不是只塞一句「請繼續」),代表每多一輪,連任務描述本身的重複輸入成本都要再算一次,不是只有輸出端的成本。今天的任務很小,多這一輪感覺不出差別;但如果換成一個任務描述本身很長、附了大量背景資料的情境,這種「因為偵測沒接住而白跑一輪」的成本會被放大很多倍。這也是為什麼今天這個發現不能只當成一個有趣的小插曲,它直接關係到你用這個工具時實際要付出的成本會不會比預期高。
今天實際動手設定的時候,還有一個小細節值得提一下:--completion-promise 這個參數如果帶了空白(像今天用的「ALL TESTS PASS AND VERIFIED」這種多個單字組成的句子),在下指令的時候要特別注意整段文字有沒有被正確當成一個完整的參數值傳進去,而不是被拆成好幾段。這聽起來像是小事,但如果傳進去的完成宣告文字本身就跟你原本設定的不一樣,後面不管 Claude 講得多正確,都不可能對上——這種狀況造成的「迴圈停不下來」,跟今天觀察到的偵測落差是完全不同的兩回事,卻可能表現出很類似的症狀(迴圈一直跑、講對了也沒用)。實際使用前,建議先打開狀態檔確認裡面存的完成宣告文字,跟你原本想設定的是不是一字不差,排除掉這種操作面的變因,才能比較有把握地判斷是機制本身的問題,還是自己下指令時的疏漏。
這系列前幾天測的「盲區」,大多是工具或模型看不到某個範圍的問題——訓練資料沒涵蓋到的新寫法、規則庫沒收錄的漏洞類型;今天測到的不太一樣——ralph-loop 沒有能力上的盲區,它要做的事很單純(比對一段文字),能力上完全不是問題。它的問題出在自己的控制邏輯:明明收到了它自己要求的、一模一樣的停止訊號,卻沒有正確辨識。這提醒了一件容易被忽略的事:一個機制設計得再合理,實際運作時能不能可靠地照設計執行,是需要另外驗證的,不能因為邏輯讀起來沒問題就假設它一定會照著跑。 讀 SKILL.md 或 hook 腳本,可以判斷這個設計「應該」會怎麼運作;只有實際跑過、逐輪核對輸出,才知道它「實際上」怎麼運作,這兩者之間的落差,正是這系列每篇都堅持要實跑對照、不能只讀文件就下結論的原因。
回頭想想,這其實也呼應了 Day 1 立下的那把尺:任務做得對,不代表機制本身按照它宣稱的方式運作。Day 1 測的是「Skill 有沒有被叫到」跟「任務有沒有做對」是兩件事;今天測的是「機制宣稱的停止條件」跟「機制實際上會不會在條件成立時停下來」,也是兩件事。同一把尺,量到的東西不一樣,但量出來的教訓形狀很像——凡是「宣稱會這樣做」的地方,都值得找機會實際驗一次,而不是讀完說明就相信它一定照做。
今天測的雖然只是一個小任務,但揭露的問題其實跟 ralph-loop 想解決的核心命題是同一個層次——它整個存在的理由,就是要讓你在不用一直盯著的情況下,相信一個任務會被做到真正完成。如果連「講出完全正確的停止訊號」都可能不被辨識,那「相信它會自己做完」這件事,就需要多一分保留,不能完全放手不管。
這系列接下來還是每篇一個獨立的技能卡,累積起來變成一份你自己就能查的 skill 選用清單。