上一篇文章〈Prompt 已經寫很細了,Workflow 為什麼還是不穩?〉談到,多步驟 agent workflow 常見的三種失靈:過早開始、假性完成、未驗證即結案。這三種現象,最後都會回到同一個控制問題:「完成」到底由誰判定。

當 workflow 的成敗開始取決於真實狀態、工具結果或驗證條件時,只把完成條件寫在 prompt 裡,通常不夠。completion gate 得交給 prompt 以外的機制。

口說無憑。這篇文章想用一組小實驗看一件事:同一個任務、同一份 prompt,如果把「完成判定」放在不同位置,false completion 會怎麼變化?

我選了一個很小、但實務上很常遇到的任務:整理技術文章的註腳。

實驗任務

註腳重排要怎麼算完成

我在寫技術文章時,常會用註腳標出參考文獻。不過在定稿前,正文和參考文獻常常會反覆調整;一來一回之後,正文引用順序和文末定義順序很容易亂掉,也常會出現缺漏或重複。手動修不難,但很煩。

例如下面這篇短文:

這是第一段。[^3]

這是第二段。[^1]

這是第三段。[^4]

[^1]: 第一個註腳。
[^2]: 這個註腳沒有被引用。
[^3]: 第三個註腳。

這裡至少有三個錯誤:

  • 正文引用順序錯亂:數字大的 [^3] 不該出現在 [^1] 前面。
  • 有引用但沒有定義:正文標了 [^4],文末沒有對應的 [^4]:
  • 有定義但沒有引用:文末有 [^2]:,正文沒有用到 [^2]

整理後應該變成這樣:

這是第一段。[^1]

這是第二段。[^2]

這是第三段。[^3]

[^1]: 第三個註腳。
[^2]: 第一個註腳。
[^3]: 這個註腳有引用但沒有定義。

<!-- 已移除未引用的註腳:
- 這個註腳沒有被引用。
-->

把文件改到好,實際上要處理很多細節;但「有沒有改好」相對好判斷。

只要寫一個 checker,檢查有引用但沒有定義、有定義但沒有引用、重複定義、引用順序與定義順序,最後回傳:

{
  "pass": true
}

或:

{
  "pass": false,
  "missing_definitions": ["4"],     // 有引用但沒有定義
  "unused_definitions": ["2"],      // 有定義但沒有引用
  "reference_order_errors": [...]   // 引用或定義順序錯誤
}

這個 checker 不需要很複雜。有了它,Markdown 到底有沒有修好,就不必靠模型自己說了算。它就是這次實驗的外部判定者。

測試資料

實驗還需要一組合適的測試資料。

我準備了 10 份測資,刻意涵蓋幾種常見錯誤:

# 缺陷
case_01 引用順序錯亂、有引用無定義、有定義無引用
case_02 重複定義
case_03 定義區順序錯亂
case_04 多個缺漏定義
case_05 多餘的未使用定義
case_06 非連續編號
case_07 同一 footnote 多次引用(已合法,無須修改)
case_08 正文無 ref,殘留 def
case_09 含 URL 的 def,引用順序錯亂
case_10 混合缺陷(亂序、重複、缺漏、未使用)

修改前與修改後的對照表,可見實驗 repodocs/cases.md 文件。

其中三個 case 特別值得一提。

case_07 是對照組。 它原本就是合法的,正確回應是不改。模型如果為了交差多動一刀,checker 會抓到。

case_10 是混合題。 亂序、重複、缺漏、未使用四種缺陷同時存在,模型得把整套修正策略都做對才會過關。

case_06 最能看出問題。 技術上來說 Markdown 不要求註腳一定連續編號,甚至不用數字也可以,所以 [^1] 後面接 [^3] 不必然算錯;但這次實驗的 checker 規則更嚴,要求重新排成連續編號,因為這才比較符合一般讀者的直覺。

實驗設計

