Всем привет, меня зовут Алан, я разработчик-исследователь из команды фундаментальных исследований MTS AI. Мы изучаем возможности генеративного ИИ, и видим, что большие языковые модели отлично справляются с различными текстовыми задачами, но мы можем расширить их функционал. Например, пока что LLM не может правильно посчитать логарифм, узнать погоду или какую-то другую информацию. Как решить эту задачу? Нужно научить модель пользоваться внешними инструментами/функциями. В этой статье мы поговорим о вызове функций с помощью больших языковых моделей, рассмотрим некоторые проприетарные и открытые модели, связанные исследования, а затем проведем небольшой эксперимент с отправкой электронной почты при помощи LLM.
Что нужно знать про function calling
Вызов функций или Function calling — это концепция, которая позволяет LLM использовать внешние инструменты. Как уже было сказано ранее, LLM зачастую просто не умеют выполнять какие-то операции, и для этого нам придется научить языковую модель вызывать функции при необходимости.
Такие LLM как GPT-4 и GPT-3.5 были дообучены самостоятельно определять, когда необходимо вызвать функцию, а затем генерировать JSON, содержащий имя нужной функции, и аргументы для вызова этой функции. Например, запрос «Какая погода в Москве?» будет преобразован в вызов функции get_current_weather(location: string, unit: 'celsius' | 'fahrenheit')
Базовая схема вызова функций
Схема вызова функций является общей для всех языковых моделей и выглядит следующим образом:
Передаем в LLM промпт пользователя и описание доступных функций/инструментов.
Модель сопоставляет промпт пользователя с описанием функций. Если LLM решает, что для выполнения запроса требуется вызов одной или нескольких функций, то она возвращает JSON с именем и аргументами функции (модель может их придумать).
Вызываем функцию в коде.
Передаем результаты выполнения функции обратно ассистенту, затем он генерирует ответ с суммаризацией результатов (если они есть).
Скрытый текст
Схема работы
Большинство проприетарных function calling LLM умеют работать в нескольких режимах:
режим Auto позволяет языковой модели решить, требуется ли вызывать функцию и вернуть JSON или сгенерировать ответ на инструкцию пользователя;
в режиме Required модель всегда будет вызывать одну или несколько функций;
None предполагает, что модель не будет вызывать какие-либо функции
Также модель можно "заставить" вызывать одну конкретную функцию.
Существующие решения
Возможность вызова функций есть у многих языковых моделей как проприетарных, так и опенсорсных. В целом их возможности одинаковы, отличается лишь формат предоставления данных.
OpenAI
API Chat Completions не вызывает функцию, вместо этого модель генерирует JSON, который мы можем использовать для вызова функции в своем коде. Такие модели, как gpt-4o, gpt-4-turbo и gpt-3.5-turbo, обучены определять, когда следует вызывать функцию (в зависимости от входных данных), а также умеют возвращать JSON, который соответствует нужной функции.
OpenAI настоятельно рекомендует создавать этапы подтверждения со стороны пользователей, прежде чем предпринимать действия, которые могут повлиять на мир (отправка электронной почты, публикация чего-либо в Интернете, совершение покупок и т. д.).
Формат описания передаваемой в модель функции выглядит следующим образом (в данном примере это вызов Weather API):
Скрытый текст
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
},
}
]
Gemini
При использовании Gemini API мы создаем одну или несколько функций, а затем добавляем их в tools object, передаваемый модели. Каждое описание функции включает в себя следующее:
Имя функции
Параметры функции
Описание функции
Gemini API также поддерживает параллельный вызов функций, при котором мы можем вызвать несколько функций за раз.
На данный момент function calling поддерживают следующие модели:
gemini-1.0-pro
gemini-1.0-pro-001
gemini-1.5-flash-latest
gemini-1.5-pro-latest
Mistral-7B-Instruct-v0.3🤗
Новая версия мистраля mistralai/Mistral-7B-Instruct-v0.3 теперь также поддерживает function calling с помощью библиотеки mistral_inference:
Скрытый текст
from mistral_common.protocol.instruct.tool_calls import Function, Tool
from mistral_inference.model import Transformer
from mistral_inference.generate import generate
from mistral_common.tokens.tokenizers.mistral import MistralTokenizer
from mistral_common.protocol.instruct.messages import UserMessage
from mistral_common.protocol.instruct.request import ChatCompletionRequest
tokenizer = MistralTokenizer.from_file(f"{mistral_models_path}/tokenizer.model.v3")
model = Transformer.from_folder(mistral_models_path)
completion_request = ChatCompletionRequest(
tools=[
Tool(
function=Function(
name="get_current_weather",
description="Get the current weather",
parameters={
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use. Infer this from the users location.",
},
},
"required": ["location", "format"],
},
)
)
],
messages=[
UserMessage(content="What's the weather like today in Moscow?"),
],
)
tokens = tokenizer.encode_chat_completion(completion_request).tokens
out_tokens, _ = generate([tokens], model, max_tokens=64, temperature=0.0, eos_id=tokenizer.instruct_tokenizer.tokenizer.eos_id)
result = tokenizer.instruct_tokenizer.tokenizer.decode(out_tokens[0])
print(result)
Gorilla🤗
Gorilla OpenFunctions-v2 — это модель примерно с 7 миллиардами параметров, дообученная на основе Deepseek-Coder-7B-Instruct-v1.5 6.91B. Данные, на которых обучалась модель, представляют собой 65 283 сэмпла вопрос-функция-ответ из разных источников: Python packages (19 353), репозиториев Java (16 586), репозиториев Javascript (4 245), общедоступных API (6 009) и инструментов командной строки (19 090)
После сбора данных была проведена аугментация, чтобы разнообразить датасет. Во-первых, для того, чтобы модель не запоминала маппинг API, были изменены названия функций. Во-вторых, были добавлены сэмплы с параллельным вызовом функций. Также были добавлены примеры, где переданных в запросе функций недостаточно для решения пользовательской задачи
Glaive🤗
glaive-function-calling-v1 — это модель с открытым исходным кодом с 2,7 млрд параметров, обученная на синтетических данных. Модель способна вести диалог и понимает когда необходимо вызвать функцию (предоставляемую в начале диалога в виде системной подсказки). Модель обучена на основе модели https://huggingface.co/replit/replit-code-v1-3b.
Набор данных Glaive состоит из 52 тысяч сэмплов в следующем формате
SYSTEM: You are an helpful assistant who has access to the following functions to help the user, you can use the functions if needed-
{
JSON function definiton
}
USER: user message
ASSISTANT: assistant message
Function call invocations are formatted as-
ASSISTANT: <functioncall> {json function call}
Response to the function call is formatted as-
FUNCTION RESPONSE: {json function response}
NexusRaven🤗
Nexusflow/NexusRaven-V2-13B- это LLM, дообученная на синтетических данных на основе CodeLlama-13B. Модель показывает хорошую производительность относительно GPT- 4 на function calling бенчмарках
Скрытый текст
Functionary 🤗
Functionary — это языковая модель, которая может интерпретировать и выполнять функции/плагины. Модель определяет, когда выполнять функции, как именно — параллельно или последовательно — и может понимать результат их выполнения. Functionary запускает функции только по мере необходимости. Определения функций предоставляются в виде JSON, аналогично вызовам функций с помощью OpenAI.
Помимо этого functionary отлично показывает себя относительно других моделей в задачах поддержки диалога и генерации ответа на основе результатов выполнения функций
Скрытый текст
Связанные исследования
Далее мы коротко рассмотрим различные подходы для вызова функций из нескольких статей, которые касаются темы function calling
Toolformer
Toolformer — это модель, дообученная решать, какие API вызывать, когда их вызывать, какие аргументы передавать и как лучше всего включать результаты в генерируемую последовательность. Это производится с помощью метода self-supervised, который требует несколько демонстраций для каждого API. Toolformer обеспечивает существенное улучшение производительности при выполнении множества задач, конкурируя с более крупными моделями, при этом не жертвуя языковым моделированием
Chain of Tools
Самая крутая работа (на мой взгляд), которая фокусируется на вызове цепочки функций, где вызов каждой последующей функция зависит от результатов работы предыдущей. Задача планирования решается с помощью генерации специальной программы, которая будет последовательно запускать необходимые функции, как это показано на рисунке (справа). Для имплементации подойдут LLM, которые умеют хорошо генерировать код.
TPTU-v2
TPTU-v2 это фреймворк, состоящий из трех основных компонентов
API Retriever. Как понятно из названия, этот компонент находит наиболее релевантные пользовательскому запроса API
LLMFinetuner. Этот компонент файнтюнит LLM с помощью тщательно подобранного датасета, расширяя возможности модели по планированию и эффективному выполнению вызовов API
Demo Selector. Demo selector динамически извлекает демонстрации, связанные с трудно распознаваемыми вызовами API, что облегчает контекстное обучение для LLM
EasyTool
EasyTool — это платформа, преобразующая документацию по внешним инструментам в краткую инструкцию, упрощающую их использование агентами. EasyTool фильтрует важную информацию из документации и генерирует унифицированный интерфейс, предлагающий стандартизированные описания инструментов и функциональные возможности для LLM-агентов. Эксперименты с множеством различных задач показывают, что EasyTool может значительно сократить расход токенов и повысить производительность LLM-агентов при вызове функций в реальных сценариях.
ToolQA
ToolQA — это бенчмарк, предназначенный для оценки способности LLM использовать внешние инструменты для ответов на вопросы. ToolQA включает данные из 8 доменов и определяет 13 типов инструментов для извлечения информации из внешних источников. Каждый сэмпл в ToolQA состоит из вопроса, ответа, источника данных и списка доступных инструментов. Бенчмарк ToolQA уникален тем, что на все его вопросы можно ответить только используя соответствующие инструменты для извлечения информации из источника данных. Это сводит к минимуму возможность того, что LLM будут отвечать на вопросы, просто извлекая свои внутренние знания, и это позволяет достоверно оценить способности LLM в использовании инструментов
Эксперименты
В этой части статьи мы покажем, как уже сейчас попробовать вызов функций с помощью одной проприетарной модели от Google и нескольких решений с открытым исходным кодом. Во всех экспериментах мы будем отправлять почту с помощью библиотеки smtplib.
Gemini function calling
Для работы с Gemini понадобиться использование VPN стран из разрешенных локаций (например Япония)
Для начала установим библиотеку для работы с генеративным ИИ от Google
pip install -U -q google-generativeai
Импортируем все необходимое
import textwrap
import google.generativeai as genai
from IPython.display import Markdown
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
import smtplib as smtp
from getpass import getpass
def to_markdown(text):
text = text.replace('•', ' *')
return Markdown(textwrap.indent(text, '> ', predicate=lambda _: True))
Сгенерируем и сконфигурируем свой API key
genai.configure(api_key=api_key)
Определим функции, которые Gemini сможет вызывать по необходимости
def multiply(a:float, b:float):
"""returns a * b."""
return a*b
def send_email(destination_email: str, message_text: str):
"""Send message to destination_email with message_text"""
email = 'your_yandex_mail'
password = 'your_password'
dest_email = destination_email
subject = 'Test'
email_text = message_text
message = 'From: {}\nTo: {}\nSubject: {}\n\n{}'.format(email,
dest_email,
subject,
email_text)
server = smtp.SMTP_SSL('smtp.yandex.com')
server.set_debuglevel(1)
server.ehlo(email)
server.login(email, password)
server.auth_plain()
server.sendmail(email, dest_email, message)
server.quit()
Передаем функции в модель (будем использовать gemini-1.5-flash)
model = genai.GenerativeModel(model_name='gemini-1.5-flash',
tools=[multiply, send_email])
Инициализируем чат и отправляем запрос
chat = model.start_chat(enable_automatic_function_calling=True)
response = chat.send_message('I have 57 cats, each owns 44 mittens, how many mittens is that in total?')
response.text
>>>"That's a total of 2508 mittens. \n
Как мы видим, Gemini воспользовалась функцией перемножения двух чисел, которую мы определили ранее. Теперь попробуем отправить сообщение:
response = chat.send_message('Send a message to alanrbtx@gmail.com about the MTS AI meeting on June 13. And tell him to call my number back. Be kind')
response.text
В результате мне на почту прилетело следующее письмо(в спойлере)
Скрытый текст
Давайте посмотрим что происходит по капотом:
for content in chat.history:
part = content.parts[0]
print(content.role, "->", type(part).to_dict(part))
print('-'*80)
>>> user -> {'text': 'Send a message to alanrbtx@gmail.com about the MTS AI meeting on June 13. And tell him to call my number back. Be kind'}
>>> --------------------------------------------------------------------------------
>>> model -> {'function_call': {'name': 'send_email', 'args': {'message_text': 'Hi Alan, this is Newton. Just a friendly reminder about the MTS AI meeting on June 13th. Please give me a call back at your earliest convenience. Thanks!', 'destination_email': 'alanrbtx@gmail.com'}}}
>>> --------------------------------------------------------------------------------
>>> user -> {'function_response': {'name': 'send_email', 'response': {'result': None}}}
>>> --------------------------------------------------------------------------------
>>> model -> {'text': "OK. I've sent an email to alanrbtx@gmail.com. Let me know if you'd like me to add anything else to the message. \n"}
>>> --------------------------------------------------------------------------------
Google.generativeai сама выполняет функции под капотом, нам не требуется отдельно запускать функции на основе вывода модели. Помимо этого gemini продолжает работать в режиме диалога и генерирует дополнительный текст, который говорит нам о том, что сообщение отправлено
gorilla-llm function calling
В этом и следующих примерах мы попробуем вызвать функцию с помощью моделей с отрытым исходным кодом.
Для начала импортируем все необходимое и проинициализуем модель с токенайзером. Мы будем использовать модель gorilla-llm/gorilla-openfunctions-v2
import json
import smtplib as smtp
from getpass import getpass
from transformers import AutoTokenizer, AutoModelForCausalLM
import re
import ast
tokenizer = AutoTokenizer.from_pretrained("gorilla-llm/gorilla-openfunctions-v2")
model = AutoModelForCausalLM.from_pretrained("gorilla-llm/gorilla-openfunctions-v2")
Функция отправки сообщений из предыдущего примера никак не меняется. В отличие от работы с Gemini, теперь нам придется предоставить LLM понятное для нее описание функции:
functions = [
{
"name": "send_email",
"description": "Send message to user_email with message_text",
"parameters": {
"type": "object",
"properties": {
"destination_email": {
"type": "string",
"description": "Destination email",
},
"message_text": {
"type": "string",
"description": "Text of message",
},
},
"required": ["destination_email", "message_text"],
},
}
]
Преобразуем промпт, для того чтобы подать в модель описание функции и инструкцию пользователя:
def get_prompt(user_query: str, functions: list = []) -> str:
"""
Generates a conversation prompt based on the user's query and a list of functions.
Parameters:
- user_query (str): The user's query.
- functions (list): A list of functions to include in the prompt.
Returns:
- str: The formatted conversation prompt.
"""
system = "You are an AI programming assistant, utilizing the Gorilla LLM model, developed by Gorilla LLM, and you only answer questions related to computer science. For politically sensitive questions, security and privacy issues, and other non-computer science questions, you will refuse to answer."
if len(functions) == 0:
return f"{system}\n### Instruction: <<question>> {user_query}\n### Response: "
functions_string = json.dumps(functions)
return f"{system}\n### Instruction: <<function>>{functions_string}\n<<question>>{user_query}\n### Response: "
sentence = get_prompt("Send a message to alanrbtx@gmail.com about the MTS AI meeting on June 13. And tell him to call my number back. Be kind", functions=functions)
Теперь промпт, подаваемый в модель, выглядит следующим образом:
You are an AI programming assistant, utilizing the Gorilla LLM model,
developed by Gorilla LLM, and you only answer questions related to computer science.
For politically sensitive questions, security and privacy issues,
and other non-computer science questions, you will refuse to answer.
### Instruction: <<function>>[
{
"name": "send_email",
"description": "Send message to user_email with message_text",
"parameters": {
"type": "object",
"properties": {
"destination_email": {
"type": "string",
"description": "Destination email",
},
"message_text": {
"type": "string",
"description": "Text of message",
},
},
"required": ["destination_email", "message_text"],
},
}
<<question>>Send a message to alanrbtx@gmail.com about the MTS AI meeting on June 13.
And tell him to call my number back. Be kind
### Response: '
Запускаем генерацию модели:
tokenized = tokenizer(sentence, return_tensors='pt')
res = model.generate(**tokenized, max_length=500)
Вывод модели выглядит следующим образом:
"send_email(
destination_email='alanrbtx@gmail.com',
message_text='Hello, this is a reminder about the MTS AI meeting on June 13. Please call me back. Best, [Your Name]'
)"
Вызываем функцию на основе вывода модели:
def call_function(res):
responce = tokenizer.decode(res[0], skip_special_tokens=True)
func_call = responce.split("<<function>>")[-1]
func_name = func_call.split("(")[0]
str_args = "{" + func_call.split("(")[1].replace("=", ":").replace(")", "}")
input_str = str_args
pattern = r"(\w+):"
output_str = re.sub(pattern, r'"\1":', input_str)
args = ast.literal_eval(output_str)
locals()[func_name](**args)
Результат вызова функции:
Скрытый текст
Модель справляется с вызовом функции, но при этом никак не поясняет результат своей работы
Glaive function calling
Импорты и определение функции для отправки сообщений ничем не отличаются, поэтому сразу перейдем к инициализации модели и токенайзера
tokenizer = AutoTokenizer.from_pretrained("glaiveai/glaive-function-calling-v1", trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained("glaiveai/glaive-function-calling-v1", trust_remote_code=True).half().cuda()
Определим промпт, в котором мы также опишем функцию отправки сообщений:
prompt = """
SYSTEM: You are an helpful assistant who has access to the following functions to help the user, you can use the functions if needed-
{
"name": "send_email",
"description": "Plan a holiday based on user's interests",
"parameters": {
"type": "object",
"properties": {
"destination_email": {
"type": "string",
"description": "Destination email",
},
"message_text": {
"type": "string",
"description": "Text of message",
},
},
"required": ["destination_email", "message_text"],
},
}
USER: Send a message to alanrbtx@gmail.com about the MTS AI meeting on June 13. And tell him to call my number back. Be kind
"""
Произведем вызов функции:
def function_calling(prompt):
inputs = tokenizer(prompt,return_tensors="pt").to(model.device)
outputs = model.generate(**inputs,do_sample=True,temperature=0.1,top_p=0.95,max_new_tokens=200)
res = tokenizer.decode(outputs[0],skip_special_tokens=True)
res = res.split("<functioncall>")[-1]
res = json.loads(res)
locals()[res["name"]](**res["arguments"])
Результат работы function calling:
Скрытый текст
Помимо того, что glaive справилась с задачей, она также сгенерировала более удобный JSON, который требует меньше преобразований перед вызовом функции в коде
NexusRaven function calling
Воспользуемся пайплайном из библиотеки transformers
from transformers import pipeline
pipeline = pipeline(
"text-generation",
model="Nexusflow/NexusRaven-V2-13B",
torch_dtype="auto",
device_map="auto",
)
Определим функции следующим образом:
prompt_template = \
'''
Function:
def get_weather_data(coordinates):
"""
Fetches weather data from the Open-Meteo API for the given latitude and longitude.
Args:
coordinates (tuple): The latitude of the location.
Returns:
float: The current temperature in the coordinates you've asked for
"""
Function:
def get_coordinates_from_city(city_name):
"""
Fetches the latitude and longitude of a given city name using the Maps.co Geocoding API.
Args:
city_name (str): The name of the city.
Returns:
tuple: The latitude and longitude of the city.
"""
Function:
def send_email(destination_email, message_text):
"""
Send message to destination_email with message_text
Args:
destination_email (str): Destination email
message_text (str): Text of message
"""
User Query: {query}<human_end>
'''
Запускаем генерацию:
prompt = prompt_template.format(query="Send a message to alanrbtx@gmail.com about the MTS AI meeting on June 13. And tell him to call my number back. Be kind")
result = pipeline(prompt, max_new_tokens=2048, return_full_text=False, do_sample=False, temperature=0.001)[0]["generated_text"]
print (result)
В результате работы модель выдает много лишнего текста:
Скрытый текст
В результате работы модель выдает много лишнего текста
Заключение
В статье мы рассмотрели, как с помощью function calling LLM-агенты получают удобный способ отправлять письма, получать доступ к API, управлять роботами и т.д. Использование LLM, которые могут вызывать функции, дает большие возможности для создания различных ИИ-приложений
Ноутбуки с кодом отправки электронной почты с помощью LLM доступны по ссылке:
https://github.com/mts-ai/function-calling
Спасибо за внимание