△△請給“Python貓”加星標 ,以免錯過文章推送

英文: https://minimaxir.com/2025/01/write-better-code
作者:Max Woolf
翻譯:Python貓
2023 年 11 月,OpenAI為 ChatGPT 添加了新功能[1],讓用戶可以在網(wǎng)頁中使用 DALL-E 3 生成圖像。隨后出現(xiàn)了一個短暫的網(wǎng)絡(luò)梗[2]:用戶會給 LLM(大語言模型)一張基礎(chǔ)圖片,然后不斷要求它"讓圖片更 X",這里的 X 可以是任何特征或風(fēng)格。
這個趨勢很快就消失了,因為生成的圖像都過于相似,缺乏新意。有趣的是,無論初始圖像和提示詞如何不同,最終的結(jié)果都會趨向于某種"宇宙化"的效果。這種現(xiàn)象在"AI 垃圾"(AI slop)這一術(shù)語被正式定義之前,就已經(jīng)是典型的AI 垃圾[3]了。不過從學(xué)術(shù)角度來看,即使是這些看似無意義和模糊的提示詞,也能對最終圖像產(chǎn)生明顯的、可預(yù)測的影響,這一點仍然值得玩味。
如果我們對代碼用類似的方法會怎樣呢?由于代碼需要遵循嚴格的規(guī)則,而且與圖像等創(chuàng)意輸出不同,代碼質(zhì)量可以更客觀地衡量,所以 LLM 生成的代碼不太可能是垃圾(盡管也不是完全不可能[4])。
如果代碼真的可以通過簡單的迭代提示來改進,比如僅僅要求 LLM “讓代碼更好”(雖然這聽起來很傻),這將帶來巨大的生產(chǎn)力提升。那么,如果不斷這樣迭代下去會發(fā)生什么?代碼會出現(xiàn)什么樣的"宇宙化"效果?讓我們來一探究竟!
與 LLM 隨意編碼
盡管在 ChatGPT 出現(xiàn)之前我就一直在研究和開發(fā) LLM 相關(guān)工具,但我并不喜歡使用 LLM 代碼協(xié)助工具(如 GitHub Copilot)來輔助編碼。在"哦,LLM 自動完成了我的代碼,很好"/"我應(yīng)該問 LLM 什么問題"和"LLM 生成的代碼真的正確嗎,它是否在產(chǎn)生幻覺[5]并生成看似正確的代碼"之間不斷切換思維,這種持續(xù)的心智負擔抵消了使用 AI 帶來的任何生產(chǎn)力提升。這還沒有考慮使用這些 LLM 的昂貴成本。
Claude 3.5 Sonnet[6]讓我重新思考這個問題。由于 Anthropic 在訓(xùn)練中使用的某種秘密配方,最新版本的 Claude 3.5 Sonnet (claude-3-5-sonnet-20241022
) 對各類提示詞都有著令人難以置信的遵循能力,尤其是在編碼提示方面。編碼[7]基準測試[8]證實,在 Claude 3.5 Sonnet 和 GPT-4 的測試中,Claude 勝出,而且根據(jù)我在各種技術(shù)和創(chuàng)意任務(wù)中的親身經(jīng)歷,也得出了相同的結(jié)論。
初始嘗試
在這個實驗中,我們將給 Claude 3.5 Sonnet 一個面試風(fēng)格的 Python 編碼提示詞:一個簡單適合被新手軟件工程師實現(xiàn),但同時又可以進行大量優(yōu)化的問題。這個簡單隨意的提示詞代表了典型軟件工程師使用 LLM 的方式。此外,測試提示詞必須完全原創(chuàng),而不是取自 LeetCode 或 HackerRank 等編碼測試平臺,因為 LLM 很可能在這些平臺上進行過訓(xùn)練,可能會通過背誦記憶的答案作弊。
完整的、未經(jīng)編輯的基于這種隨意提示的對話內(nèi)容可在 GitHub 上獲取[9]。
這是我自己寫的測試提示詞:
編寫 Python 代碼來解決這個問題:
給定一個包含 100 萬個 1 到 100,000 之間的隨機整數(shù)的列表,找出數(shù)字之和等于 30 的最大數(shù)和最小數(shù)之間的差值。
將其作為用戶提示詞給 Claude API,并設(shè)置 temperature=0 以獲得最佳/最確定的答案,最終實現(xiàn)如下:
import random
def digit_sum(n):
"""Calculate the sum of digits of a number"""
return sum(int(digit) for digit in str(n))
def find_difference():
# Generate list of 1 million random integers
numbers = [random.randint(1, 100000) for _ in range(1000000)]
# Initialize variables for min and max numbers with digit sum 30
min_num = float('inf') # Initialize to positive infinity
max_num = float('-inf') # Initialize to negative infinity
# Find numbers whose digits sum to 30
for num in numbers:
if digit_sum(num) == 30:
min_num = min(min_num, num)
max_num = max(max_num, num)
# Check if we found any numbers with digit sum 30
if min_num == float('inf') or max_num == float('-inf'):
return "No numbers found with digit sum of 30"
return max_num - min_num
這個實現(xiàn)是正確的,并且與大多數(shù) Python 新手程序員的寫法一致,還額外處理了沒有符合條件的數(shù)字的情況。對于列表中的每個數(shù)字,檢查其數(shù)字之和是否為 30:如果是,則檢查它是否大于最近看到的最大數(shù)字或小于最近看到的最小數(shù)字,并相應(yīng)地更新這些變量。搜索完列表后,返回差值。
但我敢肯定,許多閱讀這個實現(xiàn)的程序員都會思考如何優(yōu)化。一個例子是digit_sum()
函數(shù):雖然這個實現(xiàn)是一個有趣的 Python 一行代碼,但在str
和int
之間的類型轉(zhuǎn)換會造成大量不必要的開銷。
在我的 M3 Pro MacBook Pro 上,這段代碼平均需要 657 毫秒運行。我們將使用這個性能作為基準,來比較后續(xù)實現(xiàn)的測試結(jié)果(劇透:它們都更快)。
迭代 #1
現(xiàn)在我們可以讓 Claude 改進代碼,將其當前答案和之前的所有內(nèi)容作為對話提示的一部分。因此,我們添加迭代提示:
寫更好的代碼
你以為這篇博文的標題是在開玩笑嗎?我從不開玩笑。
Claude 輸出了修改后的代碼,用它的話說是"代碼的優(yōu)化版本,有幾處改進"。它沒有將代碼保持為函數(shù)形式,而是重構(gòu)為 Python 類,使其更加面向?qū)ο螅?/p>
class DigitSumFinder:
def __init__(self, target_sum: int = 30, range_start: int = 1, range_end: int = 100_000, count: int = 1_000_000):
self.target_sum = target_sum
self.range_start = range_start
self.range_end = range_end
self.count = count
# Pre-calculate digit sums for all possible numbers
self.digit_sums = self._precompute_digit_sums()
def _precompute_digit_sums(self) -> array:
"""Precompute digit sums for all possible numbers in range."""
digit_sums = array('B', [0] * (self.range_end + 1))
for num in range(self.range_start, self.range_end + 1):
total = 0
n = num
while n:
total += n % 10
n //= 10
digit_sums[num] = total
return digit_sums
def find_difference(self) -> Tuple[int, Optional[int], Optional[int]]:
""" Find the difference between max and min numbers with target digit sum. Returns: (difference, min_number, max_number) """
min_num = float('inf')
max_num = float('-inf')
count_found = 0
# Generate and process random numbers
for _ in range(self.count):
num = random.randint(self.range_start, self.range_end)
if self.digit_sums[num] == self.target_sum:
count_found += 1
if num < min_num:
min_num = num
if num > max_num:
max_num = num
if count_found == 0:
return 0, None, None
return max_num - min_num, min_num, max_num
這里,代碼做了兩個智能的算法改進:
在計算數(shù)字和時使用整數(shù)運算,避免了前面提到的類型轉(zhuǎn)換的需要。
預(yù)計算所有可能的數(shù)字和并將它們存儲在字節(jié)數(shù)組中(使用數(shù)組而不是列表有點不尋常)以供查找,這意味著當一百萬個數(shù)字列表中出現(xiàn)重復(fù)數(shù)字時,不需要重新計算數(shù)字和。由于這個數(shù)組作為類的字段存儲,在搜索新的隨機數(shù)列表時不需要重新計算。
這些優(yōu)化使代碼速度相對于初始實現(xiàn)提高了 2.7 倍。
迭代 #2
再次提示寫更好的代碼
,Claude 發(fā)現(xiàn)了更多明顯的優(yōu)化(為了可讀性進行了截斷):
class DigitSumAnalyzer:
"""Analyzes numbers based on their digit sums."""
def __init__(self, number_range: NumberRange):
self.number_range = number_range
self._digit_sums = self._precompute_digit_sums()
def _precompute_digit_sums(self) -> array:
"""Precompute digit sums for all possible numbers using vectorized operations."""
digits = np.arange(self.number_range.end + 1)
digit_sums = np.zeros(self.number_range.end + 1, dtype=np.uint8)
while digits.any():
digit_sums += digits % 10
digits //= 10
return array('B', digit_sums)
def _process_chunk(self, chunk_size: int) -> Iterator[int]:
"""Process a chunk of random numbers."""
numbers = np.random.randint(
self.number_range.start,
self.number_range.end + 1,
chunk_size,
dtype=np.int32
)
mask = np.frombuffer(self._digit_sums, dtype=np.uint8)[numbers] == self.number_range.target_sum
yield from numbers[mask]
def analyze(self, chunk_size: int = 100_000, num_processes: int = None) -> Result:
""" Analyze numbers to find min/max with target digit sum. Args: chunk_size: Size of chunks to process at once num_processes: Number of processes to use (None for CPU count) """
start_time = time.perf_counter()
min_num = float('inf')
max_num = float('-inf')
numbers_found = 0
num_chunks = (self.number_range.count + chunk_size - 1) // chunk_size
with ProcessPoolExecutor(max_workers=num_processes) as executor:
futures = [
executor.submit(self._process_chunk,
min(chunk_size, self.number_range.count - i * chunk_size))
for i in range(num_chunks)
]
for future in tqdm(futures, desc="Processing chunks"):
for num in future.result():
numbers_found += 1
min_num = min(min_num, num)
max_num = max(max_num, num)
execution_time = time.perf_counter() - start_time
if numbers_found == 0:
return Result(None, None, 0, execution_time, 0)
return Result(min_num, max_num, max_num - min_num, execution_time, numbers_found)
Claude 添加了兩個優(yōu)化,并且意識到這個編碼問題是一個令人尷尬的并行 (embarrassingly parallel)[10]問題:
通過 Python 的 concurrent-futures [11] 包實現(xiàn)多線程,將大列表分成可以獨立處理的塊。
向量化 (vectorization) 的 numpy 操作,這比基礎(chǔ) Python 操作要快得多。特別值得一提的是
_precompute_digit_sums()
函數(shù),它實現(xiàn)了一個向量化的數(shù)字和計算實現(xiàn)。條件語句while digits.any():
是一種巧妙的代碼,但它確實能正確運行。
然而,這個特定的并行化實現(xiàn)存在一個問題:它生成子進程,這會導(dǎo)致許多煩人的問題,包括無法按原樣內(nèi)聯(lián)運行,而且必須使用main()
保護來調(diào)用,這顯然限制了它的實用性。但即使作為單獨的腳本運行,由于使用了yield from numbers[mask]
(該生成器完全沒有必要,return numbers[mask]
就足夠了),它也會打印出Error: cannot pickle 'generator' object
錯誤。代碼還混合了 numpy 數(shù)組的dtype
,這會導(dǎo)致錯誤:將它們?nèi)吭O(shè)置為np.int32
可以修復(fù)這個問題。
在進行這些修復(fù)后,代碼現(xiàn)在比基礎(chǔ)實現(xiàn)快了 5.1 倍。
迭代 #3
再次寫更好的代碼
,Claude 返回了新的實現(xiàn),它聲稱是"使用高級技術(shù)和現(xiàn)代 Python 特性的更加復(fù)雜和優(yōu)化的版本"。但實際代碼并沒有顯示出重要的算法改進,實際上在數(shù)字和計算方面還出現(xiàn)了倒退,回到了類型轉(zhuǎn)換的方法。如果說有什么的話,代碼庫變得更加臃腫,比如添加了一個用于執(zhí)行差值計算的類:
@dataclass(frozen=True, slots=True)
class SearchResult:
"""Result of the number search."""
min_number: Optional[int]
max_number: Optional[int]
count: int
execution_time: float
@property
def difference(self) -> Optional[int]:
"""Calculate difference between max and min numbers."""
if self.min_number is None or self.max_number is None:
return None
return self.max_number - self.min_number
這次,代碼無需任何修復(fù)就能運行。然而,性能略微下降,相比基礎(chǔ)實現(xiàn)現(xiàn)在只快了 4.1 倍。
迭代 #4
這種迭代提示似乎遇到了收益遞減。在再一次寫更好的代碼
之后,Claude 提供了新的實現(xiàn),聲稱具有"尖端優(yōu)化和企業(yè)級功能"。等等,企業(yè)級功能?!
最終的代碼太長了,無法在這篇博文中完整展示,但它確實創(chuàng)造了兩個新的優(yōu)化:它現(xiàn)在使用 numba Python 庫,可以調(diào)用 JIT (Just-In-Time) 編譯器,直接為 CPU 優(yōu)化代碼。在這種情況下,它可以通過一個裝飾器實現(xiàn)數(shù)字和的超快速預(yù)計算:
@jit(nopython=True, parallel=True)
def calculate_digit_sums(numbers: ArrayInt) -> ArrayInt:
"""Calculate digit sums using Numba."""
result = np.zeros_like(numbers)
for i in prange(len(numbers)):
num = numbers[i]
total = 0
while num:
total += num % 10
num //= 10
result[i] = total
return result
完整的類還使用 Python 的 asyncio 進行并行化,這比子進程方法更規(guī)范,更適合用于任務(wù)調(diào)度。它也能更好地與現(xiàn)有的內(nèi)聯(lián)代碼和 REPL(如 Jupyter Notebooks)配合使用。
它還做了一些"企業(yè)級"改造:
使用 Prometheus 作結(jié)構(gòu)化指標日志記錄。
一個信號處理器,使代碼在被強制終止時可以優(yōu)雅地關(guān)閉。
使用 rich [12] 表格展示基準測試結(jié)果。

