讓 LLM 更好用的方法:ReAct prompting

自從生成式 AI 興起後,怎麼下 prompt 也成為熱門話題,prompt engineering 更成為新的研究領域。本文將專注於對 LLM prompt engineering 的探討,並介紹當前主流的 ReAct prompting。

什麼是 prompt engineering?

prompt 是指針對生成圖或生成文字的模型,希望模型幫忙完成任務,所輸入的文字。在和 LLM(大型語言模型),例如 ChatGPT 互動時,需要輸入一些話,請它回答我們的問題,這就是 prompt 最原始的模式。當然,對 LLM 說話也是門「科學與藝術」,好好地對它說話,讓它照著我們的意思完成工作同樣需要技巧。而研究這門技巧的學問就稱為「prompt engineering」(提示詞工程)。

什麼是 ReAct prompting?

ReAct (Reason+Act) 是由推理和行動兩個字所組而成,在〈ReAct:Synergizing Reasoning and Acting in Language Models〉(Shunyu et al., 2022)這篇論文中提出的一種結合推理和行動兩種能力,使語言模型能解決各種語言推理和決策任務的方法。

推理(Reasoning)的意思是:根據 LLM 現有的知識或行動後獲取的知識一步步導出結論的過程,之前使用 LLM 時的 Chain-of-Thought Prompting,就是引導 LLM 一步步推理,進而導出結論的方法(但沒有行動這項能力,所有的推理都是基於模型的生成)。這篇論文結合了行動,並引入了外部知識協助推理,最大的優點在於解決模型自身無法與外界即時進行(real time)交流的困境,且用於訓練模型的資料本身有截斷日期的問題,能減少語言模型的生成幻覺,也減少了推理過程中,模型可能在錯誤資訊上做推理,最終得出錯誤的結果的情況。

行動(Action)的意思是:讓 LLM 根據需求,思考是否尋求外部工具或使用哪一種工具的協助,在需要的時候呼叫工具進一步取得新資訊,例如使用 google search 或計算機等工具。


推理(Reasoning)中能夠適時的做出行動(Action),利用工具如搜尋引擎等來實現與外部世界的互動,以獲取即時的,相對可靠的額外信息(Observation)輔助推理。就是 ReAct prompting,這方法成功提高了語言模型的決策可解釋性和可信度。

當我們形容人類是如何思考問題時,例如出國旅遊需要規劃行程,人類會利用工具查詢當地天氣或交通狀況,再回到自己對旅遊地的了解及需求一步步安排行程,可能是想不想去、想吃什麼特色食物、或該去哪個點拍照,慢慢總結出一些結論。當然,過程中又會發現很多問題,例如某個場館週一有沒有開放,然後又透過外部工具搜尋場館資訊再回到旅遊規劃,不斷的重複這個推理和行動的過程直到完成旅遊規劃。

這樣說起來有沒有讓讀者更有共感呢?

論文作者即是透過這個洞見:在人類從事一項需要多個步驟的任務時,在步驟和步驟之間,或者說動作和動作之間,往往會有一個推理過程,提出 ReAct prompting, 讓 LLM 把內心獨白說出來,然後再根據獨白做對應的動作,來提高 LLM 答案的準確性。"

來看看論文中的例子

論文中以下面這個有趣(又有點過時)的問題為例,似乎有意要考考 LLM:


Question1:除了 Apple Remote,還有什麼設備可以控制相關軟體?(這裡的)相關軟體指的是,Apple Remote 最早可以互動的軟體。"

(1a)一般使用 LLM 的方式得到的答案有可能是 "iPod"
這答案是錯誤的,顯然是在胡扯。因為 LLM 的內容生成,是根據之前所有的字預測後續內容,所以,作者推測,出現這個結果很可能因為,LLM 其實並不知道相關答案,只是 Apple Remote 和 iPod 的語意很接近,所以才得出 iPod 這個結論。

(1b)當使用 CoT 的方法(請 LLM 一步步推理思考)
得到了"Apple Remote 當初是設計用於與 AppleTV 互動。AppleTV 可以透過iPhone、iPad 和 iPod Touch 控制。
所以答案是:iPhone、iPad 和 iPod Touch。"
這也是錯誤的。

