對 LLM 工具使用進行統一

HuggingFace發表於2024-09-26

我們為 LLM 確立了一個跨模型的 統一工具呼叫 API。有了它,你就可以在不同的模型上使用相同的程式碼,在 MistralCohereNousResearchLlama 等模型間自由切換,而無需或很少需要根據模型更改工具呼叫相關的程式碼。此外,我們還在 transformers 中新增了一些實用介面以使工具呼叫更絲滑,我們還為此配備了 完整的文件 以及端到端工具使用的 示例。我們會持續新增更多的模型支援。

引言

LLM 工具使用這個功能很有意思 —— 每個人都認為它很棒,但大多數人從未親測過。它的概念很簡單: 你給 LLM 提供一些工具 (即: 可呼叫的函式),LLM 在響應使用者的查詢的過程中可自主判斷、自行呼叫它們。比方說,你給它一個計算器,這樣它就不必依賴其自身不靠譜的算術能力; 你還可以讓它上網搜尋或檢視你的日曆,或者授予它訪問公司資料庫的許可權 (只讀!),以便它可以提取相應資訊或搜尋技術文件。

工具呼叫使得 LLM 可以突破許多自身的核心限制。很多 LLM 口齒伶俐、健談,但涉及到計算和事實時往往不夠精確,並且對小眾話題的具體細節不甚瞭解。它們還不知道訓練資料截止日期之後發生的任何事情。它們是通才,但除了你在系統訊息中提供的資訊之外,它們在開始聊天時對你或聊天背景一無所知。工具使它們能夠獲取結構化的、專門的、相關的、最新的資訊,這些資訊可以幫助其成為真正有幫助的合作伙伴,而不僅僅是令人著迷的新奇玩意兒。

然而,當你開始真正嘗試工具使用時,問題出現了!文件很少且互相之間不一致,甚至矛盾 —— 對於閉源 API 和開放模型無不如此!儘管工具使用在理論上很簡單,但在實踐中卻常常成為一場噩夢: 如何將工具傳遞給模型?如何確保工具提示與其訓練時使用的格式相匹配?當模型呼叫工具時,如何將其合併到聊天提示中?如果你曾嘗試過動手實現工具使用,你可能會發現這些問題出奇棘手,而且很多時候文件並不完善,有時甚至會幫倒忙。

更糟糕的是,不同模型的工具使用的實現可能迥異。即使在定義可用工具集這件最基本的事情上,一些模型廠商用的是 JSON 模式,而另一些模型廠商則希望用 Python 函式頭。即使那些希望使用 JSON 模式的人,細節上也常常會有所不同,因此造成了巨大的 API 不相容性。看!使用者被摁在地板上瘋狂摩擦,同時內心困惑不已。

為此,我們能做些什麼呢?

聊天模板

Hugging Face Cinematic Universe 的忠​​粉會記得,開源社群過去在 聊天模型 方面也面臨過類似的挑戰。聊天模型使用 <|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"
)

apply_chat_template 內部, 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)

實操

我們把上述程式碼串聯起來搭建一個完整的工具使用示例。如果你想在自己的專案中使用工具,我們建議你嘗試一下我們的程式碼 - 嘗試自己執行它,新增或刪除工具,換個模型並調整細節以感受整個系統。當需要在軟體中實現工具使用時,這種熟悉會讓事情變得更加容易!為了讓它更容易,我們還提供了這個示例的 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”,這是因為它遵循了函式文件字串推薦的格式。

但模型並沒有真正以程式設計方式呼叫這些工具,就像所有語言模型一樣,它只是生成文字。作為程式設計師,你需要接受模型的請求並呼叫該函式。首先,我們將模型的工具請求新增到聊天中。

請注意,此步驟可能需要一些手動處理 - 儘管你應始終按照以下格式將請求新增到聊天中,但模型呼叫工具的請求文字 (如 <tool_call> 標籤) 在不同模型之間可能有所不同。通常,它非常直觀,但請記住,在你自己的程式碼中嘗試此操作時,你可能需要一些特定於模型的 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 產品不可或缺的一部分,我們希望我們做的這些改進能讓你更輕鬆地在自己的專案中使用它們。祝你好運!


英文原文: https://hf.co/blog/unified-tool-use

原文作者: Matthew Carrigan

譯者: Matrix Yao (姚偉峰),英特爾深度學習工程師,工作方向為 transformer-family 模型在各模態資料上的應用及大規模模型的訓練推理。

相關文章