關於 Prompt engineering 我只是略懂:來自 Meta Llama 2 的提示詞工程建議


這邊文章是閱讀 Llama 2 官方文件中的提示工程建議後的筆記,從明確指示到角色提示,再到自我一致性與檢索增強,讓略懂的我為你揭示 Meta 在提示詞工程上給了哪些建議。

給予 LLM 推論任務明確的指示(Explicit Instructions)

相較於使用開放式的問法,給予 LLM 明確且詳細的指引是產生優良推論結果的關鍵。至於如何給予 LLM 明確的推論方向指引,我們可以使用以下幾項要點作為發問時的提問設置與條件限制。

  • 風格化(Stylization
    • Explain this to me like a topic on a children's educational network show teaching elementary students.
    • I'm a software engineer using large language models for summarization. Summarize the following text in under 250 words:
    • Give your answer like an old timey private investigator hunting down a case step by step.
  • 格式設定(Formatting
    • Use bullet points.
    • Return as a JSON object.
    • Use less technical terms and help me apply it in my work in communications.
  • 限制條件(Restrictions
    • Only use academic papers.
    • Never give sources older than 2020.
    • If you don't know the answer, say that you don't know.
complete_and_print("Explain the latest advances in large language models to me.")
# 上述的提示詞因為沒有給予明確的時間區間作為限制,LLM 在推論時有很大的可能性會從預訓練的 Corpus 中使用過於久遠前的資料來進行回答

complete_and_print("Explain the latest advances in large language models to me. Always cite your sources. Never cite sources older than 2020.")
# 透過明確時間範圍的指引,我們可以確保 LLM 在進行推論的時候會使用近幾年的資料來進行答案的推論

零樣本學習(Zero-shot learning)以及少樣本學習(Few-shot learning)

shot 是對於 LLM 所期望的提示與回應類型的一個範例或樣本。這個術語源於訓練計算機視覺模型時對攝影圖片的應用,其中一次拍攝指的是模型用來分類影像的一個範例或樣本。

Zero-Shot Prompting

像 Llama 2 這樣的 LLM 之所以獨特,是因為它們能夠在沒有事先看到任務範例的情況下就遵循提示詞並產生回應的推論。沒有範例的提示稱為「零樣本提示」(zero-shot prompting)。但當我們嘗試使用 Llama 2 作為情緒檢測器時,你可能會注意到輸出格式有所不同,但我們可以通過更好的提示來改善這一點。

complete_and_print("Text: This was the best movie I've ever seen! \n The sentiment of the text is: ")
# 回傳 positive sentiment

complete_and_print("Text: The director was trying too hard. \n The sentiment of the text is: ")
# 回傳 negative sentiment

Few-Shot Prompting

在提示詞中添加特定的期望輸出範例通常會導致更準確、更一致的輸出。這種技術被稱為「少樣本提示」(few-shot prompting),在以下的例子中,生成的回應遵循了我們想要的格式,提供了一個更細緻的情感分類器,它給出了正面、中立和負面反應的信心百分比。

def sentiment(text):
    response = chat_completion(messages=[
        user("You are a sentiment classifier. For each message, give the percentage of positive/netural/negative."),
        user("I liked it"),
        assistant("70% positive 30% neutral 0% negative"),
        user("It could be better"),
        assistant("0% positive 50% neutral 50% negative"),
        user("It's fine"),
        assistant("25% positive 50% neutral 25% negative"),
        user(text),
    ])
    return response

def print_sentiment(text):
    print(f'INPUT: {text}')
    print(sentiment(text))

print_sentiment("I thought it was okay")
# More likely to return a balanced mix of positive, neutral, and negative

print_sentiment("I loved it!")
# More likely to return 100% positive

print_sentiment("Terrible service 0/10")
# More likely to return 100% negative

Role Prompting

在對 LLM 輸入提示詞作為推論的指引時,在提示詞中以一個特定的角色設定作為推論進行的指引,通常可以讓生成的結果具有更高的品質,因為透過明確的角色設定,我們可以讓 LLM 進行推論時更加明確的知道要從他那極為龐大且寬廣的知識光譜中哪一個位置去作為推論的基礎,舉個例來說,當我們詢問 LLM 關於”量子力學的定義”這樣一個問題時,極有可能在 LLM 用來進行預訓練的資料集中會同時囊括了大學生寫的文章、研究生的論文以及量子領域的大神在期刊上的經典理論,如果我們沒有事先透過提示詞指引 LLM 在推論時該以什麼樣的角色來回答問題時,問題的回答品質就極有可能前後不一致或者使用到品質較差的預訓練資料作為回答問題的依據。

complete_and_print("Explain the pros and cons of using PyTorch.")

# 這樣簡單的問法很可能會得到很一般的 PyTorch 優缺點論述,涵蓋內容更多會是技術文件、PyTorch 社群內質量過低的內容等等,然後也可能會得到 PyTooch 學習曲線過於陡峭等等不著邊際的回應。

complete_and_print("Your role is a machine learning expert who gives highly technical advice to senior engineers who work with complicated datasets. Explain the pros and cons of using PyTorch.")

# 經過明確角色設定後的提示詞,通常讓 LLM 具有更明確的內容推論方向,進而更多專注在 PyTorcg 技術性的利弊分析,且這些分析可能能夠更深入提供了關於模型層的技術細節。

思維鏈(Chain-of-Thought)

許多研究顯示,僅僅是透過在提示詞中添加鼓勵 LLM 進行逐步思考的語句就能夠大大的提升了 LLM 進行複雜問題推理的能力。

complete_and_print("Who lived longer Elvis Presley or Mozart?")

# Often gives incorrect answer of "Mozart"

complete_and_print("Who lived longer Elvis Presley or Mozart? Let's think through this carefully, step by step.")

# Gives the correct answer "Elvis"

Self-Consistency

「自我一致性」(Self-Consistency)提示工程是一種用於提高 LLM 回答問題時準確性的技術。這種方法的核心思想是,通過多次讓模型對同一問題進行回答,然後選擇最常出現的答案作為最終的回應。在自我一致性方法中,模型被要求多次獨立生成對同一問題的回答。這些不同的回答被收集起來,並進行分析,以確定哪個答案出現得最頻繁。選擇最常見的答案作為最終回答的想法是,這樣的答案更可能是正確的,因為它表明模型在多次嘗試中都有相似的推理過程。

這種方法的優點在於它提高了模型回答複雜問題時的準確性。然而,它也有明顯的缺點,那就是需要更多的計算資源,因為模型需要對同一問題進行多次處理。這意味著更高的計算成本和更長的處理時間,這在某些應用場景中可能是不可接受的。

「自我一致性」提示工程在實務上是一種有用的方法,特別是在需要從大型語言模型中獲得高準確度回答時。但它需要在準確性和計算資源之間找到適當的平衡。

import re
from statistics import mode

def gen_answer():
    response = completion(
        "John found that the average of 15 numbers is 40."
        "If 10 is added to each number then the mean of the numbers is?"
        "Report the answer surrounded by three backticks, for example: ```123```",
        model = LLAMA2_70B_CHAT
    )
    match = re.search(r'```(\d+)```', response)
    if match is None:
        return None
    return match.group(1)

answers = [gen_answer() for i in range(5)]

print(
    f"Answers: {answers}\n",
    f"Final answer: {mode(answers)}",
    )

# Sample runs of Llama-2-70B (all correct):
# [50, 50, 750, 50, 50]  -> 50
# [130, 10, 750, 50, 50] -> 50
# [50, None, 10, 50, 50] -> 50

Retrieval-Augmented Generation

檢索增強生成(簡稱 RAG)是一種結合了信息檢索和文本生成的方法論。它通常應用於改善 LLM 在處理複雜問題時的性能和準確度。相較於先前提到的 Zero-shot、Few-shot 等等提示詞工程的方法論,RAG 在 LLM 進行推論之前,首先要進行信息檢索,從一個大型的、結構化的 RDBMS 或是 Vector Store 中尋找與輸入問題相關的資料。然後,這些被檢索到的數據會被用作生成回答的上下文或參考,以此來增強模型對問題的理解和回答的準確性。

這種技術的優點在於它能夠結合模型自身的語言生成能力與外部資料的豐富信息。這樣不僅可以提供更準確的回答,還可以擴展模型的知識範圍,尤其是在回答那些需要特定領域知識的問題時。然而,RAG 也有其挑戰,包括如何有效地從大量數據中檢索到最相關的信息,以及如何整合這些資料和模型原有的生成過程,以產生連貫、準確的回答。更不用說,這種方法也增加了 LLM 推論時需要的算力與 token 消耗的成本。

complete_and_print("What is the capital of the California?", model = LLAMA2_70B_CHAT)

# 當詢問 LLM 加州的首府是哪裡時,這類常識型的問題通常 LLM 都能夠在預訓練的資料集中找到正確的答案。

但是當問題的正確答案牽涉到需要具即時性或私密性的資料來輔助 LLM 進行推論的時候,這時 LLM 通常會因為預訓練的資料中沒有相關的資料可幫助他進行推論而產生兩種沒有對齊人類需求的回答結果,微調得好一點的 LLM 會禮貌地回答說他不知道,要不然就是會編造出似是而非的內容作為回答,也就是所謂的 LLM Hallucinations。

complete_and_print("What was the temperature in Menlo Park on December 12th, 2023?")

上述問題牽涉到需要知道某一個特定時間點時 Menlo Park 的氣溫,由於 LLM 預訓練完成的時間可能遠在該時間點之前,所以在 LLM 的權重之中絕對不可能 包含這樣的資訊,所以 LLM 會回答使用者說 “I’m just an AI, I don’t have access to real-time weather data or historical weather records.” 這算是一種比較好的推論結果,至少他沒有胡編亂造一個看似合理實則錯誤的答案。

complete_and_print("What time is my dinner reservation on Saturday and what should I wear?")

上述問題則是一個需要提供些個人隱私資料才能夠正確回答的問題,實務上需要讓 LLM 能夠存取使用者的行事曆才能夠知道詢問這一個問題的人在星期六是否 有晚餐的預約資訊,並根據晚餐聚會的類型來給予著裝的建議,然而在沒有辦法得到這些脈絡資訊的狀態下,LLM 若沒有產生 Hallucination 的話,回答可 能會是 “I’m not able to access your personal information [..] I can provide some general guidance”。

當等待推論的問題需要即時性或是具隱私性的外部資訊來輔助 LLM 進行答案的推論時,這時後就可以利用 RAG 這一個方法論來協助進行,在最簡單的實務上,當使用者對 LLM 發出問題之後,使用者的問題並不會立即的送入 LLM 去進行答案的推論,而是在應用端會先將使用者的問題透過文字嵌入模型轉換成一組高維度向量的表現形式,這轉換後文字向量的維度大小不一,視使用哪一個文字向量嵌入模型而定,然後我們會將這一組轉換後的高維度向量發送進向量資料庫中去計算 用來表示使用者問題的向量 與儲存在向量資料庫中那眾多筆的向量資料紀錄間的距離,並從中檢索出在距離最為相近的 Top N 筆資料,而這 Top N 筆資料的意義在於他們在相同的高維度向量空間中在距離上與 用來表示使用者問題的向量 最為接近,所以在語意上他們間的關聯性最高,進而可以用來作為推論答案的脈絡資訊。

在向量資料庫中,用來計算向量間距離的常見算法有以下幾種:

  • 歐幾里得距離 (Euclidean Distance):這是最常見的距離計算方法,適用於計算兩點之間的直線距離。
  • 曼哈頓距離 (Manhattan Distance):又稱為城市街區距離,是在座標系中兩點對應維度的差的絕對值之和。
  • 餘弦相似度 (Cosine Similarity):這種方法計算的是兩個向量之間的角度,而不是實際的距離。它常用於評估兩個向量的相似度。
  • 漢明距離 (Hamming Distance):這種方法用於計算兩個等長字串之間的差異,即在同一位置上的符號不同的位數。
  • 傑卡德相似係數 (Jaccard Similarity):這種方法用於比較兩個集合的相似度,定義為兩個集合交集大小與聯集大小的比例。

下方 code block 中的程式碼只是很簡單的用 dict 的方式模擬了要用來做 inference 時的 context。

MENLO_PARK_TEMPS = {
    "2023-12-11": "52 degrees Fahrenheit",
    "2023-12-12": "51 degrees Fahrenheit",
    "2023-12-13": "51 degrees Fahrenheit",
}

def prompt_with_rag(retrived_info, question):
    complete_and_print(
        f"Given the following information: '{retrived_info}', respond to: '{question}'"
    )


def ask_for_temperature(day):
    temp_on_day = MENLO_PARK_TEMPS.get(day) or "unknown temperature"
    prompt_with_rag(
        f"The temperature in Menlo Park was {temp_on_day} on {day}'",  # Retrieved fact
        f"What is the temperature in Menlo Park on {day}?",  # User question
    )


ask_for_temperature("2023-12-12")
# "Sure! The temperature in Menlo Park on 2023-12-12 was 51 degrees Fahrenheit."

ask_for_temperature("2023-07-18")
# "I'm not able to provide the temperature in Menlo Park on 2023-07-18 as the information provided states that the temperature was unknown."

Program-Aided Language(PAL) Models

很出乎意料的,LLM 非常不擅長將它那驚人的推論能力用在數學相關的計算問題,在 ChatGPT 剛推出來的時候,在簡單的加減乘除上就經常給出錯誤百出的答案,雖然經過了過去一年的發展,這個問題已經逐漸的被各大 LLM vendors 使用各種方法解決了,但依舊是有很多參數規模沒那麼大或是沒有透過微調去持續進行優化的 LLM 在數學計算上依舊是會給出錯誤的答案。以 Llama 2 為例,以下這簡單的四組運算的正確答案是 91,383。

((-5 + 93 * 4 - 0) * (4^4 + -7 + 0 * 5))

但當你嘗試將上面這一條簡單的算式發入 Llama 2 去進行答案推論時,它完全無法回答出正確的計算結果。

complete_and_print("""
Calculate the answer to the following math problem:

((-5 + 93 * 4 - 0) * (4^4 + -7 + 0 * 5))
""")

# 你會得到以下錯誤的推論結果,像是 92448, 92648, 95463...

因為觀察到這樣的問題,所有有學者提出了 PAL: Program-aided Language Models 這樣一個解決方案,這一個解決方案的思路是,雖然 LLM 非常不擅長處理數學上的問題,但他在生成程式碼這項任務上卻執行得異常優秀,所以在推論數學運算的答案這項任務的執行上,不直接要求 LLM 用統計思維去進行答案的推論,而是要求他先將算式轉換成某個特定程式語言(ex. Python)的程式碼,然後透過執行生成出來的程式碼進而去得到正確的答案。

complete_and_print(
    """
    # Python code to calculate: ((-5 + 93 * 4 - 0) * (4^4 + -7 + 0 * 5))
    """,
    model="meta/codellama-34b:67942fd0f55b66da802218a19a8f0e1d73095473674061a6ea19f2dc8c053152"
)

透過提示詞去要求 Code Llama 34B 去生成下方 code block 中的 Python code,透過這樣間接的方式 Llama 就能正確的將算式的運算結果透過 Python 成功的計算出來。

num1 = (-5 + 93 * 4 - 0)
num2 = (4**4 + -7 + 0 * 5)
answer = num1 * num2
print(answer)

同樣的如果你把 ((-5 + 93 * 4 – 0) * (4^4 + -7 + 0 * 5)) 發給 ChatGPT 進行答案的推論,它也會先呼叫 OpenAI Code Interpreter 去生成所提供算式的 Python 的程式碼,計算完之後才將答案伴隨著問題一同生成回應的內容給你。


Limiting Extraneous Tokens

LLM 在生成內容的時候若沒有給予明確的指引或角色與輸出格式的設定,經常會在問題的答案之外生成許多不相關的 tokens,而這些多餘的 tokens 在實務上經常會讓應用程式需要執行許多額外資料清洗的任務,我們來看看以下一個簡單的範例。

complete_and_print(
    "Give me the zip code for Menlo Park in JSON format with the field 'zip_code'",
    model = LLAMA2_70B_CHAT,
)

# Likely returns the JSON and also "Sure! Here's the JSON..."

上面這段提示詞應該是能夠讓 Llama 2 正確的以 JSON 的格式回傳,但是經常會伴隨著一些不需要的額外內容,例如它極有可能會生成 Sure! Here's the JSON... {'zip_code': 94025} 而不是很乾脆的只回應 {'zip_code': 94025}


這時我們可以透過給予更明確的提示詞指引以及條件的限制,讓 Llama 2 盡可能的回傳乾淨且可直接剖析的 JSON payload

complete_and_print(
    """
    You are a robot that only outputs JSON.
    You reply in JSON format with the field 'zip_code'.
    Example question: What is the zip code of the Empire State Building? Example answer: {'zip_code': 10118}
    Now here is my question: What is the zip code of Menlo Park?
    """,
    model = LLAMA2_70B_CHAT,
)

# "{'zip_code': 94025}"