Учим AI-ассистента вызывать функции внутри Zenno

LaGir

Client
Регистрация
01.10.2015
Сообщения
245
Благодарностей
989
Баллы
93
Приветствую всех!

В одной из предыдущих статей по LLM я упоминал о возможности пробрасывать им вызов внешних функций:
Способность запускать шаблон агенту-тестеру можно дать с помощью function calling (т.е. написать код нужных функций и прописать возможность нейронке вызывать их при необходимости). Для простого понимания – например, способность ChatGPT ходить гуглить что-то, обращаться к DALL-E за генерацией картинок – как раз реализована с помощью function calling.
Однако, как что-то подобное сделать в рамках Zenno, тогда не затрагивалось – и похоже, сейчас пришло время.
Помимо function calling в интернете можно встретить названия tool use, function call. Всё это плюс-минус значит одно и то же – (дать) возможность модели выполнять функции (практически любые) или целые инструменты. А сейчас ещё и целые API и интеграции (с помощью MCP, Model Context Protocol)

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

Понятно, что для python уже миллион готовых решений как это делать и использовать.
Но если мы обычные пользователи ZennoPoster или ZennoDroid (относительно обычные, чутка понимать C#-сниппеты важно), и хотим нативно использовать function call внутри Zenno, без необходимости присобачивать скрипты на питоне к шаблону. Что нам в таком случае делать?
Об этом и поговорим в данной статье, и рассмотрим как заставить LLM вызывать наши же функции, написанные внутри Zenno (спойлер – внутри блока "Общий код").

Для вызова функций моделью нам понадобится в первую очередь сама модель.
Для этого будем использовать бюджетный и безопасный вариант – открытые локальные LLM-ки, и соответствующий софт для их запуска. В одной из предыдущих статей я подробно рассматривал LM Studio – её предлагаю использовать и в этот раз. Так как если что-то непонятно по софту для запуска модели, можно посмотреть в той предыдущей статье.

Кейсы использования

Возможностей для использования масса (так как пробрасываемые в модель функции могут быть абсолютно любыми), детали скорее зависят от вашей фантазии и ваших шаблонов, схем работы.
В качестве понятного начального примера предлагаю рассмотреть в некотором смысле "полувнешний" кейс – случай, когда мы решили сделать себе ИИ-помощника/ассистента по управлению шаблонами в ZennoPoster.
Иными словами, мы хотим для удобства использовать базовое предназначение LLM-ком обработку естественного языка. То есть, говорим ей своими словами что нужно сделать, она понимает что нужно, и запускает соответствующую функцию с нужными параметрами.
Например, мы хотим написать сообщение нейронке "Запусти шаблон Parser123 на 100 выполнений", и чтобы оно сразу так и заработало. А не заходить в зеннку и тыкать какие-то кнопки, какие-то настройки.

В предыдущем конкурсе Zennolab Master были 2 хорошие работу на тему управления шаблонами:
Автоматизация управления проектами
EasyControl 2.0 : Управляйте ZennoPoster с помощью Telegram
Что я хочу сказать – можно скомбинировать материалы этих работ и LLM c поддержкой function call – и получить возможность управлять своими шаблонами просто путём писания своими словами команд/задач ИИ-ассистенту через чатик в telegram.

На этот простой кейс и буду ориентироваться, демонстрируя далее код и пример шаблона, как использовать модель с tool use внутри Zenno.

Выбор моделей в LM Studio

Прежде чем переходить к тестовому шаблону и коду, надо сказать несколько слов о самих моделях.
Есть модели, которые нативно поддерживают tool use (он же function calling, в разных местах названия разные), т.е. они изначально обучались под использование в таком качестве.
Именно их лучше всего выбирать для наших задач.
Благо, сейчас в LM Studio есть прям такая опция и пиктограммка при поиске моделей.

SearchModel.jpg


Заодно сразу скажу, что для наших целей не нужны большие модели – подойдут в том числе такие крохи, как этот младший Qwen 3 со скриншота.
Для запуска такой модели и видеокарта не сказать что сильно нужна – будет вполне неплохо работать и на процессоре с оперативкой, пусть и помедленнее.


Запуск сервера в LM Studio

После того, как скачали подходящую модель, нам нужно будет запустить локальный сервер в LM Studio, чтобы обращаться к LLM-ке из ZennoPoster.
Для этого просто делаем действия как показано на скриншоте.

RunServer.jpg



Шаблон с демонстрацией

Рассмотрим тестовый проект, в котором используется обращение к модели из LM Studio с возможностью вызывать ею наши функции. Он прикреплён к статье внизу в виде файла.

Сразу скажу, что код всего проекта целиком в блоке "Общий код". Когда открываем прикреплённый шаблон, по этой причине сразу стоит идти туда, так как в сниппете просто дёргается оттуда код.
Код демонстрации небольшой, всего на 400 строк, но удобнее всего его писать и рассматривать (а также расширять) в стандартной объектной структуре C#. Адаптировать в свои шаблоны тоже сразу лучше в этом виде, плюс с помощью тех же нейронок (в качестве ваших ассистентов по кодингу) расширять тоже будет вам проще.

Код разбит на 3 смысловых блока.
  1. Функции (методы), которые будут доступны нашей модели для вызова.
  2. Методы обращения к модели в LM Studio.
  3. Классы для сериализации в JSON запросов к модели (чтобы было красиво и понятно).

Давайте начнём с функций, которые будем "пробрасывать" модели.
Для примера я подготовил 5 простых функций – 3 с прямым воздействием на какой-нибудь из добавленных шаблонов в интерфейс ZennoPoster, 2 просто тестовых со сложением и умножений чисел.

C#:
/// <summary>
/// Добавляет выполнения указанному шаблону по его имени
/// </summary>
public static bool AddTriesToTemplate (string templateName, int countTries)
{
    if (IsTaskExists(templateName))
    {
        ZennoPoster.AddTries(templateName, countTries);
        return true;
    }
    return false;
}

/// <summary>
/// Устанавливает максимальное количество потоков указанному шаблону по его имени
/// </summary>
public static bool SetMaxThreadsToTemplate (string templateName, int countThreads)
{
    if (IsTaskExists(templateName))
    {
        ZennoPoster.SetMaxThreads(templateName, countThreads);
        return true;
    }
    return false;
}

/// <summary>
/// Останавливает в ZennoPoster указанный шаблон по его имени
/// </summary>
public static bool StopTemplate (string templateName)
{
    if (IsTaskExists(templateName))
    {
        ZennoPoster.StopTask(templateName);
        return true;
    }
    return false;
}

/// <summary>
/// Складывает два числа
/// </summary>
public static int Sum (int x, int y)
{
    return x + y;
}

/// <summary>
/// Умножает два числа
/// </summary>
public static int Multiply (int x, int y)
{
    return x * y;
}
Перейдём к коду, который будет обращаться к модели в LM Studio.
Метод RunExample у нас по сути является диспетчером, в котором последовательно выполняются нужные шаги.

C#:
public static void RunExample (IZennoPosterProjectModel project)
{
    string url = project.Variables["ApiUrl"].Value;
    if (string.IsNullOrWhiteSpace(url)) throw new Exception("Не указан API URL во входных настройках!");

    // Формирование запроса к модели
    var initialRequest = BuildInitialRequest(project);
    // Отправка запроса к модели
    var response = SendChatRequest(url, initialRequest);

    // Парсинг функции, есло модель решила вызвать какую-либо
    var toolCall = ParseToolCall(response);
    if (toolCall != null)
    {
        // Выполнение функции, которую решила вызвать модель
        string result = InvokeFunction(toolCall);
        project.SendInfoToLog($"Результат выполнения функции, которую вызвала модель: {result}", true);
    }
    else
    {
        project.SendInfoToLog("Модель не запросила вызов функции.", true);
    }
}
Внутри метода RunExample вызываются остальные методы, ответственные за действия на каждом шаге. В целом, в них нет чего-то специфического, чтобы рассматривать каждый из них.
Поэтому тут приведу код пары из них, наиболее интересных (остальное можете посмотреть самостоятельно, открыв шаблон в ProjectMaker).

В методе BuildInitialRequest происходит формирование запроса к модели. Тут же нам надо оформить доступные функции для вызова (а также доступные параметры для них) в понятном для модели виде.

C#:
static ChatRequest BuildInitialRequest (IZennoPosterProjectModel project)
{
    // Определяем функции, которые будут доступны LLM
    var sumFunc = new ToolDefinition
    {
        Function = new ToolFunction
        {
            Name = "Sum",
            Description = "Складывает два целых числа x и y",
            Parameters = new FunctionParametersSchema
            {
                Properties = new Dictionary<string, FunctionParameter>
                {
                    { "x", new FunctionParameter { Type = "integer", Description = "Первое число" } },
                    { "y", new FunctionParameter { Type = "integer", Description = "Второе число" } }
                },
                Required = new List<string> { "x", "y" }
            }
        }
    };
    var multiplyFunc = new ToolDefinition
    {
        Function = new ToolFunction
        {
            Name = "Multiply",
            Description = "Умножает два целых числа x и y",
            Parameters = new FunctionParametersSchema
            {
                Properties = new Dictionary<string, FunctionParameter>
                {
                    { "x", new FunctionParameter { Type = "integer", Description = "Первое число" } },
                    { "y", new FunctionParameter { Type = "integer", Description = "Второе число" } }
                },
                Required = new List<string> { "x", "y" }
            }
        }
    };
    var addTriesFunc = new ToolDefinition
    {
        Function = new ToolFunction
        {
            Name = "AddTriesToTemplate",
            Description = "Добавляет выполнения указанному шаблону по его имени",
            Parameters = new FunctionParametersSchema
            {
                Properties = new Dictionary<string, FunctionParameter>
                {
                    { "templateName", new FunctionParameter { Type = "string", Description = "Точное название шаблона" } },
                    { "countTries", new FunctionParameter { Type = "integer", Description = "Количество повторов, которое надо добавить" } }
                },
                Required = new List<string> { "templateName", "countTries" }
            }
        }
    };
    var setMaxThreadsFunc = new ToolDefinition
    {
        Function = new ToolFunction
        {
            Name = "SetMaxThreadsToTemplate",
            Description = "Устанавливает максимальное количество потоков указанному шаблону по его имени",
            Parameters = new FunctionParametersSchema
            {
                Properties = new Dictionary<string, FunctionParameter>
                {
                    { "templateName", new FunctionParameter { Type = "string", Description = "Точное название шаблона" } },
                    { "countThreads", new FunctionParameter { Type = "integer", Description = "Количество повторов, которое надо добавить" } }
                },
                Required = new List<string> { "templateName", "countThreads" }
            }
        }
    };
    var stopTaskFunc = new ToolDefinition
    {
        Function = new ToolFunction
        {
            Name = "StopTemplate",
            Description = "Останавливает в ZennoPoster указанный шаблон по его имени",
            Parameters = new FunctionParametersSchema
            {
                Properties = new Dictionary<string, FunctionParameter>
                {
                    { "templateName", new FunctionParameter { Type = "string", Description = "Точное название шаблона" } },
                },
                Required = new List<string> { "templateName" }
            }
        }
    };

    // Получаем промпт из файла
    string prompt = GetPromptFromFile(project);
    // Формируем сообщения к модели
    var messages = new List<Message>
    {
        new Message { Role = "system", Content = "Ты ассистент-помощник по управлению шаблонами в ZennoPoster, который может при необходимости вызывать функции." },
        new Message { Role = "user", Content = prompt }
    };

    // Получаем название модели из входных настроек
    string model = project.Variables["Model"].Value;
    if (string.IsNullOrWhiteSpace(model)) throw new Exception("Во входных настройках не указана модель!");

    return new ChatRequest
    {
        Model = model,
        Tools = new List<ToolDefinition> { sumFunc, multiplyFunc, addTriesFunc, setMaxThreadsFunc, stopTaskFunc },
        Messages = messages
    };
}
Тут кстати сразу замечу, что в данном демонстрационном шаблоне я сделал подхват промпта из файла "prompt.txt" из папки с шаблоном. Разумеется, так делать необязательно, можно вынести промпт в любое место, хоть во входные настройки, хоть посылать его через бота в telegram, как упоминалось выше.

Если модель ответила нам, решив вызвать какую-либо функцию – в методе InvokeFunction мы перехватываем это "намерение" модели и, собственно, производим вызов функции по её выбору, сразу с нужными параметрами.
Параметры, как можно понять, модель выбирает так же, как и саму функцию.

C#:
static string InvokeFunction (FunctionCall call)
{
    int x, y, countTries, countThreads;
    string result, templateName;

    switch (call.Name)
    {
        case "Sum":
            x = Convert.ToInt32(call.Arguments["x"]);
            y = Convert.ToInt32(call.Arguments["y"]);
            result = Sum(x, y).ToString();
            break;
        case "Multiply":
            x = Convert.ToInt32(call.Arguments["x"]);
            y = Convert.ToInt32(call.Arguments["y"]);
            result = Multiply(x, y).ToString();
            break;
        case "AddTriesToTemplate":
            templateName = call.Arguments["templateName"];
            countTries = Convert.ToInt32(call.Arguments["countTries"]);
            result = AddTriesToTemplate(templateName, countTries).ToString();
            break;
        case "SetMaxThreadsToTemplate":
            templateName = call.Arguments["templateName"];
            countThreads = Convert.ToInt32(call.Arguments["countThreads"]);
            result = SetMaxThreadsToTemplate(templateName, countThreads).ToString();
            break;
        case "StopTemplate":
            templateName = call.Arguments["templateName"];
            result = StopTemplate(templateName).ToString();
            break;
        default:
            throw new Exception($"Неизвестная функция: {call.Name}");
    }

    return result;
}
Вот в общем-то и всё, что нужно, чтобы заставить модель использовать функции внутри Zenno.
Повторюсь на всякий случай, что функции могут быть абсолютно любыми (какие сами напишите, найдёте на форуме/в интернете, какие напишет вам другая нейронка).
Также замечу, что в демонстрационном шаблоне представлена упрощённая схема – модель просто вызывает функцию при необходимости, и всё. Разумеется, ничего не мешает расширить код и сделать другую логику под ваши задачи. Например, чтобы результат выполнения функции возвращался модели, и при необходимости она с ним что-то сразу делала – к примеру, вызывала вторую функцию, используя результат от первой как параметр для второй. Логику можно сооружать какую угодно, всё зависит от конкретных задач.


Наглядную демонстрацию работы шаблона посмотрим в видео.



MCP, или куда можно двигаться дальше

Если вы плюс-минус интересуетесь новостями по LLM-кам, то наверняка были свидетелями некоторого хайпа по поводу MCP (Model Context Protocol) – открытого протокола предоставления контекста и инструментов моделям, представленого Anthropic в прошлом году.
Если говорить простыми словами – по сути это то же самое, что и обычный tool use, но более удобно для масштабных задач и при целях отвязки от своих локальных функций.
Вы можете не писать свои функции, а просто подключаться к MCP-серверам, которые предоставляют нужные возможности. MCP позволяет компаниям и сервисам самостоятельно создавать MCP-серверы, давая вашим моделям нужный функционал и контекст при подключении к ним. Как можно заметить, это что-то типа привычных нам API у различных сервисов. Только эти API не для программ, а для LLM-ок.
Ну и конечно вы можете поднимать свои MCP-сервера, отделяя функции и другие инструменты, контекст для своих моделей в полную отдельную сущность, например, и заодно на отдельную удалённую машину, как вариант.
Тут сразу пару дисклеймеров.
  1. В сети уже очень много самых различных MCP-серверов с кучей интересного и полезного функционала. Но это не значит что все из них безопасны.
  2. Изучать MCP и переходить сразу к нему не имеет смысла, если вы используете или планируете использовать только локальный свой tool use и подгрузку своего контекста моделям – так как этом случае никаким принципиальных изменений не будет, просто смена одного кода на другой.
Этот краткий абзац про MCP главным образом написан для того, чтобы у вас было примерное понимание, что есть ещё и куда можно двигаться дальше, если простой tool use вас со временем начнёт в чём-то ограничивать.

На этом всё, спасибо за внимание :-)
 

Вложения

Последнее редактирование модератором:
  • Спасибо
Реакции: BAV, XT, Ingvar и еще 9

bashka

Client
Регистрация
13.06.2017
Сообщения
195
Благодарностей
133
Баллы
43
Зачет
 
  • Спасибо
Реакции: LaGir

Кто просматривает тему: (Всего: 0, Пользователи: 0, Гости: 0)