工具使用,統一化

釋出於 2024 年 8 月 12 日
在 GitHub 上更新

目前,多個流行模型系列已經有了**統一的工具使用 API**。這個 API 意味著相同的程式碼具有可移植性——在使用 MistralCohereNousResearchLlama 模型進行聊天時,幾乎不需要或根本不需要模型特定的更改即可使用工具。此外,Transformers 現在還包含了輔助功能,使工具呼叫更加容易,並提供了完整的文件示例,涵蓋了整個工具使用過程。未來還將增加對更多模型的支援。

引言

工具使用是一個有趣的特性——每個人都認為它很棒,但大多數人並沒有親自嘗試過。從概念上講,它非常簡單:您給您的 LLM 一些工具(可呼叫函式),然後它就可以決定呼叫這些工具來幫助它響應使用者查詢。也許您給它一個計算器,這樣它就不必依賴其內部不可靠的算術能力。也許您讓它搜尋網頁或檢視您的日曆,或者您給它(只讀!)訪問公司資料庫的許可權,這樣它就可以提取資訊或搜尋技術文件。

工具使用克服了 LLM 的許多核心限制。許多 LLM 流暢且健談,但計算和事實往往不精確,對更小眾主題的具體細節也模糊不清。它們不知道訓練截止日期之後發生的任何事情。它們是通才;它們進入對話時,除了您在系統訊息中提供給它們的資訊之外,對您或您的工作場所一無所知。工具為它們提供了訪問結構化、具體、相關和最新資訊的許可權,這對於將它們變成真正有用的夥伴而非僅僅是迷人的新奇事物非常有幫助。

然而,當您真正嘗試實施工具使用時,問題就出現了。文件通常稀疏、不一致,甚至相互矛盾——對於閉源 API 和開放訪問模型都是如此!儘管工具使用在理論上很簡單,但在實踐中卻常常變成一場噩夢:如何將工具傳遞給模型?如何確保工具提示與模型訓練時使用的格式匹配?當模型呼叫工具時,如何將其整合到聊天中?如果您以前嘗試過實施工具使用,您可能會發現這些問題出奇地棘手,並且文件並非總是完整和有用的。

更糟糕的是,不同的模型在工具使用實現上可能有天壤之別。即使在定義可用工具的最基本層面,有些提供商期望 JSON 模式,而另一些則期望 Python 函式頭。即使在那些期望 JSON 模式的提供商中,細微的細節也常常不同,從而造成巨大的 API 不相容性。這產生了很大的摩擦,通常只會加深使用者的困惑。那麼,我們能做些什麼來解決所有這些問題呢?

聊天模板

Hugging Face 電影宇宙的忠實粉絲們會記得,開源社群過去曾面臨著與**聊天模型**類似的挑戰。聊天模型使用 `<|start_of_user_turn|>` 或 `<|end_of_message|>` 等控制標記,讓模型瞭解聊天中發生的事情,但不同模型使用完全不同的控制標記進行訓練,這意味著使用者需要為他們想要使用的每個模型編寫特定的格式化程式碼。這在當時是一個巨大的麻煩。

我們對此的解決方案是**聊天模板**——本質上,模型會附帶一個微小的 Jinja 模板,它會以正確的格式和控制標記為每個模型渲染聊天。聊天模板意味著使用者可以用通用、與模型無關的格式編寫聊天,相信 Jinja 模板會處理所需的任何模型特定格式。

那麼,支援工具使用的明顯方法就是擴充套件聊天模板以也支援工具。這正是我們所做的,但工具為模板系統帶來了許多新的挑戰。讓我們來回顧一下這些挑戰以及我們是如何解決它們的。在這個過程中,希望您能更深入地瞭解該系統的工作原理以及如何讓它為您服務。

將工具傳遞給聊天模板

我們設計工具使用 API 的首要標準是,它應該能直觀地定義工具並將其傳遞給聊天模板。我們發現大多數使用者會先編寫工具函式,然後弄清楚如何從這些函式生成工具定義並將其傳遞給模型。這導致了一種顯而易見的方法:如果使用者可以直接將函式傳遞給聊天模板,並讓它為他們生成工具定義呢?

然而,這裡的問題是“傳遞函式”是一種非常特定於語言的操作,許多人透過JavaScriptRust 而不是 Python 來訪問聊天模型。因此,我們找到了一個我們認為兩全其美的折衷方案:**聊天模板期望工具定義為 JSON 模式,但如果您將 Python 函式傳遞給模板,它們將自動為您轉換為 JSON 模式。**這帶來了一個簡潔的 API:

def get_current_temperature(location: str):
    """
    Gets the temperature at a given location.

    Args:
        location: The location to get the temperature for
    """
    return 22.0  # bug: Sometimes the temperature is not 22. low priority

