都在搞MCP,但是我還是要講一下大模型的函數(shù)調(diào)用
函數(shù)調(diào)用(function calling)是一種機制,允許大語言模型(LLM)通過調(diào)用外部函數(shù)或 API 執(zhí)行特定的、預(yù)定義的任務(wù)。可以將其視為一種功能,讓 LLM 將它無法獨立完成的工作“委托”出去。
例如,假設(shè)您向 LLM 發(fā)送以下提示:“特斯拉當(dāng)前的股價是多少?”
沒有函數(shù)調(diào)用的基本 LLM 可能會根據(jù)其訓(xùn)練數(shù)據(jù)中的模式“幻覺”出一個答案,例如“可能在 200 美元左右”。經(jīng)過 RLHF 優(yōu)化的模型可能會更誠實地說:“我沒有實時數(shù)據(jù),所以無法告訴你。”
讓我們用 Qwen2.5 0.5B Instruct 快速嘗試一下:
from transformers importAutoTokenizer,AutoModelForCausalLM
model_name ="Qwen/Qwen2.5-0.5B-Instruct"
model =AutoModelForCausalLM.from_pretrained(model_name)
tokenizer =AutoTokenizer.from_pretrained(model_name)
prompt ="特斯拉當(dāng)前的股價是多少?"
inputs = tokenizer(prompt, return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=30, do_sample=True)
print(tokenizer.decode(outputs[0]))
輸出:
特斯拉當(dāng)前的股價是多少?
抱歉,作為一個 AI 語言模型,我無法訪問實時金融數(shù)據(jù)。特斯拉的股價可能會快速變化。
正如預(yù)期,Qwen 表示它不知道,這與任何優(yōu)秀的指令模型一致。
但通過函數(shù)調(diào)用,LLM 可以識別對實時股價數(shù)據(jù)的需求,觸發(fā)對金融服務(wù)的 API 調(diào)用,然后生成類似“截至此刻,特斯拉的股價為 279.24 美元”的回答。
顯然,函數(shù)調(diào)用是釋放 LLM 全部潛力的關(guān)鍵功能,了解如何使用它至關(guān)重要。
現(xiàn)在,像 OpenAI GPT-4 這樣的專有模型通過 OpenAI API 原生支持函數(shù)調(diào)用。它們使用起來更簡單,因為一切都在 OpenAI 生態(tài)系統(tǒng)中處理。
Mistral AI 的許多模型(如 Mistral Small 或 Mistral Nemo)也是如此,這些模型是開源的,但也通過 API 提供。
讓我展示一個使用最新 Mistral Small 的函數(shù)調(diào)用示例。您需要一個 Mistral API 密鑰來運行此示例。
順便說一句,Mistral 的模型被嚴(yán)重低估。只要用幾次,你就會發(fā)現(xiàn)它們有多優(yōu)秀。
首先,安裝 Mistral AI 的 Python 客戶端:
pip3 install mistralai
pip3 list | grep mistralai
輸出:
mistralai 1.6.0
接下來,我們需要一個 LLM 可以調(diào)用的函數(shù)。我將使用 Flask 將該函數(shù)作為 REST 端點提供,該函數(shù)僅列出本地文件系統(tǒng)中指定目錄的所有文件。
from flask importFlask, request
from os import path, listdir
app =Flask(__name__)
@app.route('/files')
def list_files():
directory = request.args.get('directory','.')
files =(
[{'name': f}for f in listdir(directory)
if path.isfile(path.join(directory, f))
]
)
return files
if __name__ =='__main__':
app.run(host='0.0.0.0', port=8000)
您可以使用 curl 從命令行測試此端點,或者使用您喜歡的瀏覽器。
curl "http://localhost:8000/files?directory=/tmp"
輸出:
[{"name":"api.py"}]
現(xiàn)在,我們需要告訴 LLM 這個函數(shù)存在,并且在適當(dāng)?shù)臅r候使用它。因為我們?nèi)栽谑褂?Mistral LLM(目前是 mistral-small-2503),這相當(dāng)簡單。您只需以 LLM 能理解的格式描述該函數(shù)。對于大多數(shù) LLM,格式如下:
available_functions =[
{
"type":"function",
"function":{
"name":"files",
"description":"列出目錄中的文件",
"parameters":{
"type":"object",
"properties":{
"directory":{
"type":"string",
"description":"目錄的絕對路徑"
},
},
"required":["directory"]
}
}
}
]
當(dāng)然,函數(shù)描述越詳細、信息量越大,效果越好。
此時,您只需將 ??available_functions?
? 數(shù)組與提示一起傳遞給客戶端。
from mistralai importMistral
api_key ="..."
model ="mistral-small-2503"
mistral =Mistral(api_key)
response = mistral.chat.complete(
model = model,
messages =[
{
"role":"user",
"content":"列出 /tmp 目錄中的所有文件。"
}
],
tools = available_functions,
tool_choice ="any"
)
print(response.choices[0].message.tool_calls[0])
輸出:
function=FunctionCall(
name='files',
arguments='{"directory": "/tmp"}'
)
id='12bc4' type=None index=0
如您所見,Mistral Small 成功根據(jù)給定的提示推斷出需要調(diào)用 ??files?
?? 函數(shù),并使用參數(shù) ??/tmp?
?。
不過,它不會自己調(diào)用函數(shù)。這是我們需要做的事情。因此,我們檢查 ??tool_calls?
? 數(shù)組,如果它不為空,我們就運行 Mistral 希望我們運行的函數(shù)。
在我們的例子中,因為我們有一個 REST 端點,我們可以使用 ??requests?
? 庫進行 GET 請求。
import json
import requests
if response.choices[0].message.tool_calls:
tool_call = response.choices[0].message.tool_calls[0]
if tool_call.function.name =="files":
output = requests.get(
"http://localhost:8000/files?directory="+
json.loads(
tool_call.function.arguments
)["directory"]
).json()
print(output)
輸出:
[{'name':'qq1.html'}]
請注意,參數(shù)目前是以字符串形式到達,我們需要先將其轉(zhuǎn)換為 JSON。這就是代碼中包含 ??json.loads()?
? 的原因。
還值得注意的是,LL彼此之間,LLM 在這里沒有生成文本。輸出到標(biāo)準(zhǔn)輸出(stdout)應(yīng)該是您的函數(shù)。
很好,現(xiàn)在我們知道如何使用通過 API 訪問的 LLM 進行函數(shù)調(diào)用。
在本文的其余部分,我們將使用可以本地運行的開源模型。許多這些模型支持函數(shù)調(diào)用,但使用該功能需要額外步驟。您會發(fā)現(xiàn),這些模型照常生成 token,作為開發(fā)者的您需要解析、解釋并使用這些 token 來觸發(fā)函數(shù)。
讓我們看看是否可以直接將 ??available_functions?
? 數(shù)組傳遞給 Llama 3.2–1B Instruct 以使其調(diào)用我們的函數(shù)。
我們首先加載模型:
from transformers importAutoTokenizer,AutoModelForCausalLM
model_name ="meta-llama/Llama-3.2-1B-Instruct"
model =AutoModelForCausalLM.from_pretrained(model_name)
tokenizer =AutoTokenizer.from_pretrained(model_name)
接下來,我們需要將提示包裝在聊天模板中,而不是直接傳遞給模型。聊天模板應(yīng)包含 ??available_functions?
? 數(shù)組,以指定模型可以使用的工具。
messages =[
{"
role": "system", "content": "你是一個有用的助手。"},
{"role":"user","content":"/tmp 目錄中有哪些文件?"},
]
template= tokenizer.apply_chat_template(
messages, tools=available_functions,
tokenize=False
)
最后,我們對模板進行 token 化,并將其作為輸入傳遞給模型。
inputs = tokenizer(template, return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=30, do_sample=True)
print(tokenizer.decode(outputs[0]))
輸出:
...
...
<|python_tag|>
{"type":"function","function":"files",
"parameters":{"directory":"/tmp"}}
<|eom_id|>
看起來 Llama 3.2 也成功識別了函數(shù)調(diào)用。這很好!但函數(shù)調(diào)用目前只是一個(JSON 格式的)字符串,被 ??<|python_tag|>?
?? 和 ??<|eom_id|>?
?? 包裹。??eom_id?
? 只是一個特殊的消息結(jié)束 token。
提取模型輸出的函數(shù)調(diào)用有很多方法。我將保持簡單,使用正則表達式。
import json
import re
generated_text = tokenizer.decode(outputs[0])
matched = re.search(
r"<\|python_tag\|>(.*?)<\|eom_id\|>",
generated_text, re.DOTALL
)
function_call = json.loads(matched.group(1).strip())
print(function_call)
輸出:
{'type':'function','function':'files',
'parameters':{'directory':'/tmp'}}
現(xiàn)在您知道如何使用 Llama 進行函數(shù)調(diào)用。
如果從 Llama 切換到 Qwen,變化不大。但 Qwen 使用一組不同的 token 來包裹函數(shù)調(diào)用。以下是 Qwen 的輸出:
<|im_start|>assistant
<tool_call>
{"name":"files","arguments":{"directory":"/tmp"}}
</tool_call><|im_end|>
因此,您的正則表達式需要稍作調(diào)整以處理這種新格式。
本文轉(zhuǎn)載自????PyTorch研習(xí)社????,作者:PyTorch研習(xí)社
