- Регистрация
- 10.10.2019
- Сообщения
- 2
- Благодарностей
- 3
- Баллы
- 3
Идея создания этого проекта родилась из-за периодически встречающихся на форуме просьб добавить инструменты ИИ в ZennoPoster «из коробки» и периодических споров о том, как должна такая интеграция выглядеть. Пока официальная интеграция находится в разработке, архитектура программы и поддержка C# дают нам некоторую свободу действий — это позволяет реализовать собственную вариацию ИИ-агента непосредственно внутри ZennoPoster самостоятельно.
В рамках предыдущих конкурсов темы ИИ-агентов уже поднимались, однако в большинстве случаев решения были либо заточены под слишком узкие задачи, либо неудобны для внедрения в реальные проекты из-за избыточного количества «кубиков» и сложности логики.
Я поставил перед собой цель создать решение, которое было бы:
- Простым в освоении — работа строится на базе современных агентных режимов ИИ
- Гибким — архитектура позволяет адаптировать агента под сложные, многоуровневые задачи
- Удобным для интеграции — проект не перегружен визуальными элементами и легко встраивается в существующие шаблоны
Если не интересны технические детали реализации, более гибкие настройки, а только шаблон и примеры использования — листайте к разделу 7.
Содержание
- Как ИИ-агент видит браузер и взаимодействует с ним — архитектура цикла
- Структура и описание функций (Functions): что агент умеет делать
- Провайдеры: OpenAI, DeepSeek, локальные модели
- Контроль расходов
- Создание повторяемых сценариев
- Устойчивость: autoRecovery и flex-режим
- Код для работы, плагин, примеры использования
1. Как ИИ-агент видит браузер и взаимодействует с ним
Первое, с чем необходимо разобраться — как организовать эффективный диалог между браузером и искусственным интеллектом.
Сейчас много говорят об ИИ-агентах, способных выполнять «всю работу» за пользователя. Зачастую под капотом таких систем лежит MCP (Model Context Protocol) — протокол, где отдельный сервер хранит описание доступных функций программы. Это удобный стандарт для масштабируемых систем, но для нашей задачи создание полноценного MCP-сервера было бы избыточным.
Вместо этого мы пойдём по пути Function Calling: вместе с основным промптом модели передаётся массив
tools — описание функций, которые ИИ может вызвать для выполнения действий или получения данных.Схема итерационного цикла агента
В такой архитектуре процесс превращается в итерационный цикл:
- Отправка промпта (либо результата выполнения предыдущего действия). При желании первое же сообщение можно сопроводить контекстом, например скриншотом или же кодом HTML.
- Анализ и выбор действия: модель сама выбирает, что запросить — исходный код страницы, текстовое содержимое или скриншот — в зависимости от того, какой способ лучше подходит для текущего состояния страницы, анализирует ситуацию и выбирает следующую функцию (например, клик или ввод текста)
- Выполнение действия через C# внутри ZennoPoster
- Получение результата и отправка его обратно модели
Цикл повторяется шаг за шагом до тех пор, пока задача не будет выполнена; не возникнет критическая ошибка или не сработают ограничения из настроек.
2. Структура и описание функций (Functions)
Ключевым этапом реализации является составление описания доступных инструментов. Поскольку ZennoPoster обладает большим количеством методов, описания функций хранятся в формате JSON и разделены по логическим группам. Такой модульный подход даёт несколько преимуществ:
- Гибкость: можно отключать группы инструментов для моделей без поддержки Vision или в сценариях с ограниченным контекстом
- Экономия токенов: передавая только нужные инструменты, мы уменьшаем размер контекстного окна и повышаем точность выбора функции
| Группа | Инструменты | Когда нужна |
|---|---|---|
| Browser | navigate, go_back, reload_page, get_current_url, get_page_title, clear_cookies, clear_cache | Взаимодействие с браузером |
| MouseKeyboard | click, mouse_click, keyboard, scroll_into_view, focus_element, mouse_move, mouse_drag, mouse_scroll | Клики, ввод текста |
| DomReading | get_dom_text, get_page_html, get_element, get_element_count | Парсинг данных |
| FormInput | set_value, rise_event | Заполнение форм |
| Screenshots | take_screenshot | Визуальный анализ, autoRecovery (Aгент решает сам — использовать скриншот или читать DOM, можно принуждать) |
| ZennoStorage | save_variable, get_variable, list_add, list_remove, list_get, list_clear, table_set_cell ... | Работа с данными ZennoPoster |
| AgentControl | wait, report_error | Всегда, не отключается |
report_error — способ агента сообщить, что задача невыполнима. Модель вызывает его сама, если заходит в тупик, и выполнение немедленно останавливается. Важно: autoRecovery при этом не срабатывает — это всегда финальная ошибка.Фактически эта таблица — это то, что ИИ-агент умеет делать. При желании список можно расширять: добавить описание в нужный раздел и реализовать соответствующий метод.
Два примера того, как инструменты описываются в коде:
C#:
private static readonly JArray ToolsBrowser = JArray.Parse(@"[
{
""type"": ""function"",
""name"": ""navigate"",
""description"": ""Navigate the browser to a URL. Waits for the page to finish loading."",
""parameters"": {
""type"": ""object"",
""properties"": {
""url"": { ""type"": ""string"", ""description"": ""Full URL including scheme, e.g. https://example.com"" }
},
""required"": [""url""],
""additionalProperties"": false
}
},
{
""type"": ""function"",
""name"": ""go_back"",
""description"": ""Navigate back to the previous page in browser history."",
""parameters"": { ""type"": ""object"", ""properties"": {}, ""additionalProperties"": false }
}
]");
C#:
private static readonly JArray ToolsDomReading = JArray.Parse(@"[
{
""type"": ""function"",
""name"": ""get_dom_text"",
""description"": ""Return the visible text content of the current page (innerText)."",
""parameters"": {
""type"": ""object"",
""properties"": {
""max_length"": { ""type"": ""integer"", ""description"": ""Max characters to return. Default 5000."" }
},
""additionalProperties"": false
}
},
{
""type"": ""function"",
""name"": ""get_element"",
""description"": ""Find an element and return its text, inner HTML, or an attribute value."",
""parameters"": {
""type"": ""object"",
""properties"": {
""by"": { ""type"": ""string"", ""enum"": [""xpath"", ""css""] },
""selector"": { ""type"": ""string"" },
""return_type"": { ""type"": ""string"", ""enum"": [""text"", ""html"", ""attribute""] },
""attribute"": { ""type"": ""string"" },
""index"": { ""type"": ""integer"" }
},
""required"": [""by"", ""selector"", ""return_type""],
""additionalProperties"": false
}
}
]");
После получения ответа от модели метод-маршрутизатор направляет вызов к нужному методу ZennoPoster:
C#:
public string ExecuteTool(string toolName, JObject args)
{
switch (toolName)
{
case "navigate": return ToolNavigate(args);
case "go_back": return ToolGoBack();
case "reload_page": return ToolReloadPage();
case "get_current_url": return ToolGetCurrentUrl();
case "get_page_title": return ToolGetPageTitle();
case "click": return ToolClick(args);
case "mouse_click": return ToolMouseClick(args);
case "keyboard": return ToolKeyboard(args);
case "get_dom_text": return ToolGetDomText(args);
case "get_element": return ToolGetElement(args);
case "set_value": return ToolSetValue(args);
// ... остальные инструменты
default:
return "Unknown tool: " + toolName;
}
}
C#:
private string ToolMouseClick(JObject args)
{
int modelX = args["x"]?.ToObject() ?? 0;
int modelY = args["y"]?.ToObject() ?? 0;
// если был скриншот с масштабированием
double scale = _lastScreenshot?.ScaleFactor ?? 1.0;
int realX = (int)(modelX * scale);
int realY = (int)(modelY * scale);
string button = args["button"]?.ToString() ?? "left";
_instance.ActiveTab.FullEmulationMouseMove(realX, realY);
Thread.Sleep(100);
_instance.ActiveTab.FullEmulationMouseClick(button, "click");
_instance.ActiveTab.WaitDownloading();
return $"Clicked ({realX},{realY}) {button}";
}
Размер скриншота определяется через текущий viewport (видимая область страницы в браузере) браузера (делаем таким образом, чтобы мы отправляли действительно же самое что и видит пользователь, это позволяет сократить количество токенов, а также использовать модели которые не могут обрабатывать слишком большие изображения, немного большее описание этого будет позднее. (Но при этом у нас есть в коде возможность включения опции отправки сразу полного скриншота страницы, для более быстрой ориентации ИИ агента на странице)
Размер viewport определяется через выполнение JavaScript в браузере:
Полученные значения парсятся и используются как фактический размер видимой области страницы.
Если выполнение скрипта не удалось (например, страница ещё не загружена), используется запасной вариант - получение размеров экрана из профиля браузера:
Также предусмотрена работа со сжатием изображения до нужных размеров, чтобы модель помещалась в контекст более простых моделей (но лучше всего конечно использовать оригинальное разрешения, если модель это позволяет)
Размер viewport определяется через выполнение JavaScript в браузере:
C#:
string raw = _instance.ActiveTab.MainDocument
.EvaluateScript("return window.innerWidth + ' x ' + window.innerHeight;");
Если выполнение скрипта не удалось (например, страница ещё не загружена), используется запасной вариант - получение размеров экрана из профиля браузера:
C#:
return new ViewportSize(
_project.Profile.ScreenSizeWidth,
_project.Profile.ScreenSizeHeight);
C#:
public ScreenshotData TakeScreenshotData()
{
ViewportSize vp;
PageOffset offset;
if (_sendFullPageScreenshot)
{
vp = GetFullDocumentSize();
offset = new PageOffset(0, 0);
}
else
{
vp = GetViewportSize();
offset = GetPageOffset();
}
string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".png");
string resizePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + "_r.png");
try
{
ZennoPoster.ImageProcessingCropFromScreenshot(
instancePort: _instance.Port,
savePath: tempPath,
cropWidth: vp.Width,
cropHeight: vp.Height,
leftBorder: offset.X,
topBorder: offset.Y,
units: "pixel",
quality: _screenshotQuality
);
double scaleFactor = 1.0;
int finalWidth = vp.Width;
int finalHeight = vp.Height;
string pathToRead = tempPath;
if (_maxImageDimension > 0 &&
(vp.Width > _maxImageDimension || vp.Height > _maxImageDimension))
{
if (vp.Width >= vp.Height)
{
scaleFactor = (double)vp.Width / _maxImageDimension;
finalWidth = _maxImageDimension;
finalHeight = (int)(vp.Height / scaleFactor);
}
else
{
scaleFactor = (double)vp.Height / _maxImageDimension;
finalHeight = _maxImageDimension;
finalWidth = (int)(vp.Width / scaleFactor);
}
ZennoPoster.ImageProcessingResizeFromFile(
tempPath, resizePath, finalWidth, finalHeight, "pixel", true, true);
pathToRead = resizePath;
}
return new ScreenshotData
{
Base64 = Convert.ToBase64String(File.ReadAllBytes(pathToRead)),
ScaleFactor = scaleFactor,
Width = finalWidth,
Height = finalHeight
};
}
finally
{
if (File.Exists(tempPath)) File.Delete(tempPath);
if (File.Exists(resizePath)) File.Delete(resizePath);
}
}
[HR]
3. Провайдеры: OpenAI, DeepSeek, локальные модели
В коде связь с моделью реализована через интерфейс
IApiProvider — это позволяет менять провайдера не затрагивая основную логику агента. На данный момент реализовано два провайдера:| Провайдер | Эндпоинт | Особенности |
|---|---|---|
| OpenAiCompatibleResponsesProvider | /v1/responses | Используется по умолчанию. Отправляет только новые сообщения через previous_response_id. Поддерживает flex и prompt caching |
| OpenAiCompatibleCompletionsProvider | /v1/chat/completions | Универсальный. Совместим с DeepSeek, Mistral, LM Studio, Ollama. В каждом запросе передаётся полная история диалога |
При работе с OpenAiCompatibleCompletionsProvider стоит проверять поддержку Function Calling у конкретной модели — у некоторых она работает нестабильно.
C#:
// DeepSeek
var options = new ZennoposterAiAgentOptions
{
ApiKey = project.Variables["deepseek_key"].Value,
Model = "deepseek-chat",
Provider = new OpenAiCompatibleCompletionsProvider(
"https://api.deepseek.com/v1/chat/completions"),
};
// Локальная модель через LM Studio (ключ не нужен)
var optionsLocal = new ZennoposterAiAgentOptions
{
ApiKey = "",
Model = "qwen/qwen3.5-9b",
Provider = new OpenAiCompatibleCompletionsProvider(
"http://localhost:1234/v1/chat/completions"),
};
// OpenAI — провайдер указывать не нужно, подставляется автоматически
var optionsOpenAi = new ZennoposterAiAgentOptions
{
ApiKey = project.Variables["openai_key"].Value,
Model = "gpt-5.4",
};
4. Контроль расходов
Всё взаимодействие с моделью измеряется в токенах — от этого напрямую зависит стоимость выполнения задачи. В агенте реализовано несколько механизмов контроля.
Лимиты выполнения
MaxSteps— максимальное число вызовов API за один запуск. По умолчанию5. Значение-1— без ограниченийMaxTokensLimit— жёсткий лимит суммарных токенов; при превышении запуск прерывается. Значение-1— без лимита (по умолчанию). Учитывается общий объём контекста без разбивки на кешированные/некешированные токены — это сознательное упрощение
Подсчёт стоимости
Если задать цены на токены — агент будет выводить примерную стоимость каждого запуска в лог и возвращать её в результате:
InputPricePerMillion— цена входных токенов в USD за 1 млнCachedInputPricePerMillion— цена кешированных входных токеновOutputPricePerMillion— цена выходных токенов
Результаты после выполнения:
result.TotalTokens— суммарное число токеновresult.TotalInputTokens,result.TotalCachedInputTokens,result.TotalOutputTokens— разбивка по типамresult.CostFormatted— итоговая стоимость в виде строки, например$0.004231
Конкретные цифры привести сложно — всё зависит от сложности задачи и контекста страницы. Ориентир: при
ToolGroups.All описание инструментов занимает около 2800 токенов в первом запросе. На последующих шагах эта часть кешируется и тарифицируется значительно дешевле.Ограничение набора инструментов
Если задача не требует всех групп — можно передавать только нужные:
C#:
// Только навигация и чтение DOM
ToolGroups = ToolGroups.Browser | ToolGroups.DomReading
// Все группы кроме хранилища ZennoPoster
ToolGroups = ToolGroups.NoStorage
C#:
All = Browser | MouseKeyboard | DomReading | FormInput | Screenshots | ZennoStorage
NoStorage = Browser | MouseKeyboard | DomReading | FormInput | Screenshots
NoScreenshots = Browser | MouseKeyboard | DomReading | FormInput | ZennoStorage
Minimal = Browser | MouseKeyboard | DomReading | FormInput
Важно: инструменты
wait и report_error добавляются всегда и не зависят от выбранных ToolGroups.Кеширование промпта
Поскольку описание инструментов не меняется от запуска к запуску, OpenAI может кешировать эту часть контекста и тарифицировать её по сниженной ставке. Два параметра управляют этим поведением:
PromptCacheKey— строковый ключ, идентифицирующий неизменяемую часть контекста. Одинаковый ключ на всех запусках гарантирует переиспользование кешаExtendedCacheRetention— увеличивает время хранения кеша с нескольких минут до 24 часов. Актуально при редких или ночных запусках
C#:
var options = new ZennoposterAiAgentOptions
{
PromptCacheKey = "my-project-tools-v1",
ExtendedCacheRetention = true,
// ...
};
Оба параметра поддерживаются только провайдером OpenAiCompatibleResponsesProvider. При использовании OpenAiCompatibleCompletionsProvider они игнорируются. Разница в цене между обычными и кешированными токенами обычно составляет 4–10 раз в зависимости от модели.
Начальный контекст страницы (InitialContext)
Агент может сразу получить текущее состояние страницы без первого шага через параметр
InitialContext:None— контекст не передаётся (по умолчанию)Screenshot— прикрепляется скриншотPageHtml— передаётся очищенный HTML страницыDomText— передаётся только текст (innerText)
Это позволяет сократить количество шагов и ускорить выполнение задачи, особенно при работе с простыми страницами. Также часто более простые модели, хуже оптимизированные для работы в качестве ИИ агентов, вроде chatgpt-nano, работают лучше при получении контекста в самом начале, в противном случае могут выдавать ошибку.
Управление объёмом HTML
При работе с реальными сайтами исходный код страницы может занимать десятки тысяч символов — большую часть из которых составляют <script>, <style>, SVG-иконки и инлайновые обработчики событий, бесполезные для навигации. Параметр
HtmlCleanOptions управляет тем, что именно отправляется модели при вызове get_page_html, get_element (с return_type=html), а также при использовании начального контекста.
C#:
// Настройки по умолчанию (рекомендуется для большинства задач)
options.HtmlCleanOptions = new HtmlCleanOptions
{
RemoveScripts = true, // удалять <script>
RemoveStyles = true, // удалять <style>
RemoveComments = true, // удалять <!-- комментарии -->
RemoveNoscript = true, // удалять <noscript>
RemoveLinkTags = true, // удалять <link rel="stylesheet"> и preload
RemoveSvg = false, // SVG может нести семантику (иконки с aria-label)
RemoveMeta = false, // meta нужна для og:title, description и т.п.
RemoveStyleAttributes = false, // атрибуты обычно не мешают
RemoveEventAttributes = false, // onclick="" помогает понять интерактивность
CollapseWhitespace = true, // схлопывать лишние пробелы и пустые строки
DefaultMaxLength = 8000, // лимит символов после очистки
};
| Предустановка | DefaultMaxLength | Что удаляется | Когда использовать |
|---|---|---|---|
| new HtmlCleanOptions() | 8000 | script, style, комментарии, noscript, link | По умолчанию, большинство задач |
| HtmlCleanOptions.Minimal | 8000 | script, style, комментарии, noscript | Страницы с важными SVG или meta |
| HtmlCleanOptions.Aggressive | 5000 | Всё лишнее + атрибуты style/onclick + svg + meta | Простые формы, листинги |
| HtmlCleanOptions.None | без лимита | Ничего | Отладка, нужен полный HTML |
C#:
// Лимит символов
options.SetHtmlMaxLength(project.Variables["html_max_length"].Value); // "5000", "-1"
// Предустановка целиком
options.SetHtmlCleanPreset(project.Variables["html_preset"].Value); // "Default", "Minimal", "Aggressive", "None"
// Комбо: предустановка + точная подстройка лимита
options.SetHtmlCleanPreset("minimal")
.SetHtmlMaxLength("12000");
max_length, передаваемый моделью, имеет приоритет над настройкой DefaultMaxLength в HtmlCleanOptions. Это позволяет модели динамически запрашивать больше или меньше контекста при необходимости.Оптимизация работы со скриншотами
Предусмотрена работа с оптимизацией скриншотов, можно сделать так, чтобы агент не отправлял скриншоты в исходном разрешении, в случаях когда, модель для обработки имеет ограниченный контекст.
Параметр
MaxImageDimension ограничивает максимальную сторону скриншота (например, до 1024px), сохраняя пропорции. При этом также происходит коррекция координат: при уменьшении картинки агент вычисляет коэффициент ScaleFactor. Модель видит сжатое изображение, но когда она возвращает координаты для клика, инструменты BrowserTools автоматически умножают их на этот коэффициент, обеспечивая корректное попадание в элемент на реальной странице. Это значение затем используется во всех инструментах, работающих с координатами (например, mouse_click). Стоит учитывать, что даже с учётом ScaleFactor, модель может возвращать неточные координаты. Поэтому рекомендуется использовать небольшие задержки и, при необходимости, повторные попытки клика.[/LIST]Помимо
MaxImageDimension, в агенте доступны дополнительные настройки, влияющие на передачу изображений в модель:ScreenshotQuality— качество сжатия изображения (1–100).
Чем ниже значение, тем меньше размер файла, но хуже детализация. По умолчанию:70ImageDetail— уровень детализации изображения, передаваемого в API.
Допустимые значения:"original","auto","low","high"
По умолчанию:"original"SendFullPageScreenshot— еслиtrue, снимается вся страница целиком, а не только видимый viewport.
Полезно для длинных страниц, но увеличивает размер контекста
C#:
options.ScreenshotQuality = 60;
options.ImageDetail = "auto";
options.SendFullPageScreenshot = true;
5. Создание повторяемых сценариев
Одно из частых пожеланий при упоминании ИИ — научить его создавать кубики действий самостоятельно. Полноценно реализовать это как обычным пользователям не получится, но можно создать работающую альтернативу: записывать действия агента и воспроизводить их без участия ИИ.
Для этого в настройках есть флаг
RecordMacro = true. При его включении в результате появляется result.Macro — записанная последовательность всех действий агента.Макрос сохраняется и загружается в виде простого текста:
result.Macro.SaveAsText()— сохранить в строку (удобно для переменных ZennoPoster)AgentMacro.LoadFromText(text)— загрузить обратно.WithoutReadSteps()— убрать служебные шаги чтения DOM, которые при воспроизведении не нужны.Slice(from, to)— взять только часть шагов
Пример того, как выглядит сохранённый макрос:
Код:
navigate url=http://lessons.zennolab.com/en/index
click by=xpath selector="//a[contains(., 'Simple registration')]" index=0
set_value find_by=xpath selector=//*[@id='email'] value=test@example.com use_selected_items=False index=0
set_value find_by=xpath selector=//*[@id='password'] value=MyPassword123 use_selected_items=False index=0
set_value find_by=xpath selector=//*[@id='password_repeat'] value=MyPassword123 use_selected_items=False index=0
click by=xpath selector="//input[@type='submit' and @value='Create']" index=0
wait ms=1000
Формат намеренно простой — при необходимости макрос можно отредактировать вручную прямо в переменной. Также доступен метод
.Save() для сохранения в JSON — более читаемый формат для аудита, менее удобный для ручного редактирования.
C#:
bool macroExists = !string.IsNullOrEmpty(project.Variables["order_macro"].Value);
if (!macroExists)
{
// Первый запуск: агент выполняет задачу и записывает макрос
var options = new ZennoposterAiAgentOptions
{
ApiKey = project.Variables["api_key"].Value,
Model = "gpt-5.4",
RecordMacro = true,
};
options.SetMaxSteps("20");
var agent = new ZennoposterAiAgent(project, instance, options);
var result = agent.Run(project.Variables["user_prompt"].Value);
if (!result.Success)
{
project.SendErrorToLog("Агент не смог выполнить задачу: " + result.ErrorReason);
throw new Exception(result.ErrorReason);
}
if (result.Macro != null)
{
var replayMacro = result.Macro.WithoutReadSteps();
project.Variables["order_macro"].Value = replayMacro.SaveAsText();
project.SendInfoToLog("Макрос записан, шагов: " + replayMacro.Steps.Count.ToString(), false);
}
}
else
{
// Последующие запуски: воспроизведение без API
var macro = AgentMacro.LoadFromText(project.Variables["order_macro"].Value);
var player = new AIAgentMacroPlayer(project, instance);
var playResult = player.Play(macro);
if (!playResult.Success)
{
project.SendWarningToLog("Воспроизведение не удалось: " + playResult.ErrorReason);
// Сбрасываем — на следующем запуске агент запишет новый макрос
project.Variables["order_macro"].Value = "";
throw new Exception("Макрос устарел, будет перезаписан на следующем запуске.");
}
project.SendInfoToLog(
"Макрос воспроизведён: " + playResult.StepsExecuted.ToString() +
"/" + playResult.StepsTotal.ToString() + " шагов.", false);
}
Если воспроизведение упало на середине — можно продолжить с нужного шага:
C#:
// Продолжить с 5-го шага (нумерация с 0)
player.Play(macro, stopOnError: true, fromStep: 4);
// Или воспроизвести только часть макроса
var partial = macro.Slice(4, 10);
player.Play(partial);
6. Устойчивость: autoRecovery и flex-режим
autoRecovery
Даже умные модели иногда ошибаются. Флаг
AutoRecovery = true включает механизм восстановления: если выполнение инструмента завершилось ошибкой, агент отправляет модели описание ошибки и скриншот текущего состояния браузера, давая ей возможность попробовать другой подход.Однако если вызван
report_error, восстановление не происходит — выполнение завершается сразу.AutoRecovery— включить восстановление после ошибок инструментовMaxConsecutiveRecoveries— максимум последовательных восстановлений подряд (по умолчанию3)
Важно: если модель сама вызвала
report_error — это окончательная ошибка. Это означает, что ИИ не смог найти решение задачи, поэтому autoRecovery в таком случае не применяется.flexMode
Flex service tier OpenAI — режим с пониженным приоритетом, дешевле примерно вдвое (на основании текущих тарифов на gpt-5.4), но с более высокой вероятностью таймаута или ошибки 429.
При
FlexMode = true агент не падает сразу, а повторяет запрос с экспоненциальной задержкой:5 с → 10 с → 20 с → ... (не более 60 с)
Если все попытки исчерпаны — автоматически переключается на стандартный приоритет и продолжает работу. Число повторов задаётся параметром
MaxFlexRetries (по умолчанию 3).7. Результат. Код для работы, плагин, примеры использования
Резюмируя, что может делать ИИ-агент:
- Открывать страницы, очищать кеш, куки браузера
- Читать содержимое страницы — текст, HTML, атрибуты конкретных элементов
- Делать скриншоты и анализировать визуальное состояние страницы
- Кликать, вводить текст, заполнять формы, выбирать значения в выпадающих списках (используя либо скриншоты, либо HTML самостоятельно принимая решения какой метод выбрать)
- Взаимодействовать с переменными, списками и таблицами ZennoPoster, как для чтения так и для записи (не будет работать в режиме плагина)
- Ждать загрузки динамического контента
- Сообщать о невозможности выполнить задачу — если зашёл в тупик
- Пытаться решить капчу (но нужно указывать напрямую в prompt что это нужно сделать, иначе чаще всего будет игнорирование)
- Работать с разными движками браузеров - можно использовать как Chromium, Chrome, так и ZennoBrowser, главное их предварительно запусить, так как все работает все через одинаковые методы.
Агент адаптируется:
- Если элемент не найден с первого раза — пробует другой селектор или подход
- Если страница изменилась — перечитывает DOM и корректирует действия
- При включённом
AutoRecovery— анализирует ошибку по скриншоту и пробует снова
Что агент не умеет:
- Гарантировать результат на любом сайте: различные изменения, сложное строение страниц, либо интерфейса может выдавать ошибки, но он будет пытаться.
- Работать бесплатно — каждый шаг это запрос к API и расход токенов, разве что можно использовать локальные модели, но качество работы в таком случае заметно хуже.
В конце статьи я прикрепляю файл проекта в котором находится весь нужный исходный код. Никаких зависимостей извне не используется. Также отдельно прикреплены проекты плагинов как самого ИИ агента так и проигрывателя макросов, для его легкого изменения и компиляции, а также непосредственно сам плагин для добавления в проекты.
Весь исходный код в проекте сопровождается комментариями, такой информации должно быть достаточно, чтобы при встраивании в проект было понятно что делает тот или иной параметр без необходимости подробно изучать реализацию, либо использовать ИИ для дальнейшей адаптации под свои нужды.
Вариант 1 — использование напрямую в C# коде
Наиболее гибкий вариант. Полный доступ ко всем настройкам и результатам.
C#:
var options = new ZennoposterAiAgentOptions
{
// --- Подключение к API ---
ApiKey = "sk-...", // Bearer-токен. Обязательное поле
Model = "gpt-5.4", // Идентификатор модели. Обязательное поле
Proxy = "host:port", // Прокси. Пустая строка — без прокси
Provider = new OpenAiCompatibleCompletionsProvider(
"https://api.deepseek.com/v1/chat/completions"
), // По умолчанию OpenAiCompatibleResponsesProvider
// --- Лимиты выполнения ---
MaxSteps = 10, // Макс. число шагов. -1 = без лимита, по умолчанию 5
MaxTokensLimit = -1, // Макс. токенов. -1 = без лимита (по умолчанию)
// --- Повторные попытки ---
MaxRetries = 1, // Повторов при пустом ответе (не-flex). По умолчанию 1
MaxFlexRetries = 3, // Повторов в flex-режиме при таймауте / 429. По умолчанию 3
// --- Автовосстановление ---
AutoRecovery = false, // Включить восстановление после ошибок инструментов
MaxConsecutiveRecoveries = 3, // Макс. восстановлений подряд. По умолчанию 3
// --- Поведение модели ---
SystemPrompt = "You are a helpful assistant.", // Системный промпт
FlexMode = false, // service_tier=flex — дешевле, но медленнее
ParallelToolCalls = false, // Параллельные вызовы инструментов за один шаг
// --- Кеширование (только OpenAiCompatibleResponsesProvider) ---
ExtendedCacheRetention = false, // Хранить кеш 24 ч вместо стандартного TTL
PromptCacheKey = "my-project-v1", // Ключ кеша для переиспользования
// --- Ценообразование (USD за 1 млн токенов, 0 = не считать) ---
InputPricePerMillion = 2.50m,
CachedInputPricePerMillion = 0.63m,
OutputPricePerMillion = 10.0m,
// --- Инструменты ---
ToolGroups = ToolGroups.All, // Набор активных групп инструментов
// --- Запись макроса ---
RecordMacro = false, // Записывать последовательность действий в result.Macro
// --- Логирование ---
LogToZennoPoster = false, // Дублировать лог в интерфейс ZennoPoster
// --- Начальный контекст страницы ---
InitialContext = InitialContext.None,
// Добавляет контекст текущей страницы к первому сообщению агента.
// None — без контекста (по умолчанию)
// Screenshot — скриншот текущего вьюпорта
// PageHtml — очищенный HTML страницы (применяется HtmlCleanOptions)
// DomText — текстовое содержимое страницы (innerText)
// --- Очистка HTML ---
HtmlCleanOptions = new HtmlCleanOptions(), // настройки по умолчанию
// Управляет тем, что удаляется из HTML перед передачей модели.
// Влияет на get_page_html, get_element (html) и InitialContext.PageHtml/DomText.
// Быстро: options.SetHtmlCleanPreset("aggressive").SetHtmlMaxLength("5000")
};
Поля, которые задаются из текстовых переменных проекта, устанавливаются через Set-методы — они принимают строку и сами разбирают значение. Некорректные или пустые строки молча игнорируются:
C#:
options
.SetMaxSteps(project.Variables["max_steps"].Value)
.SetMaxTokensLimit(project.Variables["token_limit"].Value)
.SetPrices(
project.Variables["input_price"].Value, // "2.5" и "2,5" — оба варианта работают
project.Variables["cached_price"].Value,
project.Variables["output_price"].Value)
.SetFlexMode(project.Variables["flex_mode"].Value) // "true" / "1" / "да"
.SetAutoRecovery(project.Variables["auto_recovery"].Value)
.SetToolGroups(project.Variables["tool_groups"].Value); // "Browser, DomReading"
.SetInitialContext(project.Variables["initial_context"].Value) // "None" / "Screenshot" / "PageHtml" / "DomText"
.SetHtmlMaxLength(project.Variables["html_max_length"].Value) // "8000" / "-1"
.SetHtmlCleanPreset(project.Variables["html_preset"].Value) // "Default" / "Minimal" / "Aggressive" / "None"
Результат выполнения — AgentRunResult
Метод
agent.Run() возвращает объект AgentRunResult. Он содержит всё необходимое для обработки результата, логирования и отладки.| Свойство | Тип | Описание |
|---|---|---|
Success | bool | True, если задача выполнена успешно |
Answer | string | Финальный текстовый ответ модели. Заполняется только при Success = true |
ErrorReason | string | Причина неудачи. Заполняется только при Success = false |
TotalTokens | int | Суммарное число токенов за весь запуск |
TotalInputTokens | int | Входные токены (включая кешированные) |
TotalCachedInputTokens | int | Из них — кешированные входные токены |
TotalOutputTokens | int | Выходные токены |
EstimatedCostUsd | decimal | Расчётная стоимость в USD. Ноль, если цены не заданы в настройках |
CostFormatted | string | Стоимость в виде строки, например $0.004231 |
Macro | AgentMacro | Записанный макрос. Заполняется только при RecordMacro = true |
DebugLog | List<AgentDebugEntry> | Подробный лог каждого шага: запрос, ответ и все вызовы инструментов |
C#:
var result = agent.Run(project.Variables["user_prompt"].Value);
// Основной результат
if (!result.Success)
{
project.SendErrorToLog("Ошибка: " + result.ErrorReason);
throw new Exception(result.ErrorReason);
}
project.Variables["ai_answer"].Value = result.Answer;
// Токены и стоимость
project.Variables["total_tokens"].Value = result.TotalTokens.ToString();
project.Variables["ai_cost"].Value = result.CostFormatted;
project.SendInfoToLog(string.Format(
"Токены — вход: {0} (кеш: {1}), выход: {2}, всего: {3}",
result.TotalInputTokens,
result.TotalCachedInputTokens,
result.TotalOutputTokens,
result.TotalTokens), false);
// Макрос (если RecordMacro = true)
if (result.Macro != null)
{
project.Variables["macro"].Value = result.Macro.WithoutReadSteps().SaveAsText();
}
// Полный трейс для отладки
project.Variables["debug_trace"].Value = result.GetDebugTrace();
Метод
GetDebugTrace() возвращает текстовый дамп всех шагов: тело каждого запроса к API, сырой ответ модели, имена и аргументы вызванных инструментов и их результаты. Удобно сохранять в переменную и смотреть при возникновении проблем.
C#:
var options = new ZennoposterAiAgentOptions
{
ApiKey = project.Variables["api_key"].Value,
Model = "gpt-5.4",
};
var agent = new ZennoposterAiAgent(project, instance, options);
var result = agent.Run(project.Variables["user_prompt"].Value);
if (!result.Success)
{
project.SendErrorToLog("Ошибка: " + result.ErrorReason);
throw new Exception(result.ErrorReason);
}
project.Variables["ai_answer"].Value = result.Answer;
C#:
var options = new ZennoposterAiAgentOptions
{
ApiKey = project.Variables["api_key"].Value,
Model = project.Variables["model"].Value,
Proxy = project.Variables["proxy"].Value,
LogToZennoPoster = true,
FlexMode = true,
AutoRecovery = true,
ToolGroups = ToolGroups.All,
PromptCacheKey = "my-project-v1",
};
options
.SetMaxSteps(project.Variables["max_steps"].Value)
.SetPrices("2.50", "0.63", "10.0");
var agent = new ZennoposterAiAgent(project, instance, options);
var result = agent.Run(project.Variables["user_prompt"].Value);
if (!result.Success)
{
project.SendErrorToLog("Ошибка агента: " + result.ErrorReason);
throw new Exception(result.ErrorReason);
}
project.Variables["ai_answer"].Value = result.Answer;
project.Variables["total_tokens"].Value = result.TotalTokens.ToString();
project.Variables["ai_cost"].Value = result.CostFormatted;
project.SendInfoToLog(string.Format(
"Токены — вход: {0} (кеш: {1}), выход: {2}, всего: {3}",
result.TotalInputTokens,
result.TotalCachedInputTokens,
result.TotalOutputTokens,
result.TotalTokens), false);
project.SendInfoToLog("Стоимость: " + result.CostFormatted, false);
C#:
// DeepSeek
var options = new ZennoposterAiAgentOptions
{
ApiKey = project.Variables["deepseek_key"].Value,
Model = "deepseek-chat",
Provider = new OpenAiCompatibleCompletionsProvider(
"https://api.deepseek.com/v1/chat/completions"),
};
// Локальная модель через LM Studio — ключ не нужен
var optionsLocal = new ZennoposterAiAgentOptions
{
ApiKey = "",
Model = "qwen/qwen3.5-9b",
Provider = new OpenAiCompatibleCompletionsProvider(
"http://localhost:1234/v1/chat/completions"),
};
OpenAiCompatibleResponsesProvider используется по умолчанию — явно указывать его не нужно.Хранение макроса можно вынести в файл или в любой другой удобный для проекта формат.
C#:
bool macroExists = !string.IsNullOrEmpty(project.Variables["order_macro"].Value);
if (!macroExists)
{
var options = new ZennoposterAiAgentOptions
{
ApiKey = project.Variables["api_key"].Value,
Model = "gpt-5.4",
RecordMacro = true,
};
options.SetMaxSteps("20");
var agent = new ZennoposterAiAgent(project, instance, options);
var result = agent.Run(project.Variables["user_prompt"].Value);
if (!result.Success)
{
project.SendErrorToLog("Агент не смог выполнить задачу: " + result.ErrorReason);
throw new Exception(result.ErrorReason);
}
if (result.Macro != null)
{
var replayMacro = result.Macro.WithoutReadSteps();
project.Variables["order_macro"].Value = replayMacro.SaveAsText();
project.SendInfoToLog("Макрос записан, шагов: " + replayMacro.Steps.Count.ToString(), false);
}
}
else
{
var macro = AgentMacro.LoadFromText(project.Variables["order_macro"].Value);
var player = new AIAgentMacroPlayer(project, instance);
var playResult = player.Play(macro);
if (!playResult.Success)
{
project.SendWarningToLog("Воспроизведение не удалось: " + playResult.ErrorReason);
project.Variables["order_macro"].Value = "";
throw new Exception("Макрос устарел, будет перезаписан на следующем запуске.");
}
project.SendInfoToLog(
"Макрос воспроизведён: " + playResult.StepsExecuted.ToString() +
"/" + playResult.StepsTotal.ToString() + " шагов.", false);
}
При возникновении проблем можно получить полный лог каждого шага — запрос к API, ответ модели и результаты всех вызовов инструментов:
C#:
// Записать полный трейс в переменную для просмотра
project.Variables["debug_trace"].Value = result.GetDebugTrace();
// Или пройтись по шагам вручную
foreach (var entry in result.DebugLog)
{
project.SendInfoToLog(entry.ToString(), false);
}
Для некоторых моделей, которые пока еще не заточены для работы с ИИ агентами свойственна проблема, связанная с тем что при отправке первого сообщения им нужно сразу подавать контекст работы, без которого они не запрашивают никаких действий со стороны программы (например такое свойственно для gpt5.4-nano).
Для этого у нас есть возможность прикрепить к первому сообщению контекст — скриншот или код страницы.
Для этого у нас есть возможность прикрепить к первому сообщению контекст — скриншот или код страницы.
C#:
/ Перед вызовом Run браузер уже находится на нужной странице.
// InitialContext позволяет передать её состояние в первом сообщении,
// чтобы агент сразу понимал с чем работает.
// Вариант 1: скриншот — для визуально сложных страниц
var options = new ZennoposterAiAgentOptions
{
ApiKey = project.Variables["api_key"].Value,
Model = "gpt-5.4nano",
InitialContext = InitialContext.Screenshot,
};
// Вариант 2: HTML — для страниц с богатой структурой DOM
var optionsHtml = new ZennoposterAiAgentOptions
{
ApiKey = project.Variables["api_key"].Value,
Model = "gpt-5.4nano",
InitialContext = InitialContext.PageHtml,
HtmlCleanOptions = HtmlCleanOptions.Aggressive, // сократить объём
};
// Вариант 3: текст — для простых текстовых страниц
var optionsText = new ZennoposterAiAgentOptions
{
ApiKey = project.Variables["api_key"].Value,
Model = "gpt-5.4nano",
InitialContext = InitialContext.DomText,
};
// Установка из переменной проекта
options.SetInitialContext(project.Variables["initial_context"].Value);
// допустимые значения: "None", "Screenshot", "PageHtml", "DomText"
var agent = new ZennoposterAiAgent(project, instance, options);
var result = agent.Run("Найди и нажми кнопку оформления заказа.");
Важно: Так как в текущем виде весь код находится в отдельном namespace, для того чтобы можно было его вызвать в вашем проекте после копирования этого кода, нужно также добавить
Вариант 2 — использование как плагинusing ZennoposterAiAgentCore; в "Директивы Using"Настройки задаются в графическом интерфейсе кубика, ничего лишнего не подключается. Описание каждого параметра можно посмотреть в C# коде выше — специально дублировать подсказки в плагин не стал.
Ограничения плагина:
- Нет доступа к переменным, спискам и таблицам ZennoPoster — используется
ToolGroups.NoStorage - На выходе только финальный ответ модели и макрос для повторного выполнения.
Подробнее про плагины и как их устанавливать можно прочитать в документации:
За что отвечают конкретные настройки можно прочитать выше, либо воспользоваться поиском по документу.
На видео я продемонстрировал конкретные примеры ИИ агента на lessons.zennolab.com:
Проект открыт для изучения и доработки. Используйте на свой страх и риск — автор не берёт на себя ответственность за сбои в работе или расходы на API. В коде могут иметься ошибки и баги. Будьте внимательны при его использовании.
Вложения
-
58 КБ Просмотры: 3
-
54,6 КБ Просмотры: 2
-
163,7 КБ Просмотры: 2
-
144,1 КБ Просмотры: 2
Последнее редактирование модератором:


