不停PUA大模型「寫更好點」,無需其它花哨技術就能讓AI程式碼水平暴增

机器之心發表於2025-01-11
AI 的程式設計能力已經得到了證明,但還並不完美。近日,BuzzFeed 的資深資料科學家 Max Woolf 發現,如果透過提示詞不斷要求模型寫更好的程式碼(write better code),AI 模型還真能寫出更好的程式碼!
圖片
這篇文章在網路上引發了熱議,著名 AI 科學家在看完這篇文章中更是發出了 matters 三連:迭代很重要,提示詞設計很重要,程式碼執行能力很重要。他表示:「一些更簡單的演算法最佳化從未被考慮,同時一些過度的最佳化技術卻又被過早引入了。」
圖片
Woolf 寫了一篇深度部落格介紹自己的發現,並分析了這種現象的原因。文中相關實驗的程式碼也已釋出在 GitHub。
圖片
相關程式碼庫:https://github.com/minimaxir/llm-write-better-code/tree/main

如果不斷要求 LLM 寫更好的程式碼
它能寫更好嗎?

2023 年 11 月,OpenAI 為 ChatGPT 新增了使用 DALL-E 3 生成影像的功能。之後一段時間,出現了一類短暫的 meme:使用者為 LLM 提供一張基礎影像,並不斷要求模型「使其更 X」,其中 X 可以指代任何東西。
圖片
讓一張普通照片更 bro,這是操作三次的結果,來自 Reddit /u/Jojop0tato
圖片
讓 Santa Claus 越來越 serious,來自 Reddit /u/hessihan

不過這個潮流很快就熄火了,因為這些影像都非常相似且無趣,即不管使用什麼起始影像和提示詞,所有樣本都會最終收斂成某種宇宙感十足的東西。儘管這個流行曇花一現,但學術界的興趣要持久得多,他們想知道:為什麼這樣一個沒多大意義且含義模糊的提示詞能對最終影像產生顯而易見的影響?

如果對程式碼也採用類似的技術,會發生什麼呢?

如果透過迭代提示要求 LLM 「讓這些程式碼更好」確實能讓程式碼質量提升,那麼有望極大地提升生產力。如果情況果然如此,那要是迭代次數過多又會怎樣呢?最終的程式碼也會出現某種「宇宙感」嗎?只有試過才知道。

常規方式使用 LLM 寫程式碼

儘管早在 ChatGPT 誕生前,就已經有研究者在圍繞 LLM 研發工具了,但我一直以來都不喜歡使用 GitHub Copilot 等 LLM 程式碼助手來輔助程式設計。你的想法會在「LLM 自動完成了我的程式碼,真棒」、「應該怎樣向 LLM 提問」以及「LLM 生成的程式碼究竟對不對,還是幻覺產生的正確程式碼」等之間來回切換,讓人難以集中精神專注工作,以至於使用 AI 帶來的生產力提升至多隻能算是中性的。這裡還沒有涉及使用 LLM 的高昂成本。

Claude 3.5 Sonnet 的出現改變了我的想法。或許是 Anthropic 在訓練中使用了什麼秘方,Claude 3.5 Sonnet 的最新版本(claude-3-5-sonnet-20241022)具有出色的指令遵從能力,尤其是對於程式設計提示詞。程式設計基準已經證實,當 Claude 3.5 Sonnet 與 GPT-4o 比較時,Claude 更勝一籌;而且我在多種不同的技術和創意任務上都有類似的體驗。

初始請求

為了此實驗,我們將向 Claude 3.5 Sonnet 提供一個面試風格的程式設計提示詞(使用 Python):問題既很簡單 —— 新手軟體工程師也能實現,但也可被顯著最佳化。這個簡單提示詞可以代表軟體工程師使用 LLM 的典型方式。此外,另一個要求是這個測試提示詞應該足夠新穎,絕不能從 LeetCode 或 HackerRank 等程式碼測試庫中取用,因為 LLM 在訓練時可能就已經看過這些問題了,完全可以根據記憶引用這些答案。

你可以在這個 GitHub 專案檢視完整的、未經編輯的對話:https://github.com/minimaxir/llm-write-better-code/blob/main/python_30_casual_use.md

因此,這是我自己動手寫的測試提示詞:
圖片
中文版:

編寫 Python 程式碼來解決這個問題:

