最近我在應用 LLM 輔助軟體開發一事,做了一些實驗。實驗之後有想出一些 prompt engineering 的調校方向,勉強可以應對波坦金理解 (Potemkin Understanding)。
實驗主要有兩組:
應用 LLM 來除錯 (debug)。
應用 LLM 來生成程式碼 (code generation)。
Debug
實驗的方法如下:
準備幾組有錯誤的 code snippet
對每一組 code snippet 都用以下三組 prompt 來除錯:
除錯
用「橡皮鴨除錯法」除錯
列出所有的錯誤可能原因與驗證方法
得到的結果跟我想的基本上一致:最單純的 Debug 指令,視情況不同,也有一定的機率一次命中目標。而第二組與第三組,則可以提昇答案品質,在困難的除錯案例上,頗有應用價值。
換言之,在 Debug 的情況,可以說 prompt engineering 滿有效的。
Code Generation
這邊我實驗的案例並不是單純的 boilerplate code generation ,而是有一定難度的挑戰。
需求:給定一個 Lua cbor library ,要活用這個 cbor library 將一個 Lua table 做編碼,編碼時要先讓 table 裡的 key 前加上一個冒號,然後要在 key 之前加上 custom tag
39
。困難點:我使用的 Lua cbor library ,它的 README 寫得非常精簡。像上述的需求,算是比較靈活運用的 use case ,就算是人類都沒有辦法很快地瞬間理解該怎麼做,要腦筋轉一下。而 LLM 對事物的理解,都是流於表面的波坦金理解,自然是失敗得理所當然。
正確的答案如下 (以下是用 fennel 的程式碼來呈現)
(local cbor (require :org.conman.cbor))
;; 定義一個 keyword 函數,它可以用來註冊 __tocbor 函數
(fn keyword [s]
;; keyword changes the string $s => `:$s`
;; and return a table with the content is `{:v $changed_string}`
;; and the returned table has a `__tocbor` function in its metatable."
(let [t {:v (.. ":" s)}
mt {:__tocbor (fn [self]
(cbor.TAG._id self.v))}]
(setmetatable t mt)))
;; 宣告一個 msg,其中,它的 key 要用上面的函數修飾。
(local msg {(keyword "op") :eval (keyword "code") "(+ 1 1)"})
;; 將整個 msg 做 cbor 編碼,cbor.encode 會去呼叫 __tocbor 函數。
(cbor.encode msg)
正確的答案,我用了四組不同的 prompt 都未能抵達終點,然而,其中一組算是出乎意料之外地引用了 __tocbor
函數,至少在架構上做出了正確的形狀。(這邊在 prompt 時,暫時先沒有特別處理 key 之前要加上冒號的需求。)
第一組 prompt 如下:
考慮使用以下的 lua library https://github.com/spc476/CBOR 希望 lua.encode({op = "eval"}) 的 hex 結果是 "a1 d8 27 63 3a 6f 70 64 65 76 61 6c" 即,希望插入一個 tag 39 用來修飾 op 這個 keyword 。 該如何呼叫 lua.encode ?
第二組 prompt 對第一組 prompt 做了修改,加上
該 library 的 REAMDE 內容如下: $README
第三組 prompt 對第一組 prompt 做了修改,加上
該 library 的 github content 如下:$GITHUB
第四組 prompt 對第一組 prompt 做了修改,加上
生成答案時,除了以下的 REAEME 之外,也一併考慮: 與 Lua 最接近的程式語言,比方說 JavaScript ,JavaScript 的 cbor library 通常怎麼設計、怎麼使用最合理。 該 library 的 REAMDE 內容如下:$README
實驗的結果,是第四組 prompt 給出了最有啟發性的答案,即答案明確地使用了 __tocbor
函數,儘管依然還是錯的。
討論:prompt engineering 有效的關鍵在哪?
這邊來多思考一下 LLM 怎麼運作。
首先,在除錯實驗中的發現可以推論:有效的提示工程在於它能否成功地引導 LLM 進行更高品質的「自引用知識」(self-referenced knowledge)的檢索與應用。
當使用「橡皮鴨除錯法」時,強制了 LLM 進行多步驟的「自我對話」,這相當於要求它生成一條更長、更詳細的內部推理鏈 (Chain-of-Thought)。
當要求「列出所有的錯誤可能原因與驗證方法」時,則要求了 LLM 進行系統性的知識檢索與分類,這迫使它從多個維度去比對可能的錯誤模式。
上述這兩種提示工程策略之所以有效,可以解釋成它們讓 LLM 的運作方式改變:變成「可以檢索到其內部更多的相關資料,因而可以提昇答案的品質。」
另一方面,在程式碼生成上的實驗則揭示了 LLM 的一個根本限制:當解決方案需要深層次的、抽象的、甚至是反直覺的知識時,「波坦金理解的限制」便會暴露無遺。
在程式碼生成的實驗裡,『cbor.encode
會呼叫 __tocbor
函數』這個關鍵是一個非顯性的、需要推理的運作機制。人類工程師需要花時間閱讀文件,研究程式碼,或是基於對軟體設計普遍性原則的理解,才能將此一隱藏的模式與 cbor.encode
的行為建立起正確的關聯。
在程式碼生成的實驗裡,單純給 README (如何使用)、甚至是給了完整的 GITHUB content (如何使用與完整實作) 都無法讓 LLM 推理出 __tocbor
函數的重要性。另一方面,加上一句「類比推理」之後,似乎讓 LLM 檢索到其內部更多的相關資料,勉強算是讓 LLM 繞過了「波坦金理解的限制」,推理出了 __tocbor
函數的重要性。
結論
LLM 在當前階段並非真正理解,而是透過龐大語料的模式檢索來生成答案。prompt engineering 的本質,便是設計提問方式,引導模型在其知識空間中找到更接近「正確」的模式。對於需要深層機制推理的問題,跨領域類比(例如引入相近語言或相似框架的經驗)可以作為一種變通策略,讓模型藉由關聯檢索找出潛在的關鍵線索,可能是少數能勉強突破「波坦金理解」的可行路徑。