Всем привет, меня зовут Алан, я разработчик-исследователь из команды фундаментальных исследований 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')

Базовая схема вызова функций

Схема вызова функций является общей для всех языковых моделей и выглядит следующим образом:

  1. Передаем в LLM промпт пользователя и описание доступных функций/инструментов.

  2. Модель сопоставляет промпт пользователя с описанием функций. Если LLM решает, что для выполнения запроса требуется вызов одной или нескольких функций, то она возвращает JSON с именем и аргументами функции (модель может их придумать).

  3. Вызываем функцию в коде.

  4. Передаем результаты выполнения функции обратно ассистенту, затем он генерирует ответ с суммаризацией результатов (если они есть).

Общая схема работы

Схема работы

Большинство проприетарных 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:

Пример с 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 бенчмарках

API accuracy на различных задачах

Functionary ?

Functionary — это языковая модель, которая может интерпретировать и выполнять функции/плагины. Модель определяет, когда выполнять функции, как именно — параллельно или последовательно —  и может понимать результат их выполнения. Functionary запускает функции только по мере необходимости. Определения функций предоставляются в виде JSON, аналогично вызовам функций с помощью OpenAI. 

Помимо этого functionary отлично показывает себя относительно других моделей в задачах поддержки диалога и генерации ответа на основе результатов выполнения функций

Сравнение моделей на задачах function calling

Связанные исследования

Далее мы коротко рассмотрим различные подходы для вызова функций из нескольких статей, которые касаются темы function calling

Toolformer

Toolformer — это модель, дообученная решать, какие API вызывать, когда их вызывать, какие аргументы передавать и как лучше всего включать результаты в генерируемую последовательность. Это производится с помощью метода self-supervised, который требует несколько демонстраций для каждого API. Toolformer обеспечивает существенное улучшение производительности при выполнении множества задач, конкурируя с более крупными моделями, при этом не жертвуя языковым моделированием

Ссылка на статью

Chain of Tools

Самая крутая работа (на мой взгляд), которая фокусируется на вызове цепочки функций, где вызов каждой последующей функция зависит от результатов работы предыдущей. Задача планирования решается с помощью генерации специальной программы, которая будет последовательно запускать необходимые функции, как это показано на рисунке (справа). Для имплементации подойдут LLM, которые умеют хорошо генерировать код.

Ссылка на статью

TPTU-v2

TPTU-v2 это фреймворк, состоящий из трех основных компонентов

  1. API Retriever. Как понятно из названия, этот компонент находит наиболее релевантные пользовательскому запроса API

  2. LLMFinetuner. Этот компонент файнтюнит LLM с помощью тщательно подобранного датасета, расширяя возможности модели по планированию и эффективному выполнению вызовов API

  3. 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

В результате мне на почту прилетело следующее письмо(в спойлере)

Пример работы Gemini function calling

Давайте посмотрим что происходит по капотом:

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)

Результат вызова функции:

Пример работы gorilla function calling

Модель справляется с вызовом функции, но при этом никак не поясняет результат своей работы

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 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)

В результате работы модель выдает много лишнего текста:

Пример работы NexusRaven function calling

В результате работы модель выдает много лишнего текста

Заключение

В статье мы рассмотрели, как с помощью function calling LLM-агенты получают удобный способ отправлять письма, получать доступ к API, управлять роботами и т.д. Использование LLM, которые могут вызывать функции, дает большие возможности для создания различных ИИ-приложений

Ноутбуки с кодом отправки электронной почты с помощью LLM доступны по ссылке:

https://github.com/mts-ai/function-calling

Спасибо за внимание

Комментарии (3)


  1. Pol1mus
    31.07.2024 14:31

    С библиотекой для джемини мы просто передаем список функций и оно само работает, может даже несколько раз запустить функцию, посмотреть результаты, поменять что то запустить снова, и так пока не получит желаемый результат.

    Со всеми остальными даже просто запустить одну простую функцию - это такой гемор что даже смотреть в эту сторону не хочется. Нужна какая то упрощающая библиотека обертка.


  1. Dertefter
    31.07.2024 14:31

    Когда-то я тоже решал подобную задачу. Если вам интересно: https://github.com/dertefter/WinAutopilot


  1. WhoIsJohnGolt
    31.07.2024 14:31

    Была забыта ссылка на открытый курс deeplearning.ai, посвящённый как раз вызову функций и парсингу данных: https://www.deeplearning.ai/short-courses/function-calling-and-data-extraction-with-llms/