(1c)讓 LLM 加上可以搜尋網路的工具呢
行動 1:搜尋[Apple Remote]
結果 1:Apple 遙控器是一種遙控器…最早可以控制「Front Row」…

行動 2:搜尋[Front Row]
結果 2:無結果

行動 3:搜尋[Front Row軟體]
結果 3:Front Row 是一種過時的軟體…

行動 4:完成[yes]

得到了正確答案了嗎? 少了推理的過程,只是一直在搜尋,顯然是沒有答案。


(1d)ReAct 方法,即推理+行動:
想法(念頭)1:我需要搜尋 Apple Remote 並找到 Apple Remote 最早可以控制的軟體。
行動 1:搜尋[Apple Remote]
小結 1:Apple Remote 是一種遙控器…最早是 2005 年 Apple 推出的,原始的設計可以控制「Front Row media center」…

想法(念頭)2:Apple Remote 最早可以設計用來控制 Front Row media center。接下來我需要搜尋 Front Row,找出還有什麼其他裝置可以控制 Front Row
行動 2:搜尋[Front Row]
小結 2:搜尋的結果很發散,找不到特定答案。可以試試看搜尋「Front Row Seat to Earth」和「Front Row軟體」

想法(念頭)3:Front Row 沒有找到,我找看看「Front Row 軟體」好了
行動 3:搜尋[Front Row 軟體]
小結 3:Front Row 已經是一種過時的軟體了…

想法(念頭)4:Front Row 軟體可以由 Apple Remote 或鍵盤功能鍵控制。所以答案是鍵盤功能鍵
行動 4:完成了!答案是[鍵盤功能鍵]

答案正確。透過推理,結合行動,LLM Agent 經由設計過的步驟找到了答案。整個過程感覺就像是個不特別聰明的孩子,要把想法寫出來,然後去綜合所有想法和觀察得到的小結,接著再做出相對應的下一步動作。但顯然這個方法有效,它最終找到了答案。

容我補充說明一下:在以上的範例中事先定義了" search [entity]”,這個行動可以從相應的維基百科傳回句子。論文中還定義了 lookup [entity]這個行動,它的功能是返回頁面中[entity]附近的句子,類似 Ctrl+F 關鍵字後,傳回關鍵字附近的句子。這些行動(search、lookup)就像是人類找尋資料中的行為。

註解:Apple 第一代的 Apple Remote 可以透過紅外線控制 Mac 上的媒體「Front Row」播放音樂、影片。而 Mac OSX 10.7 已取消 Front Row 軟體應用程式。

 
引用自:https://zh.wikipedia.org/zh-tw/Front_Row、https://applefans.today/2021-apple-tv-remote-review/

程式實作說明

以上介紹完概念了,我們接著看看實作上又是怎麼一回事呢?
接下來讓我們來看看實作本文概念的程式碼囉。

首先是 sys_prompt,這就是指示 LLM 要如何完成任務的 prompt,並給出了範例。
在這邊我們寫了一段落落長的 prompt 來告訴 LLM 要怎麼樣做 ReAct prompting,這段 prompt 的大意是:請你透過思考(推理)、行動、觀察(得出小結)循環運作,最終輸出一個答案。然後告訴 LLM 思考(推理)、行動、觀察(得出小結)分別的定義,最後也告訴 LLM 做出行動時可以利用的工具如計算機、Google 搜尋並給出兩個例子。


