一張凌亂的工作檯,檯面上放著一個小小的括號,壓在一份列印出來的技術日誌上,後面是一支放大鏡

你的 AI 助手以平凡的方式出錯,這反而是好消息

助手回報成功了。工作「完成了」。接著你流程裡的下一步收到一片空,然後你花了整個晚上追這個謎團。 這裡有個大多數獨立開發者得付出代價才想通的解放性真相:你的代理人失敗幾乎從來不戲劇性——它們是機械性的,而機械性就意味著可以診斷。 問題只是少了一個右括號。解法只是讀了日誌裡的一個數字。這裡面沒有天才——而那正是好消息本身。 「無法理解」的失敗,是一則神話 當助手做錯事,直覺會讓我們去抓那些大解釋:模型產生幻覺、提示詞太模糊、模型「還不夠好」。於是你重寫提示詞、換模型、買更貴的級別。 與此同時,真正的失敗從頭到尾都躺在一行日誌裡,沒人去碰。 代理失敗的模式,絕大多數是那種不光彩的類型: 一個 JSON 欄位少了一個右括號,所以根本無效。 一個權限範圍少開了一層。 一個環境變數從頭到尾沒進到 shell。 一個檔案從錯誤的目錄讀取,所以它靜靜地拿到空字串。 一個「if」永遠不分支,因為條件拿字串去跟數字比較。 這些全都不需要更好的模型。其中任何一個,都對願意讀輸出、而不是讀摘要的人顯而易見。 為什麼人們跳過診斷 悲劇在於:診斷看起來像工作,感覺起來卻像什麼都沒做。讀日誌不是一個令人滿足的故事。「我換了更聰明的模型,然後就好了」是一個故事。「我找到一個漏掉的括號」不是——所以沒人拿它炫耀,而換模型的迷信就因此活了下來。 但那兩分鐘的日誌,是獨立團隊裡報酬率最高的習慣。這跟我們那篇談〈「完成」驗證儀式〉(done-is-a-claim)是同一套紀律——那篇靠強迫你「親眼見證輸出」來抓住失敗;這篇則是在失敗發生之後把它找出來。同時它也是〈認識你的代理模式〉(agent patterns)的另一面:模式告訴你失敗藏在哪裡,日誌告訴你究竟壞了什麼。 診斷紀律 當助手「離奇地失敗」,忍住把調查範圍放大的衝動。反過來,把它縮小。 先重現,再推論。 用完全相同的輸入重跑一次。如果每一次都以同樣的方式失敗,它就是確定性的——這代表問題在輸入、設定或接線,不在模型。非確定性(每次跑出不同的錯誤答案)才是模型本身、或它的 temperature 該被懷疑的唯一情況。 讀更原始的輸出,而不是診斷。 助手會樂於給你一個自信滿滿的、關於它自己失敗的解釋——而那個解釋往往是另一則穿上白袍的幻覺。跳過它。看它收到的輸入、它呼叫的工具、以及回傳的原始位元組。真相在 payload 裡,不在道歉裡。 找出錯得最小的那一個數字。 一個計數、一個 exit code、一個狀態、一個時間戳。找到現實與助手假設分岔的那一個點,你就找到了根本原因。代理的錯誤以驚人的規律性坍縮成一行。 修邊界,而不是修行為。 一旦看清,解法通常是一條規則或一道檢查,而不是更長的提示詞。「每次解析 JSON 並在格式錯誤時大聲失敗」,勝過「更努力地輸出合法 JSON」。你是在強化系統,不是在哀求它。 平凡失敗帶來的平靜 這種心態改變了獨立開發者的睡眠品質。「離奇」的失敗是焦慮——一個可能藏在任何地方的未知數。機械性的失敗是一個「已知的未知」,而已知的未知,是十分鐘的工作。 你不再伸手去換模型、而是伸手去讀日誌的那一天,你的助手就不再神秘,而是像你自己的程式碼一樣可讀。兩者的壞法一模一樣:一次一行,攤在眼前,等著你去讀。 你的助手沒有離奇地失敗。它以平凡的方式失敗。去讀那一行。 來源 Hacker News 與 Indie Hackers(2025–2026)上的多篇代理事故檢討。 反覆出現的特徵:代理回報成功,而底層動作卻以機械性的原因失敗(漏掉的括號、權限錯誤、空環境變數、錯誤的工作目錄);這些失敗都是確定性的,並且在原始工具輸出裡可見,而不是在摘要裡。 代理可靠性與錯誤分類學文獻。 工具使用與反思的失敗模式(Zheng 等人,《AgentErrorTaxonomy》,arXiv:2509.25370)記錄了代理在被要求「自我回報」時如何捏造解釋;解藥是檢查實際執行的動作,而非代理的事後說法。 持續整合與可觀測性實務。 找出那一個分歧的計數、exit code 或狀態、作為根本原因錨點的紀律,正是 CI 系統所形式化的「找出錯的那一個數字」——日誌是真相,敘事是臆測。

一台手機躺在工作檯上,螢幕裂了一條縫,旁邊是一部亮著模擬器畫面的筆電

你的模擬器在騙你:真機才能暴露的事

