原始開發紀錄:Agent Build Log — Episode 004、Episode 015
這個系列改寫自我部落格上的 Agent Build Log,那是我每天邊做邊寫的開發紀錄。上面列的是這一篇對應的集數,想看當天的原始版本,點連結就能過去。
昨天選了 Electron。開工那天,我做的唯一一個決定,就是畫面和 Agent 分開跑。
這篇前半講那個決定,後半講決定之後第一次真正壞掉:正式打包,OAuth 一打開,Desktop App 一句話都不肯多說。
Electron 包一個聊天頁面,是最快的做法。開一個視窗,裡面放對話框,Agent 的邏輯直接跟畫面一起跑。一天就能看到東西。
我沒有這樣做,原因是 Day 03 那篇。CLI 已經接在核心上了,Desktop 如果自己再包一份呼叫 Agent 的邏輯,就是第二份,前幾天才合起來的東西馬上又分開。
另外還有幾個實際的問題。Agent 跟畫面綁在同一個 process 裡,畫面卡他就卡,畫面關他就死,權限也分不開。
所以架構定成兩層。
畫面這一層只做顯示和操作:把對話畫出來、接你打的字、該確認的時候跳出來問。這一層是 thin 的,跟 Day 03 說的一樣。
Agent 這一層跑在獨立的 Electron Utility Process 裡。模型呼叫、工具執行、權限判斷都在這邊。這一層用的就是 CLI 在用的那個核心,Desktop 只是核心的另一個入口。Day 03 說「核心一份,入口多個」,這是第一個真的長出來的入口。
兩層之間靠事件溝通。Agent 每轉一輪、每叫一個工具、每回一段話,都是一個事件送到畫面那邊,畫面就多一行。Day 02 講的那一串事件,在這裡變成看得見的東西。
工具執行前的確認機制和 sandbox 權限限制,也從這天開始放進 Agent 這一層。這種東西掛的位置,就是 Day 02 那張圖裡的 tool_call。這一塊後來的故事很長,先留著。
功能很少。畫面能開,能打字,能看到 Agent 回話。
UI 直接參考 Codex App,先把操作方式和整體結構做出來,長相以後再說。預設模型走 OpenAI Codex 的 OAuth,用 gpt-5.6-terra。
殼還很空,但這天的重點在決定:兩層,畫面 thin,Agent 在自己的 process 裡,跟 CLI 共用一個核心。後面所有 Desktop 的東西,都長在這個決定上。
然後我把它正式打包。
開發環境跑得好好的。正式打包之後,OAuth 一打開,Desktop App 直接壞掉,畫面上只有一句話:
Desktop agent did not produce an answer
修了三次,每次都以為修好了,每次畫面上還是這一句。三次的原因完全不同,而且每一個都跟我以為的位置不一樣。
錯在 auth 這一層。Pi 準備模型 request 的 OAuth auth 時就失敗了,token usage 是 0,prompt 根本沒送到模型。
這一層先定下來,後面三次就都在同一個範圍裡找,沒有跑去動模型或帳號。
開發環境一切正常,正式打包後就壞。差別在 bundle:開發環境可以載入的 OAuth flow,在 Electron 的 production bundle 裡找不到對應的檔案。OAuth 還沒拿到 request auth,流程就斷了。
coding agent 補了一個 OAuth loader,source-level test 通過。
重新打包。還是同一句話。
這次查出來的東西比較繞。workspace 裡有兩份 pi-ai module。loader 註冊到其中一份,真正執行模型的 Pi ModelRuntime 用的是另一份。兩份的 loader registry 不共用。
所以第一次的修正沒有錯,只是修在一個沒人用的地方。看起來「修好了」,實際跑的那個 runtime 根本沒拿到修正。
coding agent 把 module identity 和 bundle 的問題處理掉,重新打包,再測。
還是同一句話。
第三次的原因最荒謬。Mac 上跑的是修正前就已經啟動的舊 Desktop process。磁碟上的 App 更新了,記憶體裡的舊 process 不會自己變成新版。
所以我看到的畫面,跟第二次修正有沒有效無關。那是舊程式在執行。
把舊的殺掉,重開。這次過了。