sys_prompt = """
You run in a loop of Thought, Action, Observation.
At the end of the loop you output an Answer
Use Thought to describe your thoughts about the question you have been asked.
Use Action to run one of the actions available to you。
Observation will be the result of running those actions.

Your available actions are:

calculate:
e.g. calculate: 4 * 7 / 3
Runs a calculation and returns the number - uses Python so be sure to use floating point syntax if necessary

call_google:
e.g. call_google: European Union
Returns a summary from searching European Union on google

You can look things up on Google if you have the opportunity to do so, or you are not sure about the query

Example1:

Question: What is the capital of Australia?
Thought: I can look up Australia on Google
Action: call_google: Australia

You will be called again with this:

Observation: Australia is a country. The capital is Canberra.

You then output:

Answer: The capital of Australia is Canberra

Example2:

Question: What are the constituent countries of the European Union? Try to list all of them.
Thought: I can search the constituent countries in European Union by Google
Action: call_google: constituent countries in European Union

Observation: The European Union is a group of 27 countries in Europe, Today, 27 countries are part of the European Union.
These countries are:

Austria、Belgium、Bulgaria、Croatia、Cyprus、Czechia、Denmark、Estonia、Finland、France、Germany、Greece、Hungary、Ireland、
Italy、Latvia、Lithuania、Luxembourg、Malta、Netherands、Poland、Portugal、Romania、Slovakia、Slovenia、Spain、Sweden

Answer: There are 27 constituent countries in European Union, the name of constituent countries are listed as follows
Austria
Belgium
Bulgaria
Croatia
Cyprus
Czechia
Denmark
Estonia
Finland
France
Germany
Greece
Hungary
Ireland
Italy
Latvia
Lithuania
Luxembourg
Malta
Netherlands
Poland
Portugal
Romania
Slovakia
Slovenia
Spain
Sweden

""".strip()

在 prompt 之後,當然是要提供能讓 LLM 使用的工具囉。
所以在這段程式碼中定義了三種工具,分別是 call_google、查詢 wikipedia、calculate :


import json
import sys
import requests
import httpx

from googleapiclient.discovery import build

GOOGLE_API_KEY = "*********"
GOOGLE_CSE_ID = "*********"

class FUNCTIONS:
    """ All available functions for the GPT calling """

    def call_google(query: str, **kwargs):
        """ Call the google chrome for searching online """

        service = build(serviceName="customsearch",
                        version="v1",
                        developerKey=GOOGLE_API_KEY,
                        static_discovery=False)
        res = service.cse().list(q=query, cx=GOOGLE_CSE_ID, **kwargs).execute()
        res_items = res["items"]
        res_snippets = [r['snippet'] for r in res_items]
        return str(res_snippets)

    def wikipedia(query: str):
        """ The wikipedia search function """

        return httpx.get("https://en.wikipedia.org/w/api.php", params={
            "action": "query",
            "list": "search",
            "srsearch": query,
            "format": "json"
        }).json()["query"]["search"][0]["snippet"]

    def calculate(what):
        return eval(what)

接著我們要使用 OpenAI 的 gpt-3.5-turbo-0125 這個 LLM 來回答問題囉,先將前面的工具放在 available_functions,再將 sys_prompt 和想問的問題(user_query)組合成 messages,這個 messages 就丟進 LLM 看會得到什麼 response。

然後就是關鍵了,在每次 LLM 的回應中,我們會判斷裡面有沒有"Action"、"Answer"等文字,如果 response 中有"Answer"的話就代表 LLM 已經決定最後要輸出什麼文字,那就太棒了,可以跳出這個迴圈,輸出答案。如果有Action 的話就調用相對應的工具, 並且將工具結果傳回給 LLM。
如果還沒找到"Answer"呢?沒關係,我們先把思考過程印出來看看並加上顏色,觀察目前 response 中要採取什麼動作該調用哪一個工具,如果都沒有的話那在這個流程中會讓使用者輸入回饋引導 LLM 繼續。
基本上就是重複這個過程,直到得到了"Answer"為止。


import openai
import re
import json

from colorama import Fore, Style

openai.api_key = "********"

# Available functions
available_functions = {n:f for n, f in FUNCTIONS.__dict__.items() if not n.startswith("_") and callable(f)}

# User query and system prompt to form the messages
user_query = f"Why did Sweden join NATO recently when it was not a member of NATO before?"

messages = [
    {"role": "system", "content": sys_prompt},
    {"role": "user", "content": user_query},
]

# regular expression regex patterns
action_re = re.compile('^Action: (\w+): (.*)$')
answer_re = re.compile("Answer: ")

