Введение
Можете скипнуть введение, если знаете:
Почему использовать Open Source лучше Open AI API
А для тех, кто остался: Давайте рассмотрим плюсы и минусы!
Плюсы:
Бесплатные модели.
Управление данными и приватность: локальное размещение моделей гарантирует, что ваши данные не покинут вашу систему.
Доступность исходного кода позволяет лучше понимать и исправлять ошибки.
Возможность fine-tuning для своих нужд.
Поддержка сообщества с постоянными обновлениями.
Отсутствие зависимости от вендоров и их политики.
Кроме того, можно использовать локально развернутую LLM вместе с OpenAI API.
Если, например, у вас заканчиваются средства на аккаунте или по какой-либо другой причине работа кода с OpenAI API приостанавливается, локальная модель может прийти на помощь. Это позволит вашему рабочему процессу продолжаться без перебоев.
Минусы:
Необходимость самостоятельно контролировать поддержку работоспособности модели.
Ответственность за развертку и настройку модели.
Необходимость арендовать или приобретать вычислительные ресурсы.
Параметры proprietary моделей обычно гораздо больше, и они могут быть более "умными".
Необходимость иметь квалифицированных специалистов для управления и обслуживания модели.
Теоретические основы
Что это такое Function Calling?
В контексте больших языковых моделей Function Calling подразумевает способность LLM определять из запроса пользователя подходящую функцию для выполнения из доступного набора функций и правильные параметры для передачи этой функции. Вместо генерации обычных текстовых ответов, LLM для вызова функций обычно настраивается так, чтобы возвращать ответы со структурированными данными, чаще всего в формате JSON. Эти структурированные данные можно использовать для выполнения предопределенных функций, таких как извлечение данных из хранилищ данных или функциональных возможностей, получение данных в реальном времени или вызов сторонних API.
Отличия Function Calling от Tools
Данный вопрос неоднократно встречался на просторах интернета. Думаем, многие понимают различия, однако стоит прояснить.
Tools — это более широкое понятие, которое включает в себя Function Calling и многие другие инструменты. Например, Assistants
, созданные с помощью Assistants API
в OpenAI API, поддерживают не только Function Calling, но и File Search (встроенный инструмент RAG для обработки и поиска в файлах), Code Interpreter (позволяет писать и запускать код на Python, обрабатывать файлы и различные данные), а также создавать собственные тулы.
От теории к практике
Да, как уже стало понятно, тулы — это действительно очень крутой и мощный инструмент. Однако необходимо, чтобы кто-то ими воспользовался. В OpenAI API для этого существуют Assistants
, но как это реализовать с локальными LLM-ками? Именно для этого мы здесь и собрались!
Модель, которая смогла: Mistral
Представляем вам лучшего работника месяца для нас: Mistral-7B-Instruct-v0.3:
Данная модель была выпущена 22 мая 2024 года, так что она может уже и не быть работником месяца, но мы начали с ней работать только сейчас, и для pet-проектов она довольно отлична.
Улучшения в Mistral-7B-v0.3
В Mistral-7B-v0.3
внесены следующие изменения по сравнению с Mistral-7B-v0.2
:
Расширен словарный запас до 32 768 токенов
Ранее размер словаря в v0.2 составлял 32 000 токенов. Может показаться, что увеличение на 768 токенов незначительно, однако эти дополнительные токены теперь могут охватывать редкие слова, новый жаргон или специфические термины.
Для справки:Llama 3
имеет словарь в 128 256 токенов. В то время какGPT-4o
превосходит их обоих, расширив словарь до 200 019 токенов по сравнению с 100 277 вGPT-4
.Поддерживается Tokenizer v3
Поддерживается Function Calling
Самое интересное и то, зачем мы сюда пришли, — это способность вызывать функции. Теперь модель можно использовать на полную мощность и в различных приложениях, позволяя ей выполнять задачи, выходящие за рамки простого создания текста.
Сравнение с GPT-4o
Если вы готовы платить денюжку, то можно использовать Mistral AI API
. Тут можно продешевить и получить почти такое же качество по генерации текста как у Open AI GPT-4o
.
Вот вам pricing GPT-4o
и моделей от Mistral
:
Ну, а качество у Mistral Large 2
сходно с GPT-4o
. Вот вам таблица с характеристиками моделей с этого сайта:
И еще вот эту:
Ну, собственно, Mistral Large 2
неплохо так догнала GPT-4o
, а еще использовать её дешевле.
Подумайте, чей код приведен ниже: описание тулов с использованием Mistral AI API или же OpenAI API?
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use. Infer this from the users location.",
},
},
"required": ["location", "format"],
},
}
}
]
Правильный ответ: Это сработает и там, и там. Ребята из Mistral AI сделали почти точно такой же API, как у OpenAI.
Они же одинаковые, Наташ!
Прекрасно, то есть, если вы захотите перейти от использования Open AI API к Mistral AI API, часть кода может остаться неизменной.
Но мы сюда за бесплатным Open Source пришли, о каком API идет речь? В этом и суть, что их API работает и бесплатно, с моделькой развернутой на LM Studio или других аналогах.
Вот пример кода:
from mistralai.client import MistralClient
from mistralai.models.chat_completion import ChatMessage
from mistralai.models.chat_completion import ChatMessage
from mistralai.client import MistralClient
from mistralai.models.chat_completion import ChatMessage
def get_current_weather(location, format):
# Тело функции опускается
return {
"location": location,
"temperature": 23,
"format": format
}
model="MaziyarPanahi/Mistral-7B-Instruct-v0.3-GGUF",
api_key = "lm-studio"
endpoint = "http://localhost:1234/"
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use. Infer this from the users location.",
},
},
"required": ["location", "format"],
},
}
},
]
messages = [
ChatMessage(role="user", content="What's is the current weather in Minsk Belarus?")
]
client = MistralClient(api_key=api_key, endpoint=endpoint)
response = client.chat(model=model, messages=messages, tools=tools, tool_choice="any")
print(response)
И на этом месте возникает вопрос: то есть, ребята из Mistral AI предоставили и модель, и API — а кто тогда будет покупать их ключи? Да, не все модели в открытом доступе, плюс они разворачивают их на своих серверах, но нам-то и не ракета в космосе нужна. И всё кажется таким заманчивым, но то ли бесплатный сыр только в мышеловке, то ли руки кривоваты, но добиться вызова функций через их API так и не получилось.
Что ж, таким образом мы плавно и перешли к использованию великого для нас crewAI.
Еще больше нерабочих решений!
На текущий момент возникают следующие сложности:
Количество моделей, нативно поддерживающих функции, достаточно невелико (среди них выбранный ранее
Mistral-7B-v0.3
).Фреймворки и платформы для локальной развёрки LLM зачастую не способствуют тому, чтобы данная фича даже в своём присутствии хотя бы изредка работала корректно.
В частности, некоторые модели из LocalAI имели в своём арсенале интересующую нас функциональность, однако на практике они показали удручающие результаты. Некоторые модели были не в состоянии вычленить нужные параметры или же сообщали о необходимости вызова функции в ситуациях, где контекст явно никак не связан с предлагаемой функцией.
Использование фреймворка AutoGen в совокупности с развернутой при помощи LM Studio модели с поддержкой Function Calling не дало никаких результатов. Агенты не использовали заявленные функции, а заставляли модель писать её самостоятельно. Естественно, подобное никуда не годится.
Вот репозиторий, на основе которого мы пытались соединить AutoGen и локальные LLM для реализации вызовов функций.
Величие сrewAI
В конце концов, замена фреймворка с AutoGen на аналогичный ему crewAI при всё той же развертке на LM Studio уже описанной модели Mistral-7B-Instruct-v0.3
принесла свои плоды.
Агент на основе данной LLM в действительности понял, какие у него есть функции и в каких ситуациях их необходимо применять. На этом месте мы и остановимся подробнее. Сейчас мы покажем, как повторить наши результаты шаг за шагом.
Вся система работала на обычном офисном ноутбуке с 16GB RAM и процессором AMD Ryzen 7 4700U без единого намёка на такую роскошь как GPU. О скорости работы в данной статье речи не идёт, но это это определённый плюс, ведь запустить свою небольшую LLM с крутыми фишками для личных целей сможет практически каждый желающий.
Теперь же к реализации:
Мы использовали Anaconda для создания виртуального окружения Python, LM Studio, установленный на ОС Windows. Для начала работы с CrewAI достаточно было установить всего две зависимости:
pip install crewai crewai_tools
.Переходим к коду. Задача перед нами стоит следующая:
1. Загрузить модель.
2. Подвязать к ней агента.
3. Описать функции-тулы и сообщить агенту о их существовании.
4. Создать задачу.
5. Запустить всю систему.
Создание объекта модели:
from langchain_community.chat_models.openai import ChatOpenAI
OPENAI_API_BASE_URL = "http://localhost:1234/v1" # Адрес, по которому мы будем обращаться к модели в LM Studio
OPENAI_API_KEY = "NA" # В базовой конфигурации LM Studio не важен
MODEL_NAME = "MaziyarPanahi/Mistral-7B-Instruct-v0.3-GGUF" # Необязательно, важна лишь фактическая модель, которую развернули в LM Studio
default_llm = ChatOpenAI(
openai_api_base=OPENAI_API_BASE_URL,
openai_api_key=OPENAI_API_KEY,
model_name=MODEL_NAME,
)
Создание функции-тулы для агента:
from crewai_tools import tool
# Аннотирование типов и docstring строго обязательны согласно документации CrewAI
# Формат docstring формально не важен, но тем подробнее объяснишь суть функции агенту, тем лучше
@tool("Summator") # Имя, под которым агент будет упоминать данную тулу
def find_sum(a: int, b: int) -> int:
"""
Function for finding the sum of two `int` numbers.
Args:
a (int): The first number.
b (int): The second number.
Returns:
int: The sum of `a` and `b`.
"""
return a + b
Создание агента:
from crewai import Agent
general_agent = Agent(
role="Assistant",
goal="Answer Questions",
backstory="",
allow_delegation=False,
verbose=True,
llm=default_llm,
tools=[find_sum], # Именно здесь мы отдаём агенту возможные функции, которые он может применить
)
Осталось создать задачу агенту и запустить Crew:
from crewai import Crew, Task
task = Task(
description="12345 + 54321",
expected_output='Int number',
agent=general_agent,
)
crew = Crew(agents=[general_agent], tasks=[task], verbose=2)
result = crew.kickoff()
print(result)
Предоставляем результаты запуска агента, использующего описанную функцию подсчёта суммы чисел:
То, что мы изначально хотели получить и даже лучше! (ну почти...читайте далее).
Теперь предоставим чуть более интересный пример, переписав функцию так, чтобы она принимала в качестве своего аргумента список целых чисел для суммирования. У моделей часто возникают проблемы с математическими расчётами, поэтому попробуем частично исправить этот недочёт.
Переписанная тула:
@tool("Summator")
def find_sum(nums: List[int]) -> int:
"""
Function for finding the sum of array of numbers.
Args:
nums (List[int]): Array of numbers to find their sum.
Returns:
int: The sum of all numbers in array.
"""
return sum(nums)
Ниже представлены результаты запроса без использования каких-либо функций и тулов и с применением тулы:
Хорошо, сложение чисел - это всё круто и прикольно, однако давайте предоставим более интересный пример. Реализуем аналог демонстрационной функции из документации OpenAI по запросу погоды на местноти.
В третий раз, код самой функции с использованием Open-Meteo API:
@tool("Weather API")
def get_current_weather(location: str) -> str:
"""
Function to get current weather in provided city
Args:
location (str): The target city.
Returns:
str: Weather info as JSON string if answer is valid or error message
"""
CITY_LATLONG_URL = "https://geocoding-api.open-meteo.com/v1/search?name={}&count=1&language=en&format=json"
url = CITY_LATLONG_URL.format(location)
response = requests.get(url)
if response.status_code == 200:
try:
data = response.json()["results"][0]
except KeyError:
return f"ERROR: Nonexisting city {location}"
latitude = data["latitude"]
longitude = data["longitude"]
else:
return f"ERROR: Failed to get city location {location}"
WEATHER_CURRENT_URL = "https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,rain,showers,snowfall,wind_gusts_10m,wind_direction_10m&wind_speed_unit=ms&timezone=auto"
url = WEATHER_CURRENT_URL.format(latitude, longitude)
response = requests.get(url)
if response.status_code == 200:
return str(response.json())
else:
return f"ERROR: Failed to get current weather in {location}"
Пришлось также немного видоизменить описание агента и его задачи:
general_agent = Agent(
role="Wheather Assistant",
goal="Answer Questions and reformat data as human-readable form. Only use tools to get weather info, formatting is your own task",
backstory="",
allow_delegation=False,
verbose=True,
llm=default_llm,
tools=[get_current_weather],
)
task = Task(
description="What is the weather in Minsk now?",
expected_output='Weather description',
agent=general_agent,
)
Спустя некоторое время ожидания, окончательный результат перед нами:
Агент сработал корректно, результат получен, все довольны — мир, дружба, жвачка! Однако... есть и небольшая ложка дёгтя:
При трёх одновременно загруженных в агента тулах модель стала путаться и запускать функции, совершенно не касающиеся текущего запроса, с выдуманными параметрами. Вполне вероятно, что это можно нивелировать гораздо более детальным пояснением сути тулов и в каких ситуациях их нужно вызывать.
Наличие тулов склоняет модель к лени. В первый раз после получении данных о погоде LLM решила вызвать функцию для форматирования JSON в читабельный текст, которой даже не существовало, вместо того, чтобы выполнить свою основную задачу самостоятельно. Исправить это удалось прописыванием более чёткого ТЗ для модели.
В отличие от OpenAI API и в целом от классического Function Calling, агенты crewAI также берут на себя обработку ответа модели. И пока в других случаях нам возвращается JSON с параметрами функции, которые необходимо так или иначе обрабатывать вручную, CrewAI задвигает это всё под капот, вызывая функцию самостоятельно.
Заключение и подводные камешки
У этого подхода есть как плюсы, так и минусы.
Плюсы: даже с RAM в 8 ГБ можно запустить квантованную Mistral размером 3-5 ГБ и радоваться вызовам функций.
Минусы: Что ж...заглянем под капот. Вот как выглядел POST запрос к модели на LLM:
{
"messages": [
{
"content": "You are Wheather Assistant. \nYour personal goal is: Answer Questions and reformat data as human-readable form. Only use tools to get weather info, formatting is your own task\nYou ONLY have access to the following tools, and should NEVER make up tools that are not listed here:\n\nTool Name: Weather API(*args: Any, **kwargs: Any) -> Any\nTool Description: Weather API(location: 'string') - Function to get current weather in provided city Args: location (str): The target city. Returns: str: Weather info as JSON string if answer is valid or error message \nTool Arguments: {'location': {'title': 'Location', 'type': 'string'}}\n\nUse the following format:\n\nThought: you should always think about what to do\nAction: the action to take, only one name of [Weather API], just the name, exactly as it's written.\nAction Input: the input to the action, just a simple python dictionary, enclosed in curly braces, using \" to wrap keys and values.\nObservation: the result of the action\n\nOnce all necessary information is gathered:\n\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n\nCurrent Task: What is the weather in Minsk now?\n\nThis is the expect criteria for your final answer: Weather description \n you MUST return the actual complete content as the final answer, not a summary.\n\nBegin! This is VERY important to you, use the tools available and give your best Final Answer, your job depends on it!\n\nThought:\n",
"role": "user"
}
],
"model": "MaziyarPanahi/Mistral-7B-Instruct-v0.3-GGUF",
"logprobs": false,
"n": 1,
"stop": [
"\nObservation"
],
"stream": true,
"temperature": 0.7
}
А вот так выглядел запрос к модели для нерабочего варианта вызова функции через Mistral AI API:
{
"messages": [
{
"role": "user",
"content": "What's is the current weather in Minsk Belarus?"
}
],
"model": [
"MaziyarPanahi/Mistral-7B-Instruct-v0.3-GGUF"
],
"tools": [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
},
"format": {
"type": "string",
"enum": [
"celsius",
"fahrenheit"
],
"description": "The temperature unit to use. Infer this from the users location."
}
},
"required": [
"location",
"format"
]
}
}
}
],
"stream": false,
"tool_choice": "any"
}
В случае сrewAI инструменты добавляются в основной промпт, а в случае Mistral AI API они передаются отдельным параметром в запросе. Поэтому мы предполагаем (но не утверждаем), что модель могла плохо справляться в случае, когда мы ей давали несколько функций, из-за перегруженности контента промпта. В целом, передача всей информации о каждой функции просто в промпт выглядит слегка как костыль и даже не совсем как Function Call, но несмотря на такие подводные камни, всё выглядит довольно хорошо: параметры функций вычленяются правильно, функции вызываются сами — а что ещё нужно?
Мы продолжаем искать различные более лучшие способы реализовывать вызов функций, создание агентов и прочие крутые фишки на Open Source решениях. Поэтому мы будем рады любому комментарию или предложению. А пока мы понимаем, что на поле между Open Source и OpenAI одерживает победу второй игрок, но первый уже совсем не аутсайдер.
Всем спасибо за прочтение данной статьи!
Авторы статьи
Янкова Анастасия (@dumonten)
Пастухов Кирилл (@AlabaAclydiem)