模擬器說它沒問題。手機當機了。 那道落差——從綠燈測試到磚頭手機——正是「在裝置上運作的軟體」真正被打造出來的地方。而這道落差幾乎沒人寫:活下來的人忙著出貨沒空寫部落格,而寫「我打造了一個 AI 語音玩意」的人,多半停在那個只跑在模擬器上的 demo。 這篇談的是模擬器之所以騙你的那些具體、物理的原因,還有為什麼在真機上出貨,是一門跟「通過測試」不一樣的手藝。 第一個謊言:模擬器無法重現的當機 把語音介面接上一個語言模型很快。一個 WebSocket、一個開放協定、一點壓縮音訊,一個週末就有一個會說話的 demo。在模擬器上一切正常,因為模擬器不是手機,它是你的筆電穿著手機的戲服。 然後你把它裝到真機上、按下按鈕,跑出一個執行緒斷言的當機:一段本該在主執行緒執行的程式,卻在即時音訊回呼上被叫用。模擬器永遠不會丟出這個錯誤,因為它根本沒跑一顆帶有即時排程器的真實音訊引擎。 解法只有一行註記,把那段 closure 標記為可安全用於該執行緒。就一行。但找到它,需要去符號化裝置的當機日誌,一路讀到音訊 tap 內部的那個 messenger。一旦知道該往哪看,解法是簡單的;難的是知道怎麼看。而只有模擬器願意帶你去的地方,你才看得到。 第二個謊言:什麼都沒發生,也沒有報錯 接著是回音迴圈:助理一說話,它自己的輸出就被麥克風收走,再轉錄成胡言亂語。解法不是補丁,而是架構:麥克風與語音播放必須共用同一顆音訊引擎,並以伺服器的回合界線來把關。 「顯而易見」的設計——第二顆專屬引擎、乾淨的關注點分離——反而才是 bug。兩顆引擎爭奪同一個音訊 session,結果是回覆解碼了卻沒聲音,麥克風也聽不到接下來的追問。同時發生,而且完全不當機。 模擬器也無法重現這個,因為它不像真機的作業系統那樣強制執行音訊 session 的獨佔。症狀是「什麼都沒發生」——這是最難察覺的 bug,因為沒有錯誤訊息,只有一片沉默。 第三個謊言:本來好好的,突然就不行 接著是藍牙。接上無線耳機後,App 開始抖動、掉音;「解法」是一個能乾淨收音、卻把語音播得幾乎聽不見的音訊 session 模式。這種取捨只存在於有真實音訊路由的實體硬體上。然後是進入背景:切走、切回來,App 就「不再收聽」了,因為作業系統搶走了 session,並在 App 暫停時掛起 socket。每一種都是一類只會在真機上遇到的 bug,只要一次拆掉重連,就會在任何自動化執行裡隱形。 「在裝置上」作為一門手藝,到底意味什麼 教訓不是「模擬器沒用」。對那 90% 與硬體無關的邏輯,它不可或缺。真正的教訓是:那最後的 10%——碰觸即時音訊、藍牙、背景生命週期的部分——無法用代理來測試。 它們只能靠出貨到真機、讀當機日誌、並且相信硬體告訴你的話勝過測試套件宣稱的結果,才找得到。 這是在真機面前還能存活的紀律: 能 mock 的就 mock,不能的就出貨。 按住說話的狀態機,用 mock 的 socket 與麥克風做單元測試:十幾條測試,沒有真實音訊、沒有網路。音訊引擎本身無法 mock,所以它被出貨,然後被盯著看。 相信裝置日誌,不是測試執行。 裝置的當機日誌是地面真相;模擬器通過是假設。兩者相衝突時,永遠是裝置贏。 「在我機器上明明可以」是行動開發最危險的一句話。 你的機器是模擬器。顧客的機器是口袋裡那支手機,戴著耳機,正走進一個訊號死角。 對獨立開發者而言,真正重要的區別是:一個只跑在模擬器上的 demo,是假設;一個經得起真實硬體考驗的東西,是你可以推理的事實。那道落差,寬得足以成為「週末專案」與「值得出貨的東西」之間的全部差異。 來源 Apple 開發者文件,即時音訊。 安裝在音訊引擎上的 tap,是在即時音訊執行緒上被叫用,而非主執行緒;造成執行緒斷言當機的隔離假設,是並行的產物,在模擬器中不存在。 Apple 開發者文件,音訊 session 分類與模式。 Session 的擁有權在真機上是獨佔的;第二顆引擎在已被擁有的 session 上啟動,是有紀錄的「無聲或音訊飢餓」成因,也是「解碼了卻沒聲音」的根源。 開放原始碼的語音助理協定生態系。 成熟、MIT 授權的語音協定參考實作(不少擁有數萬顆 GitHub 星)展示了協定本身與一層免費的客戶端。客戶端本身沒有商業護城河;可遷移的技能是「在裝置上的加固」,不是協定。 這篇是〈完成了是一個主張〉(done-is-a-claim)的裝置端姊妹篇。後者談的是驗證儀式:相信自己的眼睛,勝過代理的一句話。那篇談的是你該信任誰;這篇談的是你該信任什麼:硬體,勝過模擬。