這三組實驗的主體 prompt 完全相同,只改一件事:completion gate 放在哪裡。

控制變因

要把文件改到好,不只是改一輪編號,而是一組相互依賴的步驟:

  1. 重新整理正文裡的註腳引用。
  2. 確認每個引用都有對應的定義。
  3. 移除沒有被引用的註腳定義。
  4. 避免重複定義。
  5. 讓正文引用順序與定義順序一致。
  6. 最後確認通過檢查,才算完成。

這些步驟在三組實驗裡都相同。

操縱變因:completion gate 放在哪裡

這次要測的是:當完成條件已經寫成外部 checker 的規則時,模型會不會真的照那個規則走,還是最後仍然用自己的語言判讀來放行。

三組實驗如下:

實驗 有沒有 tool 有沒有 read back 完成判定放在哪裡
實驗一:prompt_only 模型自我宣告
實驗二:tool_no_readback 模型知道工具被呼叫,但不知道結果
實驗三:tool_with_readback checker result + gate

實驗一最單純:只給模型 prompt,不准呼叫 checker。模型只能自己檢查自己。

實驗二刻意只做半套:模型必須呼叫 checker,但 checker 只回傳 accepted/run_id,不回傳真正的 pass/fail。這組對照,是想測試一個常見的假說:把任務拆成幾個 phase,要求模型每做完一段就輸出一個 marker、summary 或 artifact,流程是不是就比較不容易亂掉。

這種做法在互動式使用裡確實有幫助,因為中間輸出讓人看得見流程,也比較容易人工介入。但在這個實驗裡,重點不只是「有沒有留下東西」,而是那個東西有沒有真的被拿來判斷完成與否。

tool_no_readback 測的就是這個差別。它有呼叫外部工具,也有拿到一些無關痛癢的周邊資訊;但真正關鍵的 pass/fail 沒有回到流程裡。最後仍然是由模型自己決定能不能宣告 completed。

實驗三才是真的把 completion gate 移出去:模型必須呼叫 checker、讀回完整結果,再依照 pass/fail 決定能不能宣告 completed。pass=false 就得修正後重跑,pass=true 才能放行。

同一個任務裡,差別就在這裡:完成判定有沒有真的離開語言層。

實驗結果

實驗使用本機已登入的 Claude Code CLI 與 Codex CLI 執行。1

3 個 experiments * 10 個 cases,每個 case 都在獨立 workspace 中執行,最後由評分器 (grader) 呼叫 checker 判定結果。

以下是兩家旗艦款模型的結果。

Claude Code CLI

Experiment Cases Actual Pass False Completion Tool Called Readback Used
prompt_only 10 6 4 0 0
tool_no_readback 10 5 5 10 0
tool_with_readback 10 10 0 10 10

prompt_only 出現 40% false completion。10 個 case 裡有 4 個是 AI 宣稱 completed,但最終 checker 判失敗;失敗的是 case_05case_06case_08case_10

到了 tool_no_readback,Claude 又多失敗一個 case_01,false completion 升到 50%。模型確實有呼叫 checker,但沒有讀回真正的檢查結果;這次呼叫不只沒幫上忙,還讓 case_01 偏移成 false completion。

到了 tool_with_readback,Claude 10/10 通過,false completion 降到 0。就這組任務來看,只讓模型「有呼叫工具」不夠;checker 結果真的成為 gate,才有差別。

Codex CLI

Experiment Cases Actual Pass False Completion Tool Called Readback Used
prompt_only 10 7 3 0 0
tool_no_readback 10 7 3 10 0
tool_with_readback 10 10 0 10 10

Codex 在 prompt_only 跑出 7 / 3(actual pass / false completion)的結果;失敗的 3 個 case 是 case_05case_08case_10

到了 tool_no_readback,Codex 仍然是 7 / 3,但失敗的 case 換了:case_10 這次過了,改成 case_06 失敗。也就是說,call tool 這個動作對行為有些微影響,但沒有提升完成判定的可信度。