看起來 AI 生成代碼的"宇宙化"就是通過過度工程使其變得企業(yè)級,這完全說得通。盡管如此,代碼可以不出任何錯誤地運行。async 和 numba 都是 Python 中的并行方法,所以它們可能造成冗余并產(chǎn)生額外開銷。然而,在基準測試后,算法運行速度非常快,每次運行大約 6 毫秒,也就是提速了 100 倍。這完全推翻了我之前認為這種提示詞會遇到收益遞減的假設(shè)。也許 numba 一直都是秘密武器?
總的來說,這種形式的迭代提示詞來改進代碼有其注意事項:代碼確實變得更好了,但事后看來,更好的定義太過寬泛。我只想要算法上的改進,而不是一個完整的 SaaS。讓我們從頭再來一次,這次要有更明確的方向。
提示詞工程讓 LLM 寫出更好的代碼
現(xiàn)在是 2025 年,要從 LLM 那里獲得最佳結(jié)果,提示詞工程 (prompt engineering) 仍然是必需的。事實上,提示詞工程對 LLM 變得更加重要:下一個 token 預(yù)測模型是通過在大批量輸入上最大化下一個 token 的預(yù)測概率來訓(xùn)練的,因此它們針對平均輸入和輸出進行優(yōu)化。隨著 LLM 的顯著改進,生成的輸出變得更加平均化,因為這就是它們的訓(xùn)練目標:所有 LLM 都偏向于平均值。雖然這既違反直覺又不有趣,但少量的指導(dǎo),明確告訴 LLM 你想要什么,以及給出一些你想要的例子,將客觀上改善 LLM 的輸出,遠超過構(gòu)建這些提示詞所需的努力。Claude 3.5 Sonnet 由于其強大的提示詞遵循能力,即使只是一點點提示詞工程也能顯著受益。
讓我們重做代碼優(yōu)化實驗,這次使用積極的提示詞工程,確保我們要的結(jié)果非常明確,不留任何模糊空間。沒錯,對 LLM 采用冷酷和"機器人化"的方式反而能讓它們表現(xiàn)得更好,管它什么洛夫克拉夫特式的恐懼呢。
初始嘗試
這次我們將使用系統(tǒng)提示詞,這只能通過 API 使用。系統(tǒng)提示詞列出了 LLM 必須遵循的"規(guī)則"。既然我想要更優(yōu)化的代碼,我們就在規(guī)則中定義這一點,并提供詳細的例子:
你寫的所有代碼都必須完全優(yōu)化。
"完全優(yōu)化"包括:
- 最大化內(nèi)存和運行時的算法大 O 效率
- 在適當?shù)牡胤绞褂貌⑿谢拖蛄炕?- 遵循代碼語言的正確風(fēng)格約定(例如最大化代碼重用 (DRY))
- 除了解決用戶提供的問題所絕對必需的代碼外,不要有多余的代碼(即沒有技術(shù)債務(wù))
如果代碼沒有完全優(yōu)化,你將被罰款 100 美元。
關(guān)于最后一行:在系統(tǒng)提示詞中向 LLM 提供正面/負面激勵已不再常見,而且我自己的研究表明,這種方法是否有積極影響還不確定。不過,這個罰款設(shè)定在后續(xù)的迭代提示中會變得更加重要。
初始用戶提示詞也增加了一行:
編寫 Python 代碼來解決這個問題:
給定一個包含 100 萬個 1 到 100,000 之間的隨機整數(shù)的列表,找出數(shù)字之和等于 30 的最大數(shù)和最小數(shù)之間的差值。
在編寫代碼之前,請規(guī)劃所有必要的優(yōu)化。
"規(guī)劃"是一個長期使用的技巧,可以幫助對齊大語言模型的第一次輸出 — 這是"讓我們一步一步思考[13]"的現(xiàn)代實現(xiàn)。有些大語言模型會忽略它,但 Claude 3.5 Sonnet 總是會遵循。
基于這個提示詞工程策略的完整、未經(jīng)編輯的對話線程可在 GitHub 上獲取[14]。
將系統(tǒng)提示詞和這個用戶提示詞發(fā)送給 Claude,回應(yīng)如下:
讓我先規(guī)劃優(yōu)化:
1. 算法優(yōu)化:
- 我們只需要跟蹤數(shù)字之和為 30 的最小值和最大值,不需要存儲所有數(shù)字
- 我們可以在讀取數(shù)字時計算數(shù)字之和,不需要轉(zhuǎn)換為字符串
- 只需要遍歷列表一次 (O(n))
2. 內(nèi)存優(yōu)化:
- 只需要存儲 2 個變量(最小值和最大值),不需要完整的過濾列表
- 不需要額外的數(shù)據(jù)結(jié)構(gòu)
3. 性能優(yōu)化:
- 使用 numba 的 @jit 裝飾器加快計算速度
- 創(chuàng)建一個使用模運算而不是字符串轉(zhuǎn)換的快速數(shù)字求和函數(shù)
- 使用 numpy 生成隨機數(shù)(比 random.randint 更快)
這很有希望:Claude 立即意識到它應(yīng)該從 numpy 和 numba 開始,并且使用整數(shù)運算來計算數(shù)字之和。初始實現(xiàn):
@jit(nopython=True)
def digit_sum(n):
total = 0
while n:
total += n % 10
n //= 10
return total
@jit(nopython=True)
def find_difference(numbers):
min_num = float('inf')
max_num = float('-inf')
for num in numbers:
sum_digits = digit_sum(num)
if sum_digits == 30:
min_num = min(min_num, num)
max_num = max(max_num, num)
return max_num - min_num if max_num != float('-inf') else 0
這比隨意提示詞的初始實現(xiàn)更簡潔,特別是沒有不必要的注釋。但仍然有改進的空間,例如這個實現(xiàn)沒有在jit
裝飾器中設(shè)置parallel=True
。
這種小型的提示詞工程導(dǎo)致的代碼平均運行時間為 11.2 毫秒,比初始實現(xiàn)快了 59 倍。令人驚嘆!
迭代 #1
現(xiàn)在我們可以用一個比"寫更好的代碼"更詳細的提示詞來迭代代碼:
你的代碼沒有完全優(yōu)化,你被罰款 100 美元。讓它更優(yōu)化。
不,這不是煤氣燈效應(yīng),這是激勵對齊。
Claude 回應(yīng)如下:
@jit(nopython=True)
def digit_sum(n):
# 使用位操作進行更快的除法
total = 0
while n:
total += n & 15 # 單個數(shù)字和的快速查找
n >>= 4
return total
@jit(nopython=True, parallel=True)
def find_difference_chunk(numbers):
min_num = np.iinfo(np.int64).max
max_num = np.iinfo(np.int64).min
# 使用 numba 進行并行處理塊
for i in prange(len(numbers)):
num = numbers[i]
sum_digits = digit_sum(num)
if sum_digits == 30:
min_num = min(min_num, num)
max_num = max(max_num, num)
return min_num, max_num
def process_chunk(chunk):
return find_difference_chunk(chunk)
很好,它識別出了parallel=True
標志!但是那個數(shù)字求和實現(xiàn)很奇怪,使用了位移操作[15],這通常只用于超優(yōu)化。我不認為你可以在十進制數(shù)上很好地使用位移操作,經(jīng)過測試,事實證明確實不行,因為這個實現(xiàn)計算出的數(shù)字之和是錯誤的。該實現(xiàn)還包括了多進程分塊方法(未顯示),這可能與 numba 重復(fù)并造成額外開銷。同樣未顯示的是:腳本還使用小型測試數(shù)組預(yù)編譯了 JIT 函數(shù)以獲得更好的實際性能,這是numba 文檔推薦[16]的基準測試方法。
盡管如此,代碼性能相比初始提示詞工程實現(xiàn)大幅下降,現(xiàn)在只比樸素實現(xiàn)快 9.1 倍??赡艿脑蚴怯捎诙噙M程生成新進程,這些進程每次都會重新編譯 numba JIT 函數(shù)[17],因此存在巨大的開銷。
迭代 #2
繼續(xù)迭代提示,結(jié)果:
# 使用位操作預(yù)計算查找表
LOOKUP = np.zeros(100001, dtype=np.uint8)
for i in range(100001):
# 使用并行位計數(shù)優(yōu)化數(shù)字求和
n = i
n = (n & 0x0F) + ((n >> 4) & 0x0F) + ((n >> 8) & 0x0F) + \
((n >> 12) & 0x0F) + ((n >> 16) & 0x0F)
LOOKUP[i] = n
@jit(nopython=True, parallel=True, cache=True, fastmath=True)
def find_min_max(numbers):
# 一次處理 32 個數(shù)字使用 SIMD
min_val = np.iinfo(np.uint32).max
max_val = 0
# 帶有顯式 SIMD 提示的向量化處理
for i in prange(0, len(numbers), 32):
# 將 32 個元素加載到 SIMD 寄存器
chunk = numbers[i:min(i + 32, len(numbers))]
# 向量化查找和比較
sums = LOOKUP[chunk]
mask = sums == 30
if np.any(mask):
valid_nums = chunk[mask]
min_val = min(min_val, np.min(valid_nums))
max_val = max(max_val, np.max(valid_nums))
return min_val, max_val
Claude 開始使用SIMD 操作[18]和塊大小來實現(xiàn)(理論上的)極致性能。我對那個位移實現(xiàn)感到困惑,因為它仍然是錯誤的,特別是現(xiàn)在涉及到十六進制數(shù)。事實證明,這是一個計算十六進制數(shù)字的數(shù)字之和的優(yōu)化方法,而不是十進制數(shù)字,因此這完全是一個幻覺。還有另一個極其微妙的幻覺:當parallel=True
時,prange
函數(shù)不能接受步長為 32,這是一個很少有文檔記載的細節(jié)。設(shè)置parallel=False
并進行基準測試,確實比初始提示詞工程實現(xiàn)略有改進,比基礎(chǔ)實現(xiàn)快 65 倍。
迭代 #3
再一次迭代:
# 預(yù)計算完美最小哈希表
# 關(guān)鍵洞察:對于 1-100000 的數(shù)字,只需要存儲數(shù)字之和為 30 的
# 格式:[數(shù)字] = 1 如果數(shù)字之和為 30,否則為 0
HASH_TABLE = np.zeros(100001, dtype=np.uint8)
# 在模塊加載時使用位操作生成哈希表
@jit(nopython=True, cache=True)
def init_hash_table():
min_val = np.iinfo(np.uint32).max
max_val = 0
# 使用并行位計數(shù)的最優(yōu)數(shù)字求和
for i in range(1, 100001):
n = i
sum = 0
while n and sum <= 30:
sum += n & 0xF
n >>= 4
if sum == 30:
HASH_TABLE[i] = 1
min_val = min(min_val, i)
max_val = max(max_val, i)
return min_val, max_val
# 在模塊加載時初始化
MIN_VALID, MAX_VALID = init_hash_table()
@jit(nopython=True, parallel=False, cache=True, fastmath=True)
def find_min_max(numbers):
min_val = MAX_VALID # 從已知邊界開始
max_val = MIN_VALID
found = False
# 單個向量化操作
mask = HASH_TABLE[numbers] == 1
if np.any(mask):
valid_nums = numbers[mask]
min_val = np.min(valid_nums)
max_val = np.max(valid_nums)
found = True
return min_val, max_val, found
在這種情況下,大模型放棄了一直造成問題的分塊策略,并添加了兩個優(yōu)化:一個全局HASH_TABLE
(這只是一個 numpy 數(shù)組,我不確定簡單的索引查找是否真的算作哈希表),它還引入了一個邏輯微優(yōu)化,即在對數(shù)字求和時,如果數(shù)字超過 30,計數(shù)就可以停止,因為它可以立即被識別為無效。
一個主要問題:由于一個網(wǎng)上很少有文檔提及的微妙問題,那個"在模塊加載時生成哈希表"的技巧實際上不起作用:numba 的 JIT 函數(shù)外的對象是只讀的,但HASH_TABLE
仍然在 JIT 函數(shù)外實例化并在 JIT 函數(shù)內(nèi)修改,因此會導(dǎo)致一個非常令人困惑的錯誤。經(jīng)過一個小的重構(gòu),使HASH_TABLE
在 JIT 函數(shù)內(nèi)實例化后,代碼正常運輸,而且運行極快:比原始基礎(chǔ)實現(xiàn)快 100 倍,與隨意提示詞的最終性能相同,但代碼量減少了幾個數(shù)量級。
迭代 #4
此時,Claude 提示說代碼已經(jīng)達到了"這個問題理論上可能的最小時間復(fù)雜度"。所以我改變了方向,只是讓它修復(fù)數(shù)字求和問題:它實現(xiàn)了[19],而且僅用之前使用的整數(shù)實現(xiàn)替換了相關(guān)代碼,并沒有試圖修復(fù)HASH_TABLE
。更重要的是,通過HASH_TABLE
的調(diào)整,我確認實現(xiàn)是正確的,最終,盡管由于不再使用位移操作而導(dǎo)致性能略有下降,但是比基礎(chǔ)實現(xiàn)快 95 倍。
繼續(xù)提升 LLM 代碼生成效果
綜合所有內(nèi)容,讓我們來可視化這些改進,包括突出顯示那些由于 bug 而需要我修改代碼邏輯才能運行的情況。