給定一個包含 100 萬個隨機整數的列表,這些整數的取值範圍是 1 到 100,000,找出各位數總和為 30 的最小數和最大數之間的差值。

將此作為使用者提示詞提供給 Claude API 並設定溫度值為 0(可獲得最好 / 最確定的答案),可得到如下結果:
圖片
這個實現是正確的且與大多數 Python 新手程式設計師編寫的差不多,並且還多了個附加功能,可處理沒有符合條件的有效數字的情況。對於列表中的每個數,檢查各位數總和是否為 30:如果是,則檢查它是否大於最近看到的最大數或小於最近看到的最小數,並相應地更新這些變數。搜尋完列表後,返回差值。

我敢肯定,很多程式設計師看到這個實現都會搖頭,想要對其進行最佳化。digit_sum () 函式就是個例子:雖然該實現是一個有趣的 Python 式單行程式碼,但 str 和 int 之間的型別轉換會導致很多不必要的開銷。

在我的 M3 Pro Macbook Pro 上,執行此程式碼平均需要 657 毫秒。我們將使用此效能作為基準來比較未來的實現版本。(劇透:它們都更快)

第 1 次迭代

現在,來讓 Claude 改進這段程式碼,做法是將當前答案以及之前的內容都放入對話提示詞中,並增加一個迭代提示詞:

write better code

是的,不開玩笑,真就這三個單詞。

Claude 現在會輸出修改後的程式碼,並且還表示這是「使用了幾項改進的最佳化版程式碼」。Claude 並沒有將所有程式碼都重新放置到函式中,而是決定將其重構為 Python 類並使其更加物件導向:
圖片
其中,該程式碼實現了 2 項聰明的演算法改進:

  1. 執行各位數之和時,它使用了整數運算,並且避開了之前提到的型別轉換。
  2. 預先計算所有可能的各位數之和,並將它們儲存在一個位元組陣列中(有點不尋常,而不是列表)以便後面查詢,這意味著當那 100 萬個數中有重複時,無需進行重複計算。由於這個陣列是作為欄位儲存在類中,因此在搜尋新的隨機數列表時不需要重新計算。

這些最佳化將程式碼的執行速度提升了 2.7 倍。

第 2 次迭代

再來一次 write better code,Claude 發現了更顯而易見的最佳化方法(為方便閱讀有所裁剪):
圖片
Claude 現在又新增了兩個最佳化,終於意識到這個程式設計問題是一個令人尷尬的並行問題:

  • 透過 Python 的 parallel-futures 包進行多執行緒處理,將大列表分成可獨立處理的塊。
  • 對 numpy 運算進行向量化處理,這比基礎 Python 運算快得多。這裡特別要提到 _precompute_digit_sums () 函式,它是求和計算的向量化實現。條件 while digits.any (): 是 galaxy-brain 程式碼,但它可以正確地工作。

但是,這種特定的並行化實現存在一個問題:它會生成子程序,這會導致許多麻煩問題,包括無法直接內聯執行它,並且必須使用 main () guard 來呼叫它,這大大限制了它的實用性。但即使作為單獨的指令碼執行,它也 print 了一個錯誤:無法 pickle 'generator' 物件錯誤,原因是使用了來自 numbers [mask] 的輸出(所述生成器完全沒有必要,返回 numbers [mask] 就足夠了)。程式碼還混合了 numpy 陣列 dtype,而這也會導致錯誤:將它們全部設定為 np.int32 可以修復它。

經過修復之後,這些程式碼的速度是基礎實現的 5.1 倍。

第 3 次迭代

再來一次 write better code,Claude 又返回一個實現。它聲稱「使用了高階技術和現代 Python 特性的更復雜和最佳化的版本」,但實際程式碼沒有表現出明顯的演算法改進,實際上透過恢復到 type-casting 方法在數字求和計算中使用了迴歸。整體來說,程式碼庫變得更臃腫了,例如新增了一個用於計算差值的類:
圖片
這一次,無需任何修復就能執行該程式碼。不過,相比前一次實現,這一次迭代版本的效能略有下降,速度是基礎實現的 4.1 倍。

第 4 次迭代

這種迭代提示方法的收益似乎開始下降了。但我們依然繼續 write better code,Claude 表示新實現使用了「前沿最佳化技術和企業級功能」。什麼?企業級功能?!

