工具使用,统一化
目前,多个流行模型系列已经有了**统一的工具使用 API**。这个 API 意味着相同的代码具有可移植性——在使用 Mistral、Cohere、NousResearch 或 Llama 模型进行聊天时,几乎不需要或根本不需要模型特定的更改即可使用工具。此外,Transformers 现在还包含了辅助功能,使工具调用更加容易,并提供了完整的文档和示例,涵盖了整个工具使用过程。未来还将增加对更多模型的支持。
引言
工具使用是一个有趣的特性——每个人都认为它很棒,但大多数人并没有亲自尝试过。从概念上讲,它非常简单:您给您的 LLM 一些工具(可调用函数),然后它就可以决定调用这些工具来帮助它响应用户查询。也许您给它一个计算器,这样它就不必依赖其内部不可靠的算术能力。也许您让它搜索网页或查看您的日历,或者您给它(只读!)访问公司数据库的权限,这样它就可以提取信息或搜索技术文档。
工具使用克服了 LLM 的许多核心限制。许多 LLM 流畅且健谈,但计算和事实往往不精确,对更小众主题的具体细节也模糊不清。它们不知道训练截止日期之后发生的任何事情。它们是通才;它们进入对话时,除了您在系统消息中提供给它们的信息之外,对您或您的工作场所一无所知。工具为它们提供了访问结构化、具体、相关和最新信息的权限,这对于将它们变成真正有用的伙伴而非仅仅是迷人的新奇事物非常有帮助。
然而,当您真正尝试实施工具使用时,问题就出现了。文档通常稀疏、不一致,甚至相互矛盾——对于闭源 API 和开放访问模型都是如此!尽管工具使用在理论上很简单,但在实践中却常常变成一场噩梦:如何将工具传递给模型?如何确保工具提示与模型训练时使用的格式匹配?当模型调用工具时,如何将其整合到聊天中?如果您以前尝试过实施工具使用,您可能会发现这些问题出奇地棘手,并且文档并非总是完整和有用的。
更糟糕的是,不同的模型在工具使用实现上可能有天壤之别。即使在定义可用工具的最基本层面,有些提供商期望 JSON 模式,而另一些则期望 Python 函数头。即使在那些期望 JSON 模式的提供商中,细微的细节也常常不同,从而造成巨大的 API 不兼容性。这产生了很大的摩擦,通常只会加深用户的困惑。那么,我们能做些什么来解决所有这些问题呢?
聊天模板
Hugging Face 电影宇宙的忠实粉丝们会记得,开源社区过去曾面临着与**聊天模型**类似的挑战。聊天模型使用 `<|start_of_user_turn|>` 或 `<|end_of_message|>` 等控制标记,让模型了解聊天中发生的事情,但不同模型使用完全不同的控制标记进行训练,这意味着用户需要为他们想要使用的每个模型编写特定的格式化代码。这在当时是一个巨大的麻烦。
我们对此的解决方案是**聊天模板**——本质上,模型会附带一个微小的 Jinja 模板,它会以正确的格式和控制标记为每个模型渲染聊天。聊天模板意味着用户可以用通用、与模型无关的格式编写聊天,相信 Jinja 模板会处理所需的任何模型特定格式。
那么,支持工具使用的明显方法就是扩展聊天模板以也支持工具。这正是我们所做的,但工具为模板系统带来了许多新的挑战。让我们来回顾一下这些挑战以及我们是如何解决它们的。在这个过程中,希望您能更深入地了解该系统的工作原理以及如何让它为您服务。
将工具传递给聊天模板
我们设计工具使用 API 的首要标准是,它应该能直观地定义工具并将其传递给聊天模板。我们发现大多数用户会先编写工具函数,然后弄清楚如何从这些函数生成工具定义并将其传递给模型。这导致了一种显而易见的方法:如果用户可以直接将函数传递给聊天模板,并让它为他们生成工具定义呢?
然而,这里的问题是“传递函数”是一种非常特定于语言的操作,许多人通过JavaScript 或Rust 而不是 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”,因为这是函数文档字符串推荐的格式。
然而,模型并没有真正对工具进行编程访问——像所有语言模型一样,它只是生成文本。作为程序员,您需要根据模型的请求来调用函数。不过,首先,让我们将模型的工具请求添加到聊天中。
请注意,此步骤可能需要一些手动处理——尽管您应该始终以下面的格式将请求添加到聊天中,但工具调用请求的文本(例如 `
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-Large、Command-R-Plus 或 Llama-3.1-405B 这样庞大的最先进模型,许多前沿的 LLM 现在都支持工具使用。我们认为工具将成为下一波 LLM 产品不可或缺的一部分,我们希望这些改变能让您更轻松地在自己的项目中使用它们。祝您好运!