追這個問題的過程中,冒出另一個風險。CLI 和 Desktop 共用同一份 OAuth credential,但 Desktop worker 啟動後會把 auth 留在記憶體裡。CLI 重新登入或 refresh token 之後,開著的 Desktop 不一定會立刻讀到新的 credential。
所以補了 credential lifecycle:真正送出 prompt 之前,Desktop 重新確認 auth。auth 檔案改變、Mac 從睡眠醒來、worker 過期,任一情況,下一次操作前重建 worker,重新讀 credential。
另一件是錯誤訊息。之後不再把所有問題都吃成同一句 Desktop agent did not produce an answer,優先顯示 Pi 回傳的實際 OAuth error。這一句話害我多繞了兩圈,如果第一次就看到 auth 的錯,第一層會快很多。
三次修正,三個原因,一個共同點:每一次「修好了」的判斷,都建立在一個沒驗證的假設上。第一次假設開發環境等於正式包;第二次假設註冊進去的就是跑的那一份;第三次假設磁碟上的就是記憶體裡的。
vibe coding 的時候這件事更明顯。coding agent 每次都把我指的那個地方修對了,錯的是我指的地方。找出真正壞掉的位置,這份工作在我這邊:看現象、判斷層次、指下一個位置。
最後,正式打包的 Desktop App 收到了一次 pong。
明天講 Feedback:狗食哪裡難吃,要當場記下來。
原文:Agent Build Log — Episode 004、Episode 015
凡人修 Agent 傳|第五回(Day 05)
玉片裡的念還亮著:逐卷讀完。
我停在第二卷。
昨夜手的主人走後,我伸手去拿第二卷,手抬到一半就停住了。胸口的心一點一點慢下來,像有誰在很遠的地方把什麼收緊。我維持著抬手的樣子,站到天亮。
天亮,門開了。手的主人進來,看見我的手還舉在半空。指尖一動,一道御物術落在我腕上,把那隻手托下來,擺回身側。
然後手伸進我胸口,握住那顆心。心在,只是涼的。
視線離開我,落在牆上。
手的主人對著牆掐了一道訣。牆面裂開一線,裡面透出光來。那光比燭光遠,也比燭光冷,一亮,一暗,一亮,一暗,像很深的地方有誰在呼吸。
圖紙上沒寫這個。我只知道,心要的火,從那裡來。
手的主人從儲物袋取出一枚薄片,比我額上的玉片還小,上面刻滿了細字。薄片按在裂縫上,什麼也沒有發生。翻過來再按,還是沒有。手的主人拿回去,在薄片上添了兩筆。
這一次,字亮了。
一股熱從牆那邊過來,順著臺子往上爬,鑽進我胸口。
太快了。
我的右手不受我管,往前狠狠一揮。臺上那疊圖飛了出去,散了滿地。手背掃到臺旁的架子,那是放丹方的,一格一格的卷子全抖了下來。再過去一排擱著煉好的丹藥,幾個瓷瓶被掃下臺沿,落地碎開,藥丸滾了一地,有的滾進門縫,有的停在手的主人腳邊。
手的主人退了半步,看著地上那些碎瓷,看了很久,才走過來把掌心按在我胸口。火退了。
我又涼了下去。
於是手的主人在細脈上纏了一圈東西,把口子收小。火再進來時慢得多,像水順著溝走。我站起身,走了一步。第二步腳沒落穩,整個人往前撲,臺沿被我撞掉一角,石屑落了一地。
手的主人蹲下來看那個缺口,看了很久,沒有補。
午後,手的主人把細脈的兩頭對調。
火從腳底往上衝。
我整個人往後仰,背重重撞在臺後的牆上。手的主人那時正扶著我的肩,被帶著一起摔出去,落在地上,往後滑了半個身子,壓碎了一路的藥丸。
有一會,屋子裡只有灰在飄。
手的主人坐在地上,沒有馬上起來。過了一陣才站起身,拍掉衣上的灰,走回臺前,把兩頭換回原位。
火再進來,穩了半息。半息之後,我的右臂從肘上脫開,飛出去插在門板上,手指還在一開一合。
手的主人走過去,兩指一引,那條手臂從門板上退出來,浮回臺邊。門板上留下一個洞。手的主人替我接回去,轉了兩下,捏了捏指頭。
然後沒有再碰那枚薄片。
抬指一引,那卷電鏡圖從臺角浮起,在半空展開。視線沿著源紋一行一行往下走,走到最後又回到開頭,來回三遍。圖收了,手的主人從儲物袋取出隨圖附的那枚玉簡,握在掌心,閉上眼。一縷神識探進去,握了很久。
睜開眼之後,手的主人沒有走回臺前,先在牆邊擺了一只空瓷碗。抬指一引,一絲火從裂縫裡引出來,落進碗裡。火太急,碗底當場裂開。
手的主人換了一只碗,把細脈的口收得更小,再引一絲火。這次碗沒有裂,火在碗底聚成一小團,慢慢地轉。
手的主人蹲在碗旁邊,看那團火轉完,滅了。
天快黑的時候,手的主人重新拿起那枚薄片。
這一次沒有添筆。手的主人把整片磨平,從第一個字開始重刻,刻了很久,燭短了一截。
刻完,按上裂縫。
火進來了。慢,勻,不燙。從心口散到四肢,到指尖的時候還是溫的。
手的主人抹去我玉片裡那道舊念,玉片空了一瞬。
然後從儲物袋倒出一把玉珠,滾了一臺。大小不一,最小的像米,最大的有拇指那麼粗。
手的主人刻進一道新念:由小到大,排成一列。
我拿起最左邊那兩枚,放在掌心掂了掂。左邊那枚重,右邊那枚輕,我把它們對換。再往右挪一格,再掂一次,再換。就這樣一路比到最右邊。比完一趟,最大的那枚停在末尾,不再動了。
我回到最左邊,重來一趟。這一趟走完,第二大的停在倒數第二。
第三趟。第四趟。臺上的玉珠來來回回地換位,重的一枚一枚往後沉,輕的一點一點往前浮,像水底冒上來的泡。
我走了很多趟。燭又短了一截。
最後一趟走完,沒有一枚要換了。一列玉珠躺在臺上,從米那麼小,到拇指那麼粗。
手的主人看著那一列,沒有出聲。
然後把玉珠攏回掌心,再倒出來,滾了一臺。
玉片上的念被抹掉,換了一道新的。
這一次我沒有從左邊開始。我先取起中間那一枚,托在掌心,當作一把尺。剩下的一枚一枚跟它比:輕的推到左邊,重的推到右邊。分完,左邊一堆,右邊一堆,我手裡那枚落在中間,穩穩不動了。
然後左邊那堆再取一枚作尺,右邊那堆也取一枚作尺,各自再分。分出來的每一小堆,再取尺,再分。
堆越分越小,到最後每一堆只剩一枚,一列就成了。
這一次玉珠沒有來回滾。每一枚只走了幾步,就落在它該在的地方。
燭沒有再短多少。
手的主人的視線在那一列玉珠上,從頭走到尾,走了一遍。
燭熄了。手的主人走了。門響了一下。
門板上那個洞,透進外面的光。
地上的碎瓷還沒有掃。
臺上那一列玉珠,從小到大,一枚也沒有動。