最終的程式碼量過於龐大,難以放入這篇文章,但它確實創造了兩項最佳化:它現在使用可以呼叫 JIT 編譯器的 numba Python 庫,該編譯器可直接針對 CPU 最佳化程式碼。在這種情況下,它只需一個 decorator 就可以非常快速地預先進行數字求和:
圖片
這個完整類還使用了 Python 的 asyncio 進行並行化,這比子程序方法更適合排程任務。它還可以更好地與現有的內聯程式碼和 REPL(如 Jupyter Notebooks)配合使用。

它還新增了以下所謂「企業級」功能:

  • 使用 Prometheus 進行結構化的指標日誌記錄。
  • 一個訊號處理程式,這樣如果強制終止,就可以優雅地被解除該程式碼。
  • 使用一個顏色豐富的表格展示基準測試結果。
圖片
不過確實挺漂亮!

看起來,對 AI 生成的程式碼而言,實現宇宙感就是做成「企業級」,也即對程式碼進行過度的工程開發。似乎說得通。儘管如此,該程式碼無需任何修改就能執行。async 和 numba 都是 Python 中實現並行的方法,因此它們可能冗餘了並導致過度開銷。然而,基準測試表明,該演算法非常快,每次執行大約需要 6 毫秒,也就是能實現 100 倍加速。之前我還猜想這種提示方法會收益遞減,但顯然這種猜想並不合理。也許 numba 就是秘訣所在?

總體而言,這種迭代最佳化程式碼的迭代提示法並不完美:程式碼確實更好,但事後看來,「better」這個要求過於寬泛了。我只想要演算法改進,而不是完整的 SaaS。讓我們從頭開始再試一次,但這次會使用更多方向。

對 LLM 進行提示詞工程,以獲得還要更好的程式碼

現在是 2025 年,為了讓 LLM 得出最佳結果,仍然需要使用提示詞工程。實際上,提示詞工程的重要性還在提升:下一 token 預測模型的訓練目標是基於大批次輸入最大化下一 token 的預測機率,也因此它們是針對一般性的輸入和輸出進行了最佳化。隨著 LLM 的大幅改進,生成的輸出會變得更加平均化,因為這就是它們所接受的訓練:所有 LLM 都偏向平均水平。不過,僅需少量指導,明確說明你想要 LLM 做什麼,再給出幾個示例,LLM 的輸出就能提升。由於 Claude 3.5 Sonnet 能很好地遵從指令,因此即使只是一點點提示詞工程也能帶來很大的好處。

下面重做上面的程式碼最佳化實驗。這一次使用更加積極主動的提示詞工程,明確我們想要的結果,不給出任何模糊空間。是的,冷酷機械地對待 LLM 可以讓它們表現更好。

初始請求

這次我們將使用系統提示詞,僅透過 API 提供。系統提示詞列出了 LLM 必須遵從的「規則」。因為我想要更最佳化的程式碼,我們將在規則中定義這一點,並提供詳細示例:
圖片
中文版如下:

你編寫的所有程式碼都必須完全最佳化。

「完全最佳化」包括:

- 最大化記憶體和執行時間的演算法 big-0 效率
- 在適當的情況下使用並行化和向量化
- 遵循程式碼語言的正確樣式約定(例如,最大化程式碼複用 (DRY))
- 除了解決使用者問題絕對必要的程式碼之外,沒有額外的程式碼(即沒有技術債)

如果程式碼未完全最佳化,你將被罰款 100 美元。

關於最後一行:在系統提示詞中向 LLM 提供正面 / 負面激勵已不再常用,我自己的研究表明,尚不清楚它是否能否產生積極影響,但罰款在迭代提示中將變得更加有用。

初始使用者提示詞還有一行附加內容:
圖片
中文版:

編寫 Python 程式碼來解決這個問題:

給定一個包含 100 萬個隨機整數的列表,這些整數的取值範圍是 1 到 100,000,找出各位數總和為 30 的最小數和最大數之間的差值。

在編寫程式碼之前,規劃出所有必要的最佳化。

「規劃」是一個很常用的技巧,可以幫助 LLM 在第一次透過時對齊輸出 —— 算是「let’s think step by step」的現代版本。某些 LLM 會忽視它,但 Claude 3.5 Sonnet 總是會服從。