tools = [get_current_temperature]    

chat = [
    {"role": "user", "content": "Hey, what's the weather like in Paris right now?"}
]

tool_prompt = tokenizer.apply_chat_template(
    chat, 
    tools=tools,
    add_generation_prompt=True,
    return_tensors="pt"
)

在內部,`get_current_temperature` 函式將擴充套件為完整的 JSON 模式。如果您想檢視生成的模式,可以使用 `get_json_schema` 函式。

>>> from transformers.utils import get_json_schema

>>> get_json_schema(get_current_weather)
{
    "type": "function",
    "function": {
        "name": "get_current_temperature",
        "description": "Gets the temperature at a given location.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The location to get the temperature for"
                }
            },
            "required": [
                "location"
            ]
        }
    }
}

如果您更喜歡手動控制,或者您正在使用 Python 以外的語言進行編碼,您可以直接將這些 JSON 模式傳遞給模板。但是,當您在 Python 中工作時,您可以避免直接處理 JSON 模式。您只需用清晰的**名稱**、準確的**型別提示**和完整的**文件字串**(包括**引數文件字串**)來定義您的工具函式,因為所有這些都將用於生成模板將讀取的 JSON 模式。其中大部分無論如何都是良好的 Python 實踐,如果您遵循它,您會發現無需額外工作——您的函式已經可以直接用作工具了!

請記住:準確的 JSON 模式,無論是從文件字串和型別提示生成還是手動指定,對於模型理解如何使用您的工具都至關重要。模型永遠不會看到您函式內部的程式碼,但它會看到 JSON 模式。它們越清晰、越準確,效果就越好!

將工具呼叫新增到聊天中

使用者(和模型文件 😬)經常忽略的一個細節是,當模型呼叫工具時,這實際上需要向聊天曆史中新增**兩條**訊息。第一條訊息是助手**呼叫**工具,第二條是**工具響應**,即被呼叫函式的輸出。

工具呼叫和工具響應都是必需的——請記住,模型只知道聊天曆史中的內容,如果它看不到它所做的呼叫以及傳遞給該呼叫的引數以獲取響應,它將無法理解工具響應。“22”本身資訊量不大,但如果您知道它之前的訊息是 `get_current_temperature("Paris, France")`,那麼它就非常有用了。

這是不同提供商之間可能存在極大差異的領域之一,但我們確定的標準是**工具呼叫是助手訊息的一個欄位**,如下所示:

message = {
    "role": "assistant",
    "tool_calls": [
        {
            "type": "function",
             "function": {
                 "name": "get_current_temperature", 
                 "arguments": {
                     "location": "Paris, France"
                }
            }
        }
    ]
}
chat.append(message)

將工具響應新增到聊天中

工具響應要簡單得多,尤其是當工具只返回單個字串或數字時。

message = {
    "role": "tool", 
    "name": "get_current_temperature", 
    "content": "22.0"
}
chat.append(message)

工具使用實戰

讓我們利用目前已有的程式碼,構建一個完整的工具呼叫示例。如果您想在自己的專案中運用工具,我們建議您試著執行這裡的程式碼——嘗試自己執行,新增或移除工具,切換模型,並調整細節,以熟悉該系統。這種熟悉程度將在您需要將工具使用整合到自己的軟體中時,大大簡化工作!為了方便起見,此示例也提供了 Jupyter Notebook 版本

首先,我們來設定模型。我們將使用 `Hermes-2-Pro-Llama-3-8B`,因為它體積小巧,功能強大,不受限制,並且支援工具呼叫。不過,如果您在複雜任務上追求更好的結果,可以使用更大的模型!

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

checkpoint = "NousResearch/Hermes-2-Pro-Llama-3-8B"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForCausalLM.from_pretrained(checkpoint, torch_dtype=torch.bfloat16, device_map="auto")

接下來,我們將設定我們的工具和要使用的聊天。讓我們使用上面的 `get_current_temperature` 示例。

def get_current_temperature(location: str):
    """
    Gets the temperature at a given location.

    Args:
        location: The location to get the temperature for, in the format "city, country"
    """
    return 22.0  # bug: Sometimes the temperature is not 22. low priority to fix tho

tools = [get_current_temperature]    

chat = [
    {"role": "user", "content": "Hey, what's the weather like in Paris right now?"}
]

tool_prompt = tokenizer.apply_chat_template(
    chat, 
    tools=tools, 
    return_tensors="pt",
    return_dict=True,
    add_generation_prompt=True,
)
tool_prompt = tool_prompt.to(model.device)