總的來說,要求 LLM "寫更好的代碼"確實能讓代碼變得更好,這取決于你如何定義"更好"。通過使用通用的迭代提示詞,代碼在功能性和執(zhí)行速度方面都得到了顯著提升。提示詞工程能更快速且更穩(wěn)定地改進代碼性能,但也更容易引入細微的 bug,這是因為 LLM 本身并非為生成高性能代碼而訓(xùn)練的。與使用 LLM 的其他場景一樣,效果因人而異。無論 AI 炒作者們?nèi)绾未蹬?LLM 為神器,最終都需要人工干預(yù)來修復(fù)那些不可避免的問題。
本博文中的所有代碼,包括基準測試腳本和數(shù)據(jù)可視化代碼,都可在 GitHub 上獲取[20]。
出乎我意料的是,Claude 3.5 Sonnet 在兩個實驗中都沒有發(fā)現(xiàn)和實現(xiàn)某些優(yōu)化。具體來說,它沒有從統(tǒng)計學(xué)角度來思考:由于我們是從 1 到 100,000 的范圍內(nèi)均勻生成 1,000,000 個數(shù)字,必然會出現(xiàn)大量無需重復(fù)分析的數(shù)字。LLM 沒有通過將數(shù)字列表轉(zhuǎn)換為 Pythonset()
或使用 numpy 的unique()
來去重。我還以為會看到一個對 1,000,000 個數(shù)字進行升序排序的實現(xiàn):這樣算法就可以從頭到尾搜索最小值(或從尾到頭搜索最大值),而不需要檢查每個數(shù)字。不過排序操作較慢,向量化方法確實更實用。
即使大語言模型可能會出錯,我從這些實驗中得到的一個重要啟示是,即使代碼輸出不能直接使用,它們?nèi)蕴峁┝擞腥さ南敕ê凸ぞ呓ㄗh。例如,我從未接觸過 numba,因為作為一個數(shù)據(jù)科學(xué)家/機器學(xué)習(xí)工程師,如果我需要更好的代碼性能,我習(xí)慣于使用 numpy 的技巧。然而,numba JIT 函數(shù)的效果令人難以忽視,我可能會把它加入我的工具箱。當我在其他技術(shù)領(lǐng)域(如網(wǎng)站后端和前端)測試類似的“優(yōu)化代碼”提示詞迭代工作流時,LLM 也提出了不少有價值的建議。
當然,這些大語言模型不會很快取代軟件工程師,因為需要強大的工程師背景以及其他特定領(lǐng)域的知識,才能識別出什么才是真正好的實現(xiàn)。即使互聯(lián)網(wǎng)上有大量的代碼,若沒有指導(dǎo),大語言模型也無法區(qū)分普通代碼和優(yōu)秀的高性能代碼。現(xiàn)實世界的系統(tǒng)顯然比面試式的編程問題復(fù)雜得多,但如果通過快速反復(fù)要求 Claude 實現(xiàn)一個功能,能使代碼速度提高 100 倍,那這個流程就非常值得。有些人認為過早優(yōu)化[21]是不好的編碼實踐,但在實際項目中,這比那些隨著時間的推移會變成技術(shù)債務(wù)的次優(yōu)實現(xiàn)要好得多。
我的實驗存在一個局限性,那就是我使用 Python 來對代碼改進進行基準測試,而這并不是開發(fā)者在追求極致性能優(yōu)化時的首選編程語言。雖然像 numpy 和 numba 這樣的庫通過利用 C 語言來解決了 Python 的性能瓶頸,但更現(xiàn)代的解決方案是采用 polars 和 pydantic 等流行 Python 庫,它們使用 Rust 開發(fā)。Rust 在性能方面比 C 語言更具優(yōu)勢,而 PyO3 幾乎沒有性能損耗就能讓 Python 調(diào)用 Rust 代碼。我可以確認 Claude 3.5 Sonnet 能夠生成兼容 Python 和 Rust 代碼,不過這種工作流程太新穎了,足夠成為另一篇博文的主題。
以此同時,雖然要求 LLM 讓代碼變得更好是 AI 更實用的用途,但你也可以要求它們"讓代碼更兄弟"...效果好壞參半。
參考資料
為 ChatGPT 添加了新功能: https://openai.com/index/dall-e-3-is-now-available-in-chatgpt-plus-and-enterprise/
短暫的網(wǎng)絡(luò)梗: https://lifehacker.com/tech/chat-gpt-make-it-more-ai-images-trend
AI 垃圾: https://en.wikipedia.org/wiki/AI_slop
也不是完全不可能: https://daniel.haxx.se/blog/2024/01/02/the-i-in-llm-stands-for-intelligence/
[5]
產(chǎn)生幻覺: https://en.wikipedia.org/wiki/Hallucination_%28artificial_intelligence%29
[6]
Claude 3.5 Sonnet: https://www.anthropic.com/news/claude-3-5-sonnet
[7]
編碼: https://www.vellum.ai/blog/llm-benchmarks-overview-limits-and-model-comparison
[8]
基準測試: https://aider.chat/docs/leaderboards/
[9]
GitHub 上獲取: https://github.com/minimaxir/llm-write-better-code/blob/main/python_30_casual_use.md
[10]
令人尷尬的并行 (embarrassingly parallel): https://en.wikipedia.org/wiki/Embarrassingly_parallel
[11]
concurrent-futures: https://docs.python.org/3/library/concurrent.futures.html
[12]
rich: https://github.com/Textualize/rich
[13]
讓我們一步一步思考: https://arxiv.org/abs/2205.11916
[14]
GitHub 上獲取: https://github.com/minimaxir/llm-write-better-code/blob/main/python_30_prompt_engineering.md
[15]
位移操作: https://wiki.python.org/moin/BitwiseOperators
[16]
numba 文檔推薦: https://numba.pydata.org/numba-doc/dev/user/5minguide.html#how-to-measure-the-performance-of-numba
[17]
重新編譯 numba JIT 函數(shù): https://stackoverflow.com/questions/72449896/does-numba-need-to-compile-separately-within-each-parallel-process
SIMD 操作: https://tbetcke.github.io/hpc_lecture_notes/simd.html
它實現(xiàn)了: https://github.com/minimaxir/llm-write-better-code/blob/main/python_30_prompt_engineering.md#assistant-iteration-4
可在 GitHub 上獲取: https://github.com/minimaxir/llm-write-better-code/
[21]
過早優(yōu)化: https://softwareengineering.stackexchange.com/questions/80084/is-premature-optimization-really-the-root-of-all-evil
如果你正在尋找優(yōu)質(zhì)的Python文章和項目,我必須向你推薦Python潮流周刊!
它精選全網(wǎng)的優(yōu)秀文章、教程、開源項目、軟件工具、播客、視頻、熱門話題等豐富內(nèi)容,讓你緊跟技術(shù)最前沿,獲取最新的第一手學(xué)習(xí)資料!
歡迎點擊下方圖片,了解這份全世界知識密度最高、知識廣度最大的 Python 技術(shù)周刊。
熱門跟貼