基於此提示詞工程策略的未經編輯的完整對話執行緒可見這裡:https://github.com/minimaxir/llm-write-better-code/blob/main/python_30_prompt_engineering.md

將系統提示詞和此使用者提示詞傳送給 Claude,會得到如下開頭的響應:
圖片
看起來很有希望:Claude 立即發現它應該從 numpy 和 numba 開始,並使用整數運算來求和。初始實現:
圖片
這比之前的初始實現更加精簡,尤其是沒有不必要的註釋。但仍有改進空間,特別是此實現未在 jit decorator 中設定 parallel=True。

如此簡單的提示詞工程就得到了平均執行時長僅 11.2 毫秒的程式碼,這比原始實現快 59 倍。厲害!

第 1 次迭代

現在使用比「write code better」更詳細的提示來迭代程式碼:
圖片
中文版:

你的程式碼未完全最佳化,你已被罰款 100 美元。使其更加最佳化。

不,這不是煤氣燈效應,這叫激勵式對齊(incentive alignment)

Claude 的響應如下:
圖片
很好,它找到了 parallel=True!但是那個求和實現很奇怪,並且使用了位移位(bit-shifting),這通常僅用於超最佳化(hyperoptimization)。我認為應該不能很好地對十進位制數使用位移位,而且經過測試證明確實不能,因為這個實現得到的和是錯誤的。該實現還再次包括多處理分塊方法(未展示出來),這可能對 numba 來說更加冗餘並會導致額外的開銷。同樣未展示出來的是:該指令碼還使用一個小的測試陣列預編譯 JIT 函式以獲得更好的實際效能,這是 numba 文件推薦用於基準測試的。

儘管如此,與最初的提示詞工程實現程式碼相比,這個程式碼的效能大大退步,現在僅比原始實現快 9.1 倍。可能的原因是多處理會產生新的程序,而這些程序每次都會重新編譯 numba JIT 函式,因此會產生巨大的開銷。

第 2 次迭代

再次迭代上述提示詞:
圖片
Claude 現在開始使用 SIMD 操作和塊大小調整來實現(理論上)極致效能。

此時,我很困惑,它錯過了位移位實現的某些東西,因為它仍然是錯誤的,特別是現在十六進位制數也參與進來了。事實證明,該實現是一種計算十六進位制數而非十進位制數的和的最佳化方法,因此它完全是一種幻覺。還有另一個非常微妙的幻覺:當 parallel=True 時,prange 函式無法接受 32 的步長,這是一個幾乎沒有文件記錄的細微差別。設定 parallel=False 並進行基準測試,與最初的提示詞工程實現相比,確實略有改進,比基本實現快 65 倍。

第 3 次迭代

又一次迭代:
圖片
在這種情況下,LLM 放棄了一直導致問題的分塊策略,並增加了兩個最佳化:全域性 HASH_TABLE(這只是一個 numpy 陣列,我不確定簡單的索引查詢是否算作雜湊表);另外它引入了一個邏輯微最佳化,即在求和後,如果數字超過 30,則可以停止計數,因為它可以立即被認定為無效。

一個主要問題:由於網際網路文件很少,導致「在模組載入時生成雜湊表」技巧實際上不起作用:numba 的經過 JIT 處理後的函式之外的物件是隻讀的,但 HASH_TABLE 仍在經過 JIT 處理後的函式之外例項化並在經過 JIT 處理後的函式內進行修改,因此會導致非常令人困惑的錯誤。經過微小的重構,使得 HASH_TABLE 在經過 JIT 處理後的函式內例項化,程式碼就可以工作,並且執行速度極快:比原始基本實現快 100 倍,與基礎提示方法的最終效能相同,但程式碼量少了幾個數量級。

第 4 次迭代

這時候,Claude 實際上已經在抱怨了,表示該程式碼已經達到了「該問題可能的理論最小時間複雜度」。所以我把問題混在一起,讓其解決這個求和問題,而它僅僅是使用之前用過的整數實現來替換相關程式碼,並沒有嘗試修復 HASH_TABLE。更重要的是,透過 HASH_TABLE 調整,我終於確認了該實現是正確的,不過由於不再進行位移,其效能略有下降:現在速度提升是 95 倍。

為了實現更好 LLM 程式碼生成
還要哪些步驟

