一個大型語言模型(LLM)出廠的樣子是這樣的:沒有記憶、不知道你的事、也不能動手。要讓它變得好用,最先要補的那個洞其實最不起眼:輸出不受控。
這是七層裡的第二層:結構與範例。它不改變模型本身,只改變你把話送進去的方式。而這一層有個很少被講清楚的事實:改變的幅度,比你以為的大很多。
對使用 LLM 的人來說,模型是一個黑盒子。 你看不到裡面,能動的只有送進去的字,能看的只有出來的字。所以這一層做的事,說穿了就是在黑盒子裡找確定性:從外面試出什麼樣的輸入,能換到你預期得到的輸出。
拿同一段程式碼,在 Claude 開兩個新對話,各問一次。這是要被 review 的程式碼,一個很常見的速率限制中介層:
import time
# 每個 IP 每分鐘最多 100 次
_hits: dict[str, list[float]] = {}
def allow(ip: str) -> bool:
now = time.time()
hits = _hits.setdefault(ip, [])
hits[:] = [t for t in hits if now - t < 60]
if len(hits) >= 100:
return False
hits.append(now)
return True
問法 A:
幫我 review 這段程式碼。
(貼上上面那段)
問法 B:
你是一位資深後端工程師,正在做上線前的 code review。
<context>
這是內部 API 的速率限制中介層,尖峰流量約每秒 200 個請求,多執行緒的 web server。
團隊的優先順序:正確性 > 可讀性 > 效能。
這段是新寫的,還沒上線。
</context>
<code>
(貼上上面那段)
</code>
<instructions>
1. 只指出「上線後會出事」的問題:正確性錯誤、併發問題、資源耗盡。
2. 每一項請寫成三行:問題是什麼 / 為什麼會出事 / 怎麼改。
3. 如果你認為沒有嚴重問題,就直接說沒有,不要為了湊數而列。
</instructions>
兩邊跑完並排看一次。下面是我自己跑的結果:同一個模型(Claude Opus 5),各開一個全新的工作階段,各跑一次,沒有挑、沒有重跑。回覆都很長(兩邊都附了一段改寫後的程式碼),這裡只節錄結構與關鍵句,文字一字未改。
A 的回覆(節錄):
嚴重
1. 記憶體只增不減(可被用來 DoS)
2. 不是 thread-safe
→ 加一把 threading.Lock(要求更細的話可以每個 IP 各一把鎖)。
3. 只在單一程序內有效
中等
4. time.time() 會受系統時鐘調整影響
5. IP 從哪裡來(這段程式碼沒有,但很關鍵)
小問題
6. 效能
7. Magic numbers
8. 行為上的取捨(確認是不是刻意的)
B 的回覆(節錄):
我找到 3 個上線後一定會出事的問題,另外 2 個要看部署方式。請求被擋時不記錄,這個滑動視窗寫法本身沒錯;每次請求最多掃 100 筆,每秒 200 次也不構成效能問題。
1. 併發時計數會錯(一定會出事)
怎麼改:用 threading.Lock 把整段包起來。每秒 200 次,一把全域鎖就夠,不必做成每個 IP 一把鎖。
2. 用 time.time() 算時間間隔(一定會出事)
3. _hits 只增不減,記憶體會一直長(一定會出事,速度看來源 IP 有多少)
4. 部署方式可能讓設計直接失效(上線前請確認)
所有人共用每分鐘 100 次,尖峰每秒 200 次,不到 1 秒就全部被擋,等於上線即停擺。
先講這段程式碼本身。它有兩個上線後才會痛的問題,兩個都看得出來,不需要跑模型就能確認:
_hits 永遠不會刪掉空掉的 IP 條目。 過期的時間戳會被濾掉,但那個 IP 的 key 會一直留在字典裡。被掃描或分散式來源打過一輪之後,這個字典只會長大,不會縮小,最後吃掉記憶體。allow() 對共用字典做了「讀取、修改、寫回」,卻沒有鎖。 在多執行緒的 web server 上,兩個請求可能同時通過 len(hits) >= 100 的檢查再各自 append,實際放行量會超過你設定的上限。我跑之前的預期是:A 只會給對任何一段 Python 都成立的建議,B 才比較有機會指到這兩個。結果我猜錯了一半:A 也兩個都抓到了,還另外指出 time.time() 會被校時影響、服務放在 proxy 後面會拿錯 IP。
所以差別不在「有沒有找到」,而在其他地方:
| 面向 | 問法 A | 問法 B |
|---|---|---|
| 列了幾項 | 8 項,連效能、magic numbers 都列進來 | 4 項,全部是「上線後會出事」的,並排除效能:「每秒 200 次也不構成效能問題」 |
| 要不要下決定 | 兩邊都講:加一把鎖,「要求更細的話可以每個 IP 各一把鎖」 | 用你給的數字下決定:「每秒 200 次,一把全域鎖就夠」 |
| 部署風險 | 放在 proxy 後面,所有人「共用 100 次」 | 直接算給你看:「尖峰每秒 200 次,不到 1 秒就全部被擋,等於上線即停擺」 |
模型夠強的時候,結構改變的不是它看不看得出問題,而是它拿你給的資訊做了哪些取捨。 A 不知道環境,所以每種可能都講、每個決定都兩邊押;B 知道,所以敢刪、敢下結論。順帶一提,兩邊都主動附上了改寫後的程式碼,B 的指示裡並沒有要求。
這是各跑一次的結果。同一個提示詞再跑一次,內容就可能不一樣(原因放在文末),所以這兩次結果是一個例子,不是證據。
一個直覺的解釋是「B 比較用心」,但那不是真正的原因。
真正的原因很單純:模型只看得到你送進去的東西。 它不知道這段程式碼跑在什麼環境、扛多少流量、是不是多執行緒、團隊在意正確性還是速度、有沒有上線。A 的問法裡,這些全部是空白,而它還是得講出些什麼。這次它的做法是全部列出來、每個決定都兩邊押:可能是多執行緒也可能不是,所以兩種鎖都提;可能在意效能也可能不在意,所以小問題也列。這不是模型偷懶,而是資訊不足時最安全的輸出:不做取捨。
review 這件事特別容易看出這個道理:你沒辦法在不知道意圖的情況下判斷一段程式碼寫得好不好。 同一段速率限制,如果它只是本機的小工具,上面那兩個問題根本不值得提;如果它要扛正式流量,那就是上線當天會被叫起來處理的東西。模型跟人一樣,沒有這個資訊,就不敢替你刪東西。
B 做的事其實只有三件:給它情境、指定格式、講明不要什麼。這三件事沒有一件跟「模型多聰明」有關,全部都是把你腦中已經有、但它沒有的東西寫下來。
到這裡為止,都還算符合直覺。接下來這件事就不是了。
2024 年一篇 ICLR 的論文(Sclar et al.)做了一件很乾淨的實驗:把提示詞的內容完全固定,連範例選哪幾個、照什麼順序放都鎖死,只改格式。改的是什麼?分隔符號、要不要空格、用什麼符號列點、大小寫。純粹是排版。
結果:在 53 個任務上,光是這些排版差異,準確率的落差中位數就有 7.5 個百分點。而且那只是從 10 種格式裡抽樣算出來的,作者說這是下限。極端情況下(單一模型的最大值)落差到過 76 個百分點。
我第一次看到這個數字的時候,直覺反應是「那一定是小模型才這樣」。不是。另一篇 2024 年的研究(Voronov et al.)測了 21 個模型,從 7.7 億到 700 億參數,結論是:一個不好的模板,可以把其中最強的模型壓到接近亂猜的水準。
看到上面那些數字,最自然的反應是:好,那告訴我哪種格式最好,我照抄。
問題就在這裡。同一批研究也證明了,沒有這種東西。 這正是黑盒子的代價:你量得到某個格式比較好,卻沒辦法從裡面解釋它為什麼好,所以也沒辦法保證它換個模型還成立。
Sclar 那篇特別檢查了「好格式能不能換個模型繼續用」,答案是幾乎不能:格式好壞的排名在不同模型之間的一致性只有 55% 到 61%,而隨機猜是 50%。Voronov 那篇講得更直接:最佳模板不跨模型轉移,同一個家族的模型之間也不行。
所以請不要從這篇文章帶走「要用某某符號當分隔線」這種結論。那不是這些研究說的話,而且下一版模型出來就可能翻盤。
這兩件事要一起記住,缺一不可:
「結構會改變輸出,而且幅度可能大到嚇人」是有實證的;
「所以你應該照這個格式寫」沒有。
這正是為什麼這個系列在後面會花一整篇談評估集。當你沒辦法靠通則決定格式的時候,剩下唯一可靠的方法就是針對你自己的任務,量一次。
這是我讀完上面那幾篇之後的第一個念頭:沒有最佳格式沒關係,我自己反覆調,總會調到好的吧?
可以調到好,但你得到的是「對這個模型、這個任務、這批測試題」好的格式,不是一個好格式。
原因有兩個,都在同一批論文裡:
說穿了,反覆調整真正在做的事是搜尋,而搜尋要有分數才搜得下去。後面會提到的 DSPy,就是把這件事自動化。
值得注意的是,兩篇論文給的解法都不是「找到最好的那一個」:Sclar 提出 FormatSpread,回報多種合理格式下的表現區間;Voronov 提出 Template Ensembles,把好幾個模板的答案合起來。兩個都接受了同一件事:單一格式靠不住,那就不要把賭注押在單一格式上。
這些研究測的是 700 億參數以下的開放權重模型,任務以分類與少樣本為主。它們證明了現象存在,但不能直接拿來推論今天的 Claude 敏感到什麼程度,那要你自己量。
有件事值得問:模型憑什麼聽你的「要求」?它不是在猜下一個字嗎?
因為那是被訓練出來的行為。2022 年的 InstructGPT(Ouyang et al.)用人類偏好去微調模型,標註者在他們的提示詞分布上比較兩邊的輸出,偏好微調後模型的比例是 85%,而且13 億參數的微調版本,贏過 1750 億參數的原版。
這個數字要小心解讀:它是偏好率,不是準確率,也不是「好了 85%」;對手換成有好好寫提示詞的原版模型時,比例會掉到 71%。論文自己也報告了代價,有些既有指標反而退步了。
但方向很清楚:「聽話」是一種被訓練出來的能力,不是模型天生就有。 你寫的每一條要求之所以有效,是因為有人花錢請人標註過「照做的回答比較好」。
Claude 的官方提示詞指南原本散在大約十頁,現在收斂成一頁最佳實務。裡面有幾條夠具體、可以直接照做:
<example> 裡,多個包在 <examples> 裡,讓模型分得出哪些是範例、哪些是指令。<instructions>、<context>、<input>,名稱一致,有層次就巢狀。system 參數裡,Claude 官方的說法是「一句話就有差別」,他們給的範例真的就只有一句:You are a helpful coding assistant specializing in Python.
官方另外提到「把問題放在最後,測試中最多可以提升 30% 的回答品質」。這句話值得寫下來,但要標清楚:那是廠商自己的說法,原文只寫 “in tests”,沒有交代實驗設計。 跟上面那幾篇同儕審查的研究不是同一個等級的證據,我自己會把它當成「值得一試的方向」而不是「已知的事實」。
把這些套回第一篇那個請求結構,B 的問法實際上長這樣:
{
"model": "claude-opus-5",
"max_tokens": 1024,
"system": "你是一位資深後端工程師,正在做上線前的 code review。只指出上線後會出事的問題。",
"messages": [
{ "role": "user", "content": "<context>內部 API 的速率限制中介層,尖峰約每秒 200 個請求,多執行緒 web server。優先順序:正確性 > 可讀性 > 效能。尚未上線。</context>\n<code>def allow(ip: str) -> bool: ...</code>\n<instructions>每一項寫成三行:問題是什麼/為什麼會出事/怎麼改。沒有嚴重問題就直說。</instructions>" }
]
}
第一篇說過「這個結構會跟著我們走完七層」。這就是第一次兌現:這一層加的東西,全部落在 system 欄位和 messages 的內容裡,沒有任何新的機制。你在 Claude 介面上打的每一句話,最後也是被塞進同一個地方。
順帶把界線講清楚,因為這一篇兩種東西混在一起了:前面那些研究講的是通用現象,格式敏感度、模板的影響,換成任何一家的大型語言模型都會遇到;而 XML 標籤這套慣例、system 欄位的用法,是 Claude 自己的建議,換一家就得重看它自己的文件。
值得知道還有一派人的主張是:你根本不該手寫這串字。
DSPy(Stanford NLP,3.8 萬顆星)的立場寫在它的標語裡:用程式寫,而不是用提示詞寫。 你宣告輸入輸出的規格,由最佳化器去搜尋實際要送出去的字串。理由跟上面那些研究完全一致:既然人類挑的格式沒有理論保證、又不跨模型轉移,那就別讓人去挑。
我目前的看法是:小規模、需要人看得懂的任務,手寫還是比較快;一旦你開始需要在多個模型之間搬、或是要反覆調同一條流程,這個主張就變得很有說服力。這個系列暫時走手寫路線,但知道有這條路存在,比較不會把手寫當成唯一解。
第一,提示詞變成一個要維護的東西。 B 那段字,你下次還要再打一次;改了一個地方,你不會記得上次為什麼那樣寫。它從「一句話」變成了資產。
第二,你開始需要判斷「改了有沒有比較好」。 這一篇整篇都在講結構會影響結果,卻又說沒有通用的最佳格式。那唯一的出路就是量測。眼睛看兩三次是看不出來的:同一個提示詞跑兩次,答案本來就不會一模一樣,你看到的差別可能只是這一次的運氣。
第三,講太細反而會綁住它。 格式指定得越死,它越沒有空間給你想不到的好答案。這中間有一條線,而我目前的理解是:好答案和壞答案的分界,取決於你需要保證輸出符合哪些要求。 必須保證的,例如格式要能被程式讀進去、某幾類問題一定要檢查到,就寫死;不需要保證的,就留給它發揮。B 就是一個例子:三行的格式是寫死的,而它主動附上的改寫程式碼,沒有人要求,但也沒有人禁止。
還有一個很實際的成本:B 比 A 長很多,而長度是要付錢的。第一篇提過可以用 count_tokens 量一段文字的長度,這裡正好派上用場,把 A 和 B 各量一次,你會很有感。
既然同一個提示詞每次答案都不一樣,有人乾脆從推論端下手,讓黑盒子同樣的輸入、永遠同樣的輸出。這裡推薦兩個實驗性的專案:detLLM 是一位 ETH Zürich 資工碩士的個人開源專案,用來驗證推論能不能重現、在輸出分岔時留下重現資料;Thinking Machines Lab 的 batch_invariant_ops 則示範了在 vLLM 上做到確定性推論。兩者都需要模型跑在你自己控制的機器上,透過 API 用 Claude 的時候用不上。
但要記得:確定性保證的是「每次都一樣」,不是「每次都對」。 它讓黑盒子變得可重現,沒有讓它變得透明。所以在黑盒子外面,你真正握得住的還是兩件事:把輸入寫清楚,把輸出算清楚。
用範例教,比用形容詞教有效,但理由不是你想的那個。
這一篇講的是「把話講清楚」。下一篇講的是「直接給它看」,也就是少樣本(few-shot)。而那裡有兩個更反直覺的結果:把範例的標籤全部換成錯的,影響小到令人意外;但只是把同一組範例換個順序,分數卻可以從接近最佳掉到接近亂猜。
temperature 參數:官方寫明 0.0 也不會完全固定,而且 Claude Opus 4.6 之後的模型只接受 1.0。