while True:
    # Send user_query and system_prompt to the GPT
    response = openai.ChatCompletion.create(
        model='gpt-3.5-turbo-0125',
        messages=messages,
    )

    # Get the response from the GPT and add it as a part of memory
    response_msg = response["choices"][0]["message"]["content"]
    messages.append({"role": "assistant", "content": response_msg})

    # If the respionse contains the keyword "Answer: ", then return
    if answer_re.search(response_msg):
        print(Fore.YELLOW + response["choices"][0]["message"]["content"])
        print(Style.RESET_ALL)
        break

    # Print the thinking process
    print(Fore.GREEN + response_msg)
    print(Style.RESET_ALL)

    # Take actions
    actions = [action_re.match(a) for a in response_msg.split("\n") if action_re.match(a)]
    if actions:
        action, action_input = actions[0].groups()
        try:
            print(Fore.CYAN + f" -- running {action} {action_input}")
            print(Style.RESET_ALL)
            # Apply available tools for the function execution
            obervation = available_functions[action](action_input)
            # print(Fore.BLUE + f"Observation: {obervation}")
            print(Style.RESET_ALL)
            messages.append({"role": "user", "content": "Observation: " + obervation})
        except:
            raise NotImplementedError
    # If no action is detected then try to ask user to provide some feedback
    else:
        print(Fore.RED + f"No action is detected, you can give some feedback by input")
        user_feedback = input("Feedback: ")
        messages.append({"role": "user", "content": user_feedback})
        
        
程式的輸出結果:Thought & Action 為綠色,-- running 為藍色,Answer 為黃色

在此也將 LLM 的思考過程印出來讓大家可以理解中間發生了什麼事情,
我在程式中詢問了:user_query = f"Why did Sweden join NATO recently when it was not a member of NATO before?"
(瑞典先前不是北約成員國,但為何最近加入北約呢?)


Thought: I'll look up recent information on why Sweden decided to join NATO, 
as this decision would likely involve considerations of national security, regional stability, and political factors.

Action: call_google: Why did Sweden join NATO recently?

可以看到 LLM 開始自己 murmur 了起來:我會查一下最近關於瑞典為什麼決定加入北約的信息,因為這個決定可能會涉及國家安全、區域穩定和政治因素的考慮。
然後決定做出行動:call_google:為什麼瑞典最近加入北約?


-- running call_google Why did Sweden join NATO recently?

接著使用我們賦予它的工具:running call_google 為什麼瑞典最近加入北約?


Answer: Sweden decided to join NATO due to a combination of reasons. These include the desire to strengthen its security in light of recent regional developments, notably Russia's aggressive actions, 
such as its war that has pushed the alliance to expand. Sweden, along with Finland, has been an official partner of NATO since 1994 and has been a major contributor to the alliance, participating in various NATO operations. 
The Swedish government assessed that joining NATO was the best way to protect its national security. Additionally, Sweden's membership was seen as a means to strengthen NATO itself and add to the stability in the Euro-Atlantic area. 
The process of joining NATO was completed after overcoming the last hurdles, including obtaining the necessary approvals from all existing member states, such as Turkey and Hungary.

最後從搜尋到的資料中整理出答案來:瑞典基於多種原因決定加入北約。 其中包括根據最近的地區事態發展加強其安全的願望,特別是俄羅斯的侵略行動。

很有趣吧!

現有使用 ReAct prompting 的框架

如果需要開發產品,我推薦讀者去看看 langchain 這個框架,它肯定幫你省了很多需要"自己動手造輪子"的部分,讓你快速使用 ReAct prompting 的方式做開發。

https://python.langchain.com/docs/modules/agents/agent_types/react.html

結語

這篇文章中,我們清楚說明了 ReAct prompting 的原理,並用範例程式示範了如何實現論文中的方法。我初次看到 ReAct prompting 時除了感嘆 LLM 的強大之外,也對人類靈活巧妙使用 LLM 的方法,不禁在心裡想:哇!居然可以這樣妙用!希望能帶給你些許內心的漣漪。

(撰稿工程師:莊智宇)

參考資料