現在我們準備根據模型可訪問的工具來生成模型對使用者查詢的響應。

out = model.generate(**tool_prompt, max_new_tokens=128)
generated_text = out[0, tool_prompt['input_ids'].shape[1]:]

print(tokenizer.decode(generated_text))

然後我們得到

<tool_call>
{"arguments": {"location": "Paris, France"}, "name": "get_current_temperature"}
</tool_call><|im_end|>

模型已請求工具!請注意,它如何正確推斷應傳遞引數“Paris, France”而不是僅僅“Paris”,因為這是函式文件字串推薦的格式。

然而,模型並沒有真正對工具進行程式設計訪問——像所有語言模型一樣,它只是生成文字。作為程式設計師,您需要根據模型的請求來呼叫函式。不過,首先,讓我們將模型的工具請求新增到聊天中。

請注意,此步驟可能需要一些手動處理——儘管您應該始終以下面的格式將請求新增到聊天中,但工具呼叫請求的文字(例如 `` 標籤)在不同模型之間可能有所不同。通常,它非常直觀,但請記住,當您在自己的程式碼中嘗試此操作時,可能需要一些模型特定的 `json.loads()` 或 `re.search()`!

message = {
    "role": "assistant", 
    "tool_calls": [
        {
            "type": "function", 
            "function": {
                "name": "get_current_temperature", 
                "arguments": {"location": "Paris, France"}
            }
        }
    ]
}
chat.append(message)

現在,我們實際上在 Python 程式碼中呼叫工具,並將其響應新增到聊天中。

message = {
    "role": "tool", 
    "name": "get_current_temperature", 
    "content": "22.0"
}
chat.append(message)

最後,就像我們之前做的那樣,我們格式化更新後的聊天並將其傳遞給模型,以便它可以在對話中使用工具響應。

tool_prompt = tokenizer.apply_chat_template(
    chat, 
    tools=tools, 
    return_tensors="pt",
    return_dict=True,
    add_generation_prompt=True,
)
tool_prompt = tool_prompt.to(model.device)

out = model.generate(**tool_prompt, max_new_tokens=128)
generated_text = out[0, tool_prompt['input_ids'].shape[1]:]

print(tokenizer.decode(generated_text))

我們獲得了對使用者的最終回覆,該回復是使用中間工具呼叫步驟的資訊構建的。

The current temperature in Paris is 22.0 degrees Celsius. Enjoy your day!<|im_end|>

遺憾的響應格式不統一問題

在閱讀此示例時,您可能已經注意到,儘管聊天模板在將聊天和工具定義轉換為格式化文字時可以隱藏模型特定的差異,但反過來卻並非如此。當模型發出工具呼叫時,它將以自己的格式進行,因此您目前需要手動解析它,然後才能以通用格式將其新增到聊天中。值得慶幸的是,大多數格式都非常直觀,因此這應該只需要幾行 `json.loads()`,或者最壞情況下,一個簡單的 `re.search()` 來建立您需要的工具呼叫字典。

不過,這仍然是過程中“未統一”的最大部分。我們有一些解決它的想法,但它們還沒有完全準備好投入使用。正如孩子們所說,“讓我們再琢磨琢磨。”

總結

儘管存在上述一些小問題,我們認為這與之前工具使用分散、混亂且文件不足的情況相比,是一個巨大的進步。我們希望這能讓開源開發者更容易地將工具使用納入他們的專案,用一系列工具來增強強大的 LLM,從而增加驚人的新功能。從像 Hermes-2-Pro-8B 這樣的小模型到像 Mistral-LargeCommand-R-PlusLlama-3.1-405B 這樣龐大的最先進模型,許多前沿的 LLM 現在都支援工具使用。我們認為工具將成為下一波 LLM 產品不可或缺的一部分,我們希望這些改變能讓您更輕鬆地在自己的專案中使用它們。祝您好運!

社群

非常棒的文章。寫得非常清楚。

不過我有一個問題。模型似乎要麼回覆(i)文字響應,要麼回覆(ii)工具呼叫。然而,在原始的 ReAct 論文中,存在一個“思考”->“行動”->“觀察”的迴圈。換句話說,為了響應使用者的查詢,模型首先輸出一個“思考”,然後是“行動”。我該如何實現這一點(即讓模型在執行工具呼叫之前“思考”)?

以下是 HotpotQA 的原始 ReAct 提示(來自 ReAct 官方 GitHub 儲存庫):https://raw.githubusercontent.com/ysymyth/ReAct/refs/heads/master/prompts/prompts_naive.json

如果您檢查這些提示,您會注意到“思考”出現在“行動”之前。

註冊登入以評論

© . This site is unofficial and not affiliated with Hugging Face, Inc.