用 Claude 打造個人軟體
原文由 Nelson Elhage 于 發布,訂閱此部落格
本月初,我用 Claude 把一個 Emacs 套件(的一部分)移植到 Rust,將執行時間縮短了 1000 倍以上(其中一個實際案例:從 90 秒縮短到約 15ms)。
這類為了完成正事而得先繞去處理一堆瑣碎支線的雜活,我無論是在工作上還是為了維護自己的電腦環境,都還算常做。不過這一次,在我的監督下,Claude 幾乎獨力完成了整個專案,我幾乎一行程式碼都沒寫,相較於手動實作,專案進度被大幅加快了。
一開始做這個專案時,我原本還預期 Claude 頂多只能幫上一點小忙——大概就是「更好用的文件搜尋引擎,兼更好用的 Stack Overflow」這種程度。儘管我在 Anthropic 工作、也偶爾會讀 Simon Willison 的文章,我的期待顯然已經大大過時了!
這次經驗讓我對 LLM 在軟體工程、以及在我個人工作中的角色,有了不少改觀。這些想法還在發展中,但這篇文章試著記錄下我的經歷,並一邊思考該如何調整自己的行為、信念與期待,一邊把想法說出來。
問題所在
過去一年多來,我成了 Obsidian.md 的重度使用者,用它取代了原本 Emacs org-mode 檔案和 Workflowy 的組合。我喜歡 Obsidian 的地方有很多,但跟這篇文章有關的重點是:(a) 它直接把資料以原生的 Markdown 檔案形式儲存在本地磁碟上,以及 (b) 有一個還不錯的 Emacs 模式可以跟這些檔案互動。即使在筆電上,我主要還是用 Obsidian 的應用程式,但能夠隨時切進 Emacs 做複雜的文字處理,或是在寫程式時直接在編輯器裡管理待辦清單,對我來說非常有價值。
不過,隨著我的知識庫越來越大,問題出現了:obsidian.el 變得慢到無法使用。光是打開一篇筆記就會讓 Emacs 卡住,一開始是幾秒鐘,後來甚至要一分鐘以上。問題最後發現出在 obsidian-update 函式上:obsidian.el 會定期重新走訪整個知識庫,掃描每一篇筆記的標籤與詮釋資料,以更新內部的索引,而這個完全用 elisp 寫成的掃描過程,結果就是非常緩慢。
我的計畫
我算是個還過得去的 elisp 程式設計師,但我完全沒有分析或最佳化 elisp 效能的經驗,而且 GitHub 上的討論串裡已經有其他人的幾次嘗試。我想,不如換個做法。
我計畫用 Rust 寫一支小程式來掃描並盤點我的知識庫,把最精簡的詮釋資料以 JSON 輸出,再修改 obsidian.el 去讀取這個輸出。我對 Rust 比較得心應手,也知道它通常效能很好,而且有非常棒的效能分析工具。
這樣的解法也許很難被上游專案接受,但我相當樂觀地認為,對我自己使用而言,這會是一個相對小而可控的專案,而且我很確定這樣就能解決我自己的問題。
使用 Claude
一時興起,我決定看看 Claude 能幫我解決多少問題。我本來沒預期效果會多好,但還是想試試看——或許 Claude 會讓我驚艷。我決定從一個定義相對明確、目標也相對清晰的請求開始,但請它一口氣完成專案的大部分:
[Uploaded file:
obsidian.el]
obsidian-update函式慢到讓人難以忍受。我想透過把相關邏輯移植到一個 Rust 程式來加速,它會以 JSON 輸出相關的細節。你能幫我寫這個 Rust 程式的第一個版本嗎?它應該接受 vault 目錄作為命令列參數,其他所有值都可以假設為預設值。
簡而言之:這樣就成了。這是最初的版本。
只用一個提示詞,沒有任何 chain-of-thought 或其他顯式的推理過程,Claude 就讀完了約 1,000 行 Emacs Lisp,找出其中與 obsidian-update 相關的約 200 行,辨識出那段程式碼與檔案中其他部分的資料邊界,設計出該資料的 JSON 格式,然後把相關邏輯移植成約 150 行的 Rust 程式碼。
產出的程式碼一次就編譯成功並能正常運作,Claude 甚至還產生了一個可用的 Cargo.toml,裡面包含了所需的依賴套件。
我進一步嘗試——它能不能也寫 Emacs 那一端?我決定野心大一點,請它使用 Emacs advice 來修補 obsidian.el,目標是產出一個我可以直接放進自己設定檔、而不需要 fork 或修改上游專案的東西。提示詞如下:
[Uploaded file:
obsidian.el]
[Uploaded file:obsidian-scan/src/main.rs]現在,請寫一段 elisp 程式碼來修補
obsidian.el,讓它使用obsidian-scan。請不要直接編輯原始檔案,而是使用 elisp 的「advice」功能,這樣我就能在載入 obsidian.el 之後,把你的程式碼片段放進我自己的設定檔中。
這個也幾乎一次就成功了。它有一個(就我所知的)bug:Emacs 端會在標籤前加上一個 #,但 Rust 端已經包含了一個,結果產生了像 ##writing 這樣的標籤。我請 Claude 修掉它:
obsidian--tags-list裡出現了帶有重複##的項目,像是##writing。你覺得我們應該修 Rust 的程式碼還是 elisp?選一個來更新吧。
Claude 選擇了 elisp,並重寫了檔案,做了這個微不足道的修正。這正是這個專案中剩下所有互動的寫照——我提出一個改進或修正需求,Claude 就會執行,最多只有一些小小的狀況。
我用這種方式又來回了幾次。值得一提的是,我請 Claude 輸出(檔案 -> 標籤)的對應關係,並在 Emacs 中讓 obsidian-tag-find 使用它。我現在已經把 Rust 程式和 elisp 修補都加進了我個人的系統設定中,到目前為止都用得很滿意!
整個專案大概只花了一個下午的時間。
反思
Claude 寫程式的能力遠比我想像的強
我當初下那個原始提示詞時,其實沒抱太大期待。但我聽說 Claude 3.5 Sonnet 在寫程式方面進步很多,而且一般來說,即使預期會失敗,我還是會試著定期讓 LLM 試試看,因為這是我發現唯一能真正掌握它們能力水準的方法。
首先,讓我印象深刻的是最初的重寫:我本來以為 Claude 在有明確規格的情況下寫個約 100 行程式碼應該沒問題,但它能從上千行的 elisp 中萃取出隱含的規格,並依照那個規格實作,讓我很驚訝。更讓我驚艷的是後續反覆迭代系統的過程;Claude 持續展現出很強的能力,能從上下文視窗中讀取中等規模的程式碼,並以相當細膩的方式更新或回應它們。我嘗試的幾乎每一件事都「直接就成功了」。
在理智上,我非常清楚這些模型的進步是指數級的,6 個月或 12 個月前形成的印象幾乎肯定已經嚴重過時。但那種抽象的理解,並不足以讓人對新的現狀形成準確的印象!我所知道唯一能做到的方法,就是不斷地使用這些模型、與它們互動(並閱讀其他使用者的回報)。正因為如此,即使預期會失敗,我還是嘗試了這個提示詞,也正因為如此,我才寫下這篇反思。
你「應該」對此感到驚艷嗎?
光是在寫這篇筆記的過程中,我就對自己對 Claude 的反應和想法感到著迷。
如上所述,我最初的反應是「驚訝且佩服」,Claude 超出了我的預期。原始的 obsidian.el 大約有 10,000 個 token 長;Anthropic 直到 2023 年才發布第一個能處理這麼多文字的公開模型,而且是根本能處理的程度。而在我的任務中,Claude 需要的不只是從輸入中擷取單一事實或某種概括的重點,而是要準確理解並詮釋散落在檔案各處的許多細節。這種事在不久前模型還做不到!而且它是在單一提示詞內、沒有任何 chain-of-thought 或顯式推理的情況下完成的!所以,是的,我確實感到佩服。
與此同時,就在我撰寫這篇文章的當下,我注意到自己心裡出現了一種熟悉的「移動門檻」心態!我發現自己正從驚訝與讚嘆,轉向一種近乎見怪不怪的輕描淡寫,至少可以從兩個角度來看:
- 好吧,但我本來就該預期到這個嗎?我已經看過許多工程師對 Claude 3.5 Sonnet 的程式能力讚不絕口。我親眼看過它解出 Anthropic 的面試題目;這次的練習涉及的程式碼是多了一些,但我也看過它閱讀並回答關於大得多的程式碼片段的問題。我感到驚訝,但那比較是我期待值的問題,而不是模型真的有多令人驚艷!
- 這個任務真的有那麼令人驚艷嗎?這個專案「只」涉及約 1000 行程式碼;我平常處理的許多專案動輒數十萬甚至數百萬行程式碼!把 elisp 重寫成 Rust 聽起來也許很厲害,但它本質上「只是」一個翻譯問題:根子上算是定義得相當明確,而語言模型在「看起來像」翻譯的任務上,本來就相當強。
我覺得這兩種觀點——感到敬畏,以及輕描淡寫——在不同層面上都是成立的!以下這些說法都同時為真:
- LLM 寫程式的能力比一年前強得多、得多
- 而且,更不用說,它們現在的能力在 2、3 年前聽起來絕對像是天方夜譚的科幻情節
- 但同時,現在的模型也或多或少「只是同一趨勢的延續」;它們令人印象深刻,但看起來也沒有跟一年前 ML 圈內人和領袖們預測我們會達到的水準差太多
- 還有,儘管令人印象深刻,現在的模型仍然遠遠不及專家級人類的表現。它們在適當的情境下很有用,但(還)無法「取代我的工作」
我想,這大概就是現在 ML 和 LLM 的本質。指數級的效能提升速度,意味著我們永遠卡在兩種體驗之間:一邊是敬畏、驚奇與科幻感,另一邊則是「喔,這只是老調重彈,有什麼好大驚小怪的?」
我抱持著謹慎的興奮
我覺得這個專案讓我對與 Claude/其他 LLM 協作來打造軟體,產生了一種個人層面的興奮與熱情,是我以前從未有過的。
具體來說,我感受到了一種為了解決自己的痛點——「搔到自己的癢處」——而寫軟體、打造自己想要的工具的喜悅與興奮,這種感覺我已經好幾年沒有體會過了。在我更年輕的時候,尤其是在大學期間,我身邊充滿了自己寫的、或是朋友或像我們這樣的人為了自給自足、賦予自己新能力而寫的各種零碎軟體。我們感受到巨大的力量、樂觀與可能性,真切地相信我們可以形塑自己的運算環境,打造自己的工具、雕琢自己想要的體驗。這種精神體現在各種規模上,既包括了幾個朋友之間使用的小腳本,也包括了當時許多最大、最成功的開源專案。
這些年來,世界變了,我也變了。我們的資料越來越被隔離在各種由壟斷企業擁有與營運、態度從漠不關心到懷有惡意的雲端服務孤島中。一切都變得更加複雜,層層疊疊地被抽象、複雜性以及難以理解的 OAuth 高塔所掩蓋。我自己也在職涯中讀了許多、也在那類機構中工作,已經學會反射性地把所有軟體都當成要大規模部署、並存活多年的東西來寫。我甚至喜歡那種工作模式,也享受穩健地建構、尋找堅實基礎的挑戰,但加上每次要搞懂如何向 Google API 驗證身分、或遷移到新版 JavaScript 框架時所感受到的徹底絕望,結果就是我大多已經不再為自己和朋友打造小型軟體了。
而現在,久違地,我再次感受到那股舊日興奮的微光!Claude 在處理那些晦澀難懂的驗證系統、或掌握 React 的最新版本、或其他任何瑣事上,都比我強得多。感覺我們正活在一個——現在、或至少很快——只要能準確描述出我希望存在的工具或腳本,Claude 就能去做那些苦工的世界。而當時間過去、API 被棄用、我們需要遷移到某個框架的第 27 版時,Claude 大概也能承擔那份工作。甚至,我們或許會活在一個每隔幾年就把所有程式碼丟掉、在一個全新的 Claude 對話中,用同樣的問題描述加上最新文件的副本,從零開始重來的世界。我感到一絲希望與興奮:我是否能重新學會打造小型軟體,為自己或為我的社群所用,讓 Claude 幫忙處理過去二十年來滲入軟體工程的無數惱人雜務,同時也讓我能停留在高層次的設計與概念層面,避免讓我那強迫性的完美主義卡在細節裡?
我是在跟工具對抗,而不是與它協作
Claude.app/claude.ai 對於這類問題,感覺並沒有設計得很好。我花了很多時間在介面與磁碟上的檔案之間複製貼上,也把錯誤訊息或其他輸出再複製貼回給 Claude。
我發現要為對話維持適當的上下文很具挑戰性。最後我手上有三個檔案——原始的 obsidian.el、Rust 移植版的 main.rs,以及轉接用的 elisp 程式碼 obsidian-scan.el。我不清楚到底該一直使用同一個長對話,還是該把所有檔案都放到一個專案(Project)裡,然後定期開新對話(什麼時候開?)。在單一長對話中工作很直觀,但隨著上下文視窗變長,速度會變慢,而且我也發現 Claude 最終會被歷史紀錄中各種不同版本的檔案搞混。我試著用「加入專案」按鈕把Claude 產生的 artifacts 複製回專案,但遇到了兩個主要問題:
- 我會在 Project 中留下過時的版本,因為我正和 Claude 快速地反覆修改那些產生的 artifacts
- Claude 不讓我重新命名/修改 artifacts 的標題,所以我最後會得到一個名為「Obsidian.el Performance Enhancement with Rust Scanner」的檔案,這讓我在提問或要求針對特定檔案做修改時,很難指涉它們。
這個領域目前正有大量的活躍開發;或許使用適合的 MCP 伺服器或眾多新興的 AI 優先 IDE 之一,會讓這件事變得更簡單。
不過,整體而言,這次經驗更加深了我的信念:圍繞 LLM 的工具與介面設計,遠遠落後於模型本身的實際能力,而且即使不考慮未來的模型進步,這本身也是一個正在積極實驗與開發的領域。
在明確定義的介面之間工作
在與 Claude 協作時,我發現自己會本能地把問題拆解成具有相對明確且可測試介面的子問題。舉例來說,我不會在一次提問中就請它同時修改 Rust 和 elisp 的程式碼,而是先請它在 Rust 端加入一個功能,然後透過檢查輸出的 JSON 來抽樣驗證,接著再請它做對應的 elisp 修改。
我不確定這有多常是必要的,但我想我這麼做,有很大一部分是為了我自己,而不只是為了 Claude;以介面邊界與可測試性為基礎來拆解工作,能幫助我感覺自己理解並能驗證正在發生的事情,也讓我知道如果情況變得混亂時該如何介入或除錯。這在某種程度上,就像我與資淺工程師合作的方式——由我來做較多的系統設計或拆解,然後把定義明確的元件委派出去。
這絕對也有助於正確性與測試。我並沒有費心去驗證 Claude 的 Rust 程式碼與原始 elisp 吻合到什麼程度,因為除非我發現 bug,否則我其實不在乎;但「給定這個檔案系統結構,你應該找到這些筆記,包含這些標籤與其他詮釋資料」是一個非常容易測試的問題,而如果我更在意,或是未來發現了 bug,我可以把它們寫成測試案例,再請 Claude 去改進程式碼,而不需要自己鑽進程式碼深處去挖。
我預期——至少在可見的未來——這仍會是一個強大的模式:由人類定義介面邊界或進行系統拆解,而 Claude 在「字裡行間」完成工作。可以想見,隨著時間推移,Claude 將能夠越來越自主地處理越來越大的子系統,而我們也會建立起信心與技巧,去處理那些規格更模糊的子系統。
程式碼比以往任何時候都更便宜
有一段時間以來,我越來越抱持這樣的觀點:程式碼的行數本身相對來說是便宜的;軟體開發中真正有價值的資源,更像是對程式碼的理解——它在做什麼、為什麼這麼做——以及知道該寫什麼程式碼的知識1。
LLM 似乎很可能會大幅加劇這個趨勢,至少在短期內是如此。現在你只要花幾分錢就能產生數千行程式碼;但沒有任何人類能理解它們,而且就目前而言,LLM 在除錯、重構、設計與維護這些程式碼方面的能力,還比不上它們生成程式碼的能力。因此,程式碼比以往任何時候都更便宜,但我猜想,洞見、良好的架構設計與理解,至少在目前,會變得比以往任何時候都更有價值。
我猜想這個變化也會影響哪些架構模式會變得合理。也許,寫出容易刪除的程式碼會變得比以往任何時候都更重要,這樣你就可以直接丟掉整個模組,請 Claude N+1 或 GPT M+1 重新來過。也許,打造涵蓋多種不同程式語言的「多語言」系統的門檻會大幅降低,如果你能夠收割不同語言生態系或效能特性的好處,而不需要太擔心要去聘請精通四種冷門語言的工程師的話。這將會很有意思。
我一直想寫點關於 Naur 的 Programming as Theory-Building 的東西,這是我最喜歡的論文之一,也是我所知最早捕捉到這個洞見的論文之一。 ↩︎
隨機一篇部落格
留言
登入後參與討論