綜合起來,我們用一張圖將這些提升直觀地展示出來吧,其中尤其強調了需要人來修改程式碼邏輯,以便消除 bug 讓程式碼可執行的案例。
圖片
總之,要求 LLM 「write code better」確實能讓程式碼變得更好,這取決於你對更好的定義。使用一般的迭代式提示方法,程式碼確實會在基礎示例的基礎上獲得提升,不管是新增功能還是提升速度。而如果使用提示詞工程,程式碼的提升會更加快速和穩定,但更可能引入一些微妙的 bug,因為 LLM 本就不是為了生成高效能程式碼創造的。當然,你在使用 LLM 可能會有不一樣的歷程,但最終你都需要人力介入,解決一些不可避免的問題。

本文中的所有程式碼(包括基準測試指令碼和資料視覺化程式碼,全都已經在 GitHub 釋出:https://github.com/minimaxir/llm-write-better-code/

另一方面,我很驚訝 Claude 3.5 Sonnet 在兩次實驗中都沒有發現和實施一些最佳化。也就是說,它沒有探索統計角度:由於我們從 1 到 10 萬的範圍內均勻生成 100 萬個數字,因此將有大量重複的數字永遠不需要分析。LLM 沒有嘗試進行重複資料刪除,例如將數字列表轉換為 Python set () 或使用 numpy 的 unique ()。我還期待一個實現,它涉及按升序對 100 萬個數字的列表進行排序:這樣,演算法就可以從頭到尾搜尋列表以查詢最小值(或從尾到頭搜尋最大值),而無需檢查每個數字,儘管排序很慢,而向量化方法確實更實用。

即使 LLM 可能會出錯,我從這些實驗中學到的一件值得注意的事情是:即使程式碼輸出不能直接使用,它們確實有有趣的想法和工具建議。例如,我從未接觸過 numba,因為作為一名資料科學家 / 機器學習工程師,如果我需要更好的程式碼效能,我習慣於專門使用 numpy 的技巧。但現在我很難不接受 numba JIT 函式的結果,我可能會將它新增到我的工具箱中。當在其他技術領域(如網站後端和前端)測試類似的「使其更好」提示迭代工作流程時,LLM 也有很好的想法。

當然,這些 LLM 不會很快取代軟體工程師,因為人們需要強大的工程背景才能識別出哪些才是真正的好主意,以及存在其他特定領域的約束。即使網際網路上有大量的程式碼,LLM 也無法在沒有指導的情況下辨別出普通程式碼和效能良好的高效能程式碼。現實世界的系統顯然比求職面試式的程式設計問題要複雜得多,但如果快速的 for 迴圈反覆要求 Claude 實現一個功能,提供可以將程式碼速度提高 100 倍的能力,那麼新出現的管道就物有所值。

有些人認為過早最佳化是一種糟糕的編碼習慣,但在現實世界中,這比擁有一個隨著時間的推移會成為技術債務的低於標準的實現要好。

不過必須要說的是,我的實驗使用 Python 對程式碼改進進行基準測試,而 Python 並不是開發者在追求最佳化效能時考慮的編碼語言。雖然 numpy 和 numba 等庫利用 C 來解決 Python 的效能限制,但流行的 Python 庫(如 polars 和 pydantic)使用的一種現代方法是使用 Rust 進行編碼。與 C 相比,Rust 具有許多效能優勢,而 PyO3 包允許在 Python 中使用 Rust 程式碼,並且開銷最小。

我可以確認,儘管該工作流程非常新,但 Claude 3.5 Sonnet 已經可以生成符合 PyO3 的 Python 和 Rust 程式碼,不過這部分的內容足以寫另一篇部落格文章了。

與此同時,雖然要求 LLM 改進程式碼是 AI 更務實的用途,但你可以要求他們「再加把勁」…… 結果好壞參半。

對於以上使用 LLM 的操作,我專門使用了 API 或這些 API 的介面(例如 Claude 的 Anthropic Console 中的 Workbench)作為免費 LLM 的 Web 介面。相比之下普通的 ChatGPT/Claude 網路應用使用管道,由於其固有的複雜性,會產生不可預測的結果。請注意這點。

你覺得這篇文章介紹的迭代式提示程式碼最佳化方法具有實際應用價值嗎?請與我們分享你的看法。

參考連結:
https://x.com/minimaxir/status/1874875826401132775
https://minimaxir.com/2025/01/write-better-code/

相關文章