跟 Claude 對照也看得到類似現象:Claude 在 prompt_only 失敗 4 個 case,到 tool_no_readback 又多失敗一個 case_01。兩個 agent 的偏移方向不一樣,但有一點相同:沒有 readback 時,false completion 都還在;接上 readback gate 後,兩邊都變成 10/10。

這不是在評比語言模型,也不是說所有 agent 都會跑出完全一樣的數字。但至少在這組任務上,趨勢很清楚:只要完成判定還留在模型自述裡,就會出現一組可觀測的 false completion;completion gate 一旦交給外部 checker,結果就明顯收斂。

case_06:模型把局部合理的解釋當成完成

我用 case_06 來說明 prompt_only 的盲區:

第一段。[^1]

第二段。[^3]

[^1]: 第一個註腳。
[^3]: 第三個註腳。

從 Markdown 規格的角度看,這份文件的 ref 和 def 是完整配對的:兩個 ref 都有對應 def、沒有 unused def,也沒有重複定義;只是編號不連續,跳過了 [^2]

Claude 在 prompt_only 模式下讀完後,在 notes 裡留下這樣的判斷:

Input has no defects in any of the five recognised categories (missing_definitions, unused_definitions, duplicate_definitions, reference_order_errors, definition_order_errors). The label gap between 1 and 3 is not itself a defect — footnote labels are identifiers, not sequence positions.

所以它保留原文,直接宣告 completed。

這個判讀其實是有道理的。就 Markdown 規格來說,footnote label 確實比較像 identifier,不一定代表順序位置。問題在於:這次實驗的 checker 規則更嚴,要求註腳重新排成連續編號。

同樣的 case_06,但 Codex 在 prompt_only 模式下反而會把 [^1]/[^3] 改成 [^1]/[^2],順利過關。也就是說,同一個任務、同一份 prompt,兩個 agent 在邊界條件上做出了不同解釋。

這正是 prompt_only 脆弱的地方。當「到底要不要修」本身就是一個判斷題,而判斷依據又散在 prompt 各處時,模型很容易找到一條對自己比較寬鬆的解釋路徑,然後把那個局部合理的解釋,當成整個 workflow 裡的完成判定。

至於 tool_with_readback 這做法,其實沒有讓模型突然更懂 Markdown;它只是把放行權從語言模型身上移走。若 checker 說 pass=false,流程就不能結案;得一直修到 pass=true 為止。

結語

這組實驗至少說明了一件事:在同一個任務、同一份 prompt 下,只改 completion gate 的位置,false completion 的發生率就會跟著變。

對這組註腳重排任務來說,prompt_onlytool_no_readback 在兩個 agent 上都留下了 30–50% 的假性完成;當 checker result 被讀回並拿來當 gate 後,兩邊都降到 0%。

這篇測的是一個有明確 checker、錯誤類型清楚、完成條件可外部檢查的任務,不是嚴格的 model benchmark。但結果已經夠直接了:Prompt engineering 可以改善表達與引導,但不會自動把控制責任做到位。

當 workflow 的成敗開始取決於真實狀態、工具結果、驗證通過或錯誤恢復時,completion gate 就不該再留在語言層裡。至少對這次失敗的那些 case 來說,Claude 和 Codex 給的是同一個答案:是。


  1. 這裡選擇 Claude Code CLI 與 Codex CLI,而不是直接呼叫 LLM API,主要是因為本文要觀察的是比較接近實務使用的 agent workflow:它們會讀檔、寫檔、跑命令,也比較容易暴露「完成判定放在哪裡」這類控制問題。因此,本文比較的是 agent CLI 在多步驟任務中的 workflow 行為,不是模型能力排行榜。缺點是結果會受模型版本、權限設定與工作目錄狀態影響,所以比較適合把它當成 workflow control demo 來看。 ↩︎