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

LaGir

Client
Регистрация
01.10.2015
Сообщения
246
Благодарностей
1 003
Баллы
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 вас со временем начнёт в чём-то ограничивать.

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

Вложения

Последнее редактирование модератором:

bashka

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

AGAT

Активный пользователь
Регистрация
17.11.2018
Сообщения
169
Благодарностей
37
Баллы
28
а когда появится ассистент, который по текстовому промту собирает логику, переменные, кубики, вопросы уточняющие задает?:ah:
 

LaGir

Client
Регистрация
01.10.2015
Сообщения
246
Благодарностей
1 003
Баллы
93
а когда появится ассистент, который по текстовому промту собирает логику, переменные, кубики, вопросы уточняющие задает?:ah:
Возможно, условный Claude Desktop (или его аналоги) уже что-то смогут сделать в зеннке на базовом уровне. В теории, следующие поколения таких агентов смогут делать что-то путное в зеннке. Другое дело, много ли в этом смысла, пускать агента в визуальное программирование, если у Zenno есть дублирующий API в коде (с которым агенты справятся куда лучше априори)...
Так же высока вероятность, что внутренний агент появится внутри зеннки. Касательно сроков так же абсолютно непонятно, особенно смотря на прогресс ZP8.
 

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