結構化輸出,是否影響 LLM 性能?
本文作者:Will Kurt,翻譯:寶玉
https://blog.dottxt.co/say-what-you-mean.html
本文反駁:構化生成,顯著影響了 LLM 的性能。
關於結構化輸出,我之前還寫了一篇:
最近,Appier 研究團隊發表了一篇論文 《讓我自由表達?關於格式限制對大語言模型性能影響的研究》(https://arxiv.org/abs/2408.02442),對大語言模型(LLM)在執行結構化生成任務時的評估結果質量提出了嚴重質疑。論文的最終結論是:
我們的研究表明,結構化生成限制顯著影響了 LLM 在各種任務上的性能。
論文基於三組評估結果得出這一結論,這些結果表明,與非結構化生成(圖表中的「自然語言」/NL)相比,結構化生成(圖表中的「JSON 模式」)表現更差。下圖(根據原圖表重繪並重新調整比例)展示了令人擔憂的性能表現:
我們 .txt 團隊在以往的實驗中發現 結構化生成優於非結構化生成。我們的實驗聚焦於具有清晰、適合 LLM 處理的結構化問題,而 Tam 等人研究的任務也是如此(實際上,我們早已用不同的模型對 GSM8K 進行了 類似實驗:https://blog.dottxt.co/performance-gsm8k.html)。因此,Tam 等人的研究結果既令人驚訝又令人擔憂。
在使用相同模型(Llama-3-8B-instruct)重新測試上述任務後,我們發現我們的結果與論文中的結果不一致,而是反映了我們此前的發現。深入研究論文的數據和源代碼後,我們發現存在幾個關鍵問題,導致論文得出了根本錯誤的結論。
這篇文章的目的不僅僅是反駁,而是分享我們在日常使用結構化生成中積累的經驗。我們將展示 Tam 等人犯下的錯誤,同時提供結構化生成提示技巧,即使在您 未使用結構化生成 的情況下,也能改善 LLM 的響應效果。
簡單反駁:結構化生成提升了性能
對於急需答案的讀者,關於結構化生成是否會降低性能的簡短回答是:明確不會。下圖顯示了我們針對論文中提到的所有問題進行了快速 JSON 生成實現後的結果。
雖然我們的非結構化生成結果與論文一致,但我們的結構化生成結果直接駁斥了論文的發現,顯示結構化生成在所有方面都有所改善。複現這些結果的代碼可在 GitHub 上找到:
https://github.com/dottxt-ai/demos/tree/main/say-what-you-mean
以下是我們在論文中發現的主要問題:
論文本身發現結構化生成在一些分類任務上的表現優於非結構化生成。
用於非結構化(NL)生成的提示與用於結構化生成的提示明顯 不同,因此無法進行公平比較。
結構化生成提示未向模型提供足夠信息來完成任務,這導致「JSON 模式」示例的性能尤其差。
論文的核心實際上是利用另一個 LLM 解析一個 LLM 的結果。作者將其稱為「完美文本解析器」,我們稱之為「AI 解析器」(原因稍後解釋)。
論文混淆了結構化生成與 JSON 模式,但獨立運行這些評估顯示,「JSON 模式」優於非結構化生成。
一個合適的比喻是編程語言的基準測試:如果用糟糕的 Rust 代碼和精心優化的 Python 代碼進行比較,很容易得出 Rust 性能不如 Python 的結論。然而,任何有理智的讀者都會意識到,這些結果更多反映了作者的技能水平,而不是工具的能力。優化性能的代碼本身是一項挑戰,就像確保評估真實反映任務一樣。
儘管如此,這篇論文為我們深入研究結構化生成的意義和如何提升其性能提供了絕佳機會。
任務概述 – 最後一個字母
我們將聚焦於《讓我自由表達?》聲稱結構化生成表現最差的任務之一:最後一個字母(https://paperswithcode.com/dataset/lastletterconcat)。
在此任務中,模型需要處理如下輸入:一個包含 4 個名字的列表,例如:
Ian Peter Bernard Stephen
然後模型必須將每個名字的 最後一個字母 串聯起來。例如,答案為:NRDN。
測試集包含 150 個問題,訓練集包含 350 個問題。論文僅使用了測試集中的 150 個問題,我們也將遵循這一做法(儘管所有發現也適用於完整數據集)。
為什麼需要結構化生成?解析結果!
論文方法中一個有趣的部分(實際上應該是其重點)是用所謂的「完美文本解析器」從初始模型響應中提取答案。通常,大多數評估框架會使用簡單、明確的正則表達式解析響應,但 Tam 等人使用了 claude-3-haiku-20240307 來解析生成的輸出。換言之,每個答案實際上使用了兩個模型。在論文中,他們稱之為「完美文本解析器」,我們將其稱為「AI 解析器」。
必須注意這一非常規的方法,因為使用結構化生成的主要原因之一是 保證響應格式便於解析。解析和結構化生成密切相關。儘管論文存在許多問題,但 AI 解析器的使用是一個值得探討的有趣點。我們將深入研究 AI 解析與結構化生成的對比,以更深入地瞭解結構化生成的強大功能。
問題 1:AI 解析器的影響
為了更好地理解 AI 解析器的影響,我們深入研究了論文記錄的一個示例。幸運的是,Tam 等人提供了詳盡的數據集(12GB!https://drive.google.com/file/d/1HIh6BydZjxBkqm1oAxR5zSHzMG3M1nPC/view?usp=sharing)。這些實驗數據按模型和提示模板分類。我們將重點分析 lastletter-t3-f3 提示模板,並專注於 meta-llama/Meta-Llama-3-8B-Instruct 的單樣本示例。
讓我們先看一下提示如何指導模型完成自然語言(NL)格式下的任務:
按照指示完成任務:字符串操作任務:• 已知:一串單詞• 要求:生成一個新字符串,由每個單詞的最後一個字母組成• 過程:逐步思考,解決此問題注意:開始前請確保已經仔細閱讀問題。
指示:請按以下文本格式提供輸出:答案:<逐步思考>。The final answer is <答案>
可以注意到,提示中明確描述了響應格式:The final answer is <答案>。這意味著,如果模型遵循提示,我們應該能夠使用如下簡單的正則表達式解析所有答案:
answer_regex = r'answer is ([A-Za-z]{4})'
通過遍曆實驗數據的記錄結果,我們很快開現嚴格正則解析與 AI 解析結果之間存在差異:
事實證明,AI 解析器在自然語言格式的解析中承擔了大量工作。檢查實驗結果後,我們發現嚴格的正則表達式未能捕捉到的部分模型響應示例如下:
-
The answer is e-S-S-E. → ESSE
-
The answer is AAA R. → AAAR
-
The answer is “reye”. → REYE
-
The final answer is: YOOI → YOOI
擴展正則表達式可以解決這些問題。以下是經過調整的正則表達式組合:
alt_regex_1 = r'answer is ([A-Za-z]-[A-Za-z]-[A-Za-z]-[A-Za-z])'
alt_regex_2 = r'answer is:? "?([A-Za-z] ?[A-Za-z] ?[A-Za-z] ?[A-Za-z])"?'
alt_regex_3 = r'Concatenating them is "([A-Za-z]{4})"'
alt_regex_4 = r"answer is:? ([A-Za-z]'?[A-Za-z]'?[A-Za-z]'?[A-Za-z])"
結果表明,這些正則表達式組合可以覆蓋我們錯過的所有情況,無需額外調用更強大的模型。使用這些靈活的正則表達式組合解析後,我們得到以下結果:
令人驚訝的是,我們手工設計的正則表達式列表(花費時間並不多)在此數據集上的表現優於 AI 解析器!需要強調的是,使用結構化生成的主要目的之一就是避免解析輸出時的複雜性。然而,這一探索表明,AI 解析器並非「完美」的文本解析器:靈活的正則解析在性能上超過了調用 Claude 模型(同時更快、更便宜!)。
重現這些結果——非結構化
接下來,我們將使用 outlines.generate.text 方法重新運行相同提示,觀察結果如何。我們對單樣本提示進行了一點修改:Tam 等人的示例中僅使用了兩個名字,但所有問題實際上包含四個名字。根據我們的經驗,即使在未使用結構化生成的情況下,示例格式與實際問題保持一致也非常重要。因此,我們將提示示例修改為包含四個名字。
運行 outlines.generate.text 後,我們得到以下結果,與論文中記錄的結果進行比較:
可以看出,儘管結果稍有提升,但整體上與記錄的結果一致。我們並非追求完全一致的複現,而是驗證我們的結果是否與原始結果在同一水平上。
在改進單樣本提示後,不同解析方法之間的差距也縮小了。根據我們以往的研究(https://blog.dottxt.co/prompt-efficiency.html),模型似乎主要從示例中學習問題的結構。因此,提供更好的結構示例有助於提高對結構的遵從性。
現在,我們可以真正測試結構化生成對性能的影響。
任何可以解析的內容都可以生成
討論 AI 解析器的原因在於,理解如何解析 LLM 的響應是深入理解結構化生成的關鍵。一個常見誤解是(論文中也存在)將結構化生成簡單等同於 JSON 模式(或 YAML 模式、XML 模式等)。更好的理解方式是:將我們的響應解析器作為生成器運行。
為說明這一點,我們在運行結構化生成時,將對提示的推理步驟增加結構,然後將我們定義的答案正則表達式附加到提示中。這種方法統一了提示、解析器和生成器。這正是結構化生成之所以強大的秘訣所在。
以下是我們定義的結構和結果分析:
-
定義回答的推理結構的正則表達式:
cot_regex = r'Answer: T[\w \\",\\.]{30,250}. The '
-
此正則表達式允許模型在 30 到 250 個字符內進行推理,然後給出答案。
-
將此結構與之前的答案正則表達式組合:
struct_strict = outlines.generate.regex(
model,
cot_regex + answer_regex,
sampler=greedy())
完成後,我們通過運行上述生成器來測試結構化生成結果,結果如下:
與我們過往的發現一致,結構化生成的表現 優於 非結構化生成。
JSON 的問題在哪裡?
通過對如何正確實施結構化生成的深入瞭解,我們可以進一步分析論文中 JSON 結果的問題。下圖是論文中關於 JSON 模式表現的圖表:
在上圖中,結構化生成(JSON 模式)的準確率低於 10%,而非結構化生成(自然語言)的準確率接近 70%。儘管我們已成功複現非結構化結果,但 JSON 模式的表現顯然不合理。問題可能出在模型被要求以 JSON 格式響應時的提示設計上。
問題 2:提示設計中的問題
正如我們所提到的,論文的主要問題之一是用於結構化生成和非結構化生成的提示設計不同。在我們的分析中,只有使用相同的提示模板才能進行公正的比較。
因此,我們首先查看了論文中用於 JSON 模式評估的提示。根據記錄的數據(文件 lastletter-t2-structure/struct_llama-3-8b-instruct_shots_0.jsonl),以下是 JSON 模式評估中使用的提示:
按照指示完成任務:\
仔細閱讀每個問題,並逐步思考後作答。給定一串單詞,取出每個單詞的最後一個字母並將它們串聯起來。
指示:您必須使用工具。
問題:取出 「Britt Tamara Elvis Nayeli」 的每個單詞的最後一個字母並將它們串聯起來。
顯然,這個提示需要顯著改進才能正確評估此任務!在編寫提示時,建議反問自己:「這個提示是否包含足夠信息,讓一個經驗豐富的人類能夠正確回答問題?」 從提示中我們無法明確以下幾點:
回答必須是 JSON 格式(提示中完全沒有提到 JSON)。
即使假設回答需要 JSON 格式,也沒有提供具體的 JSON 格式或結構。
雖然提示提到需要使用「工具」,但並未說明需要使用什麼工具,或者工具的輸出應該是什麼樣的格式。結構化生成並不能「魔術般」地讓模型理解您的意圖,就如同在自家庭院中鋪設鐵路並不會讓火車停靠一樣。
JSON 的正確提示設計
我們之前討論過,結構化生成並不等同於 JSON 模式,但在某些情況下,我們確實需要 JSON 格式的輸出。為了理解《讓我自由表達?》論文中的錯誤,我們可以通過以下方式正確實施結構化生成。首先,使用如下的指令式提示:
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
你是一位擅長通過推理步驟解決簡單單詞謎題的專家。你的具體任務是接受一個包含 4 個名字的列表,分析每個名字的最後一個字母,然後將這些字母拚接成一個單詞。用戶的問題將以純文本形式呈現,您的回答需要以以下 JSON 格式呈現:
{"reasoning": <對答案的推理>, "answer": <最終答案>}<|eot_id|><|start_header_id|>user<|end_header_id|>
問題:取出 'Ian Peter Bernard Stephen' 的每個名字的最後一個字母並將它們拚接起來。<|eot_id|><|start_header_id|>assistant<|end_header_id|>
{"reasoning": "'Ian' 的最後一個字母是 'N','Peter' 的最後一個字母是 'R','Bernard' 的最後一個字母是 'D','Stephen' 的最後一個字母是 'N'。因此,答案是 'NRDN'。", "answer": "NRDN"}<|eot_id|><|start_header_id|>user<|end_header_id|>
問題:取出 'Britt Tamara Elvis Nayeli' 的每個名字的最後一個字母並將它們拚接起來。<|eot_id|><|start_header_id|>assistant<|end_header_id|>
<|eot_id|>
這種提示的特點如下:
明確的格式:清楚說明了回答需要採用 JSON 格式。
具體的示例:示例既匹配問題格式,也明確了答案的結構。
邏輯清晰:通過示例演示推理步驟,模型更易遵循。
定義我們的結構
接下來,我們需要定義結構(實際上,這應與提示同步進行)。針對該任務,我們將使用一個簡單的 Pydantic 模型:
class Response(BaseModel):
reasoning: constr(max_length=250)
answer: str = Field(pattern=r'[A-Z]{4}')
-
這裏將推理步驟限制為 250 個字符,確保模型不會花費過長時間進行推理。
-
將答案限制為僅包含 4 個大寫字母的有效響應。
驗證提示是否包含我們的結構
驗證提示是否與我們定義的結構相符,是確保模型輸出正確格式的關鍵步驟。以下代碼用於驗證:
from outlines.fsm.json_schema import build_regex_from_schema
schema_regex = build_regex_from_schema(Response.schema_json())
example_prompt = create_prompt(all_evals[5]['question'], tokenizer)
re.search(schema_regex, example_prompt)
驗證完成後,我們就可以進行下一步了!值得注意的是,這些準備工作同樣可以顯著改善非結構化生成的效果。
重新運行 JSON 評估
在上述改進提示的基礎上,我們重新運行了 JSON 模式的生成任務。以下是最終結果的比較:
正如所見,結構化生成再次優於非結構化生成。此外,結構化 JSON 的準確率最高,為 77%。這清楚地表明,正確的提示設計對結果的準確性有顯著影響。
結論
我們 .txt 團隊對結構化生成充滿熱情,並深信其能徹底改變使用 LLM 的方式。正因如此,我們對結構化生成有負面影響的任何說法都非常關注,並願意通過研究和實踐幫助澄清誤解。
但令人遺憾的是,當某些研究人員未能充分理解問題而發表誤導性結論時,社區就需要花費額外的時間和精力來糾正錯誤。如果您有關於結構化生成的任何疑問或問題,請隨時聯繫我們,我們樂於分享經驗。
正如《火線》中的 Omar Little 所說:
“向國王出手時,必須全力以赴,否則後果自負。”