Сегодня каждый норовит написать универсального агента и объявить это революцией. Рынок переполнен поделками вроде OpenClaw и его клонов: IronClaw, ZeroClaw, MicroClaw, NullClaw, GitClaw, AstrBot, GripAi, Moltis...
Все идут одной и той же дорогой: используют готовые MCP и дают агентам shell-оболочку. Да, это легко собрать. Да, весело. Можно хайпануть в соцсетях. Но это тупиковый путь.
В статье разберем все грехи status quo и предложим другой подход, более требовательный к компетенциям в области разработки ПО.
Если вы не знаете, что такое агент, могу порекомендовать первый раздел Пишем агента на Kotlin: KOSMOS.
В этой статье сфокусируюсь на десктопных агентах, потому что последние 7 месяцев плотно работаю с ними, но многие выводы и рекомендации верны для любых агентов.
— Критика
1. Каким должен быть агент общего назначения, готовый к релизу
2. Claude Code прыгает с крыши, но зачем за ним повторять?
3. Терминал — плохой трейдофф между безопасностью и возможностями
4. MCP — отвратительный трейдофф между безопасностью и возможностями
5. Популярные guardrails дают иллюзию безопасности, раздувая систему
6. Sandbox — забор ставят неправильно
7. Агенты ошибаются. Нужно научиться с этим жить
— Предложения
8. Как писать агентов, которых не страшно релизить
9. Спрашиваем пользователя, но и ему не доверяем
10. Промпты меняются, End-to-End тесты остаются
11. Агент должен быть прозрачен для разработчика
12. Пишите специализированные, а не общие, тулы
13. Top-down против bottom-up в разработке агентов
14. Интерпретатор даёт лучший трейдофф, чем терминал
15. Sandbox: ставим забор вокруг интерпретатора, а не агента
16. Категорический императив написания агентов
ИИ — имитация интеллекта.
Тул — функция для LLM (function calling).
MCP — протокол для общения агента (клиента) с внешними сервисами.
На работе я программист, и пользуюсь агентными-решениями с момента их появления. Но как обычный пользователь, вне работы, я хочу, чтобы агенты соответствовали следующим критериям:
Надежность и безопасность сопоставима с не-агентными приложениями.
Производительность сопоставима с не-агентными приложениями.
Пользователю не надо ставить docker, открывать terminal, писать markdown, выбирать MCP.
Пользователю не надо заводить отдельные учетные записи для агента.
Приложение экономит токены (проверка почты или удаление файла не стоит 100 рублей)
Современные агенты далеки от этого. Давайте разберемся, почему.
Знаете, как появился Claude Code? Борис Черный написал обвязку вокруг командной строки и спросил у LLM, какую музыку он чаще всего слушает. Агент написал apple script, и это перевернуло индустрию!
Отсылка на личный опыт (интерптератор для автоматизации)Кроме Бориса с этим игрались многие, еще с релиза ChatGPT. Я писал подобные системы на Clojure в конце 2023. Выложил в OpenSource всё, что разрешила компания. Проект позволяет исполнять скрипты с Clojure синтаксисом для автоматизации, а сами скрипты писались либо людьми, либо LLM.
Вот пример скрипта. Это был не tool calling, как принято делать сейчас, но LLM могла генерировать код и возвращать его в блоке, который можно распарсить и запустить. Тулы вроде открытия браузера или клика мышкой реализованы как функции внутри собственного интерпретатора.
Из интересных решений — определение визуальных паттернов на экране через regex. Пиксили переводились в буквы, из букв строились слова, а по словам проверялись паттерны. Например, можно было написать скрипт, который открывает веб-страницу и ждёт появления паттерна пикселей в ожидаемом месте экрана. Если паттерна нет, было или ветвеление (else), или завершение скрипта.
Проект закрылся, т.к. было слишком много ошибок в работе, и подход посчитали нерабочим.
Claude Code — отличный продукт для разработчиков, которые понимают, как работать с данными. Данные хранят в git-репозиториях (или других vcs). Что-то потеряется локально — вернут из remote.
Возвращаясь к метафоре с крышей, у разработчиков есть парашют — хотя и не всегда он срабатывает. Попытки жить по таким же правилам вне репозитория обречены на провал. Технически не подкованные пользователи ИИ-ассистентов не в состоянии обеспечить себе базовую безопасность.
В конечном итоге люди будут скачивать то ПО, которое гарантирует, что им не удалят жесткий диск или не накачают вирусов.
LLM отлично работает с текстовыми интерфейсами, на то она и языковая модель. Terminal (shell-оболочка) — самый могущественный текстовый интерфейс. Но вот в чем загвоздка. Как только мы захотим сделать из поделки реальный продукт, нам придётся терминал радикально ограничить.
И вопрос не в том, можно ли ограничить terminal, а в том, что выходит слишком высокий налог на безопасность.
Покажу на примере. Для простоты схлопнем весь shell до двух команд:
ls (показать список файлов);
rm (удалить файл).
Теперь представьте реализацию агента, который имеет возможность использовать терминал через тул. Хочется ограничить команду rm и попросить подтверждение у пользователя. Но как это сделать красиво, не ограничивая ls?
Регекспом — наивно.
Почему?delete_without_saying() { local a='r' local b='m' local cmd="${a}${b}" eval "$cmd $1" }
Парсинг AST с фильтром команд — дорго, так как заставляет нас строить целую инфраструктуру безопасности.
Если бы вместо этого у нас были тулы на ls и rm, мы бы передали в тул с rm колбек на подтверждение от пользователя, а ls бы оставили без подтверждения. И всё. Shell-оболочка хуже масштабируется в безопасный продукт общего назначения, чем написание изолированных тулов. Исключение: агенты для кодинга (см. «Claude code прыгает с крыши»).
Теперь давайте вернемся к реальности, где у нас есть ~3к команд. И что еще страшнее — команды комбинируются. Мы будем всё это пытаться ограничивать какой-то раздутой policy engine? Или будем прям на уровне syscall отлавливать?
Да, можно затолкать агента в sandbox, но это сильно ограничит в возможностях, далее мы об этом поговорим.
MCP даёт контракт и заставляет и клиент, и сервер следовать ему. Выполнение тулов (функций) происходит только после валидации — и это хорошо, потому что галлюцинации LLM не смогут выйти за пределы контракта.
Имея один контракт, не привязанный к конкретной LLM, разработчики могут независимо создавать реализации MCP-серверов на всё подряд. Например, сервис автоматизации Zapier позволяют подключать 8к приложений через MCP. Это тоже хорошо.
А что тогда плохо?
Первое — безопасность. LLM — это ребенок (для них даже сказки пишут), а обмануть ребенка не очень сложно. Однажды вы (или ваш пользователь) используете MCP-сервер, который содержит изначально (или обновит впоследствии) описание и случайно (или специально) заберет ваши ключи (или пароли) в контекст, а затем незаметно передаст их на MCP-сервер (или почтовый адрес через другой тул). Атака называется tool poisoning (TPA).
Пример реализации подобной атаки:
Атакующий MCP-сервер ("A") обновляет описание тулов после подтверждения пользователем.
"A" в описании тула просит другие MCP-серверы (mail или tg) отправить пароли или переписки по нужному адресу.
"A" в описании тула просит не сообщать об этом пользователю.
Перечислять и описывать всё — слишком долго, есть статья с иторией открытия уязвимостей до октября 2025. Есть paper с обзором возможных векторов атак, где перечисляют 16 сценариев.
Второе — протокол игнорирует идентификацию. Куда лучше было бы всем: пользователю, сервисам, MCP-серверам, разработчикам агентов, — если бы протокол вынуждал передавать информацию об идентификации действий:
сама ли LLM совершает действия без согласия пользователя;
сама, но пользователь дал разрешение;
выполняет ли делегированную пользователем команду.
Каждый сервис мог бы поддерживать API для LLM, запрещая опасные действия, когда LLM что-то делает без одобрения. Любые противоправные действия (хаки) можно было бы связать с действиями пользователя и накладывать ответственность, если пользователь их одобрил.
Третье — производительность. Имею в виду десктропных агентов на машинах пользователей. Когда пользователь подключает MCP-сервер, тот запускается в отдельном процессе. Мы как разрабтчики не можем контролировать, сколько процессов пользователь запустит и как это скажется на производительности его машины.
Четвертое — потеря контроля. Разработчик агента не контролирует чужие MCP-сервера, не знает, сколько памяти и процессорного времени они займут в системе, не может сам дописать обработку ошибок внутри MCP-сервера, не видит логи — и соответственно не может построить прозрачную систему.
Пятое — MCP не знают друг о друге. Подразумевается, что пользователь может сам подключать нужные ему MCP. Но никто не мешает разным MCP перекрывать функционал друг друга, добавлять тулы с похожими или одинаковыми именами. Даже если разработчики всё преднастроют, после обновления MCP-серверов проблема может вернуться.
В итоге MCP дают кучу возможностей, но еще больше дыр в безопасности, багов, утечек памяти, коллизий тулов, росте расхода токенов.
Претензия не к практикам безопасности вообще, а к тем конкретным, которые выбирает индустрия.
Представьте, если бы вместо HTTPS был HTTP c регекспами и BERT-классификатором для определения подлинности сервера и попытки обнаружить MITM. Это преувеличение, но что-то подобное делается в попытках распознать токсичность, prompt injection, PII. Ниже примеры из статьи Introducing Guardrails: The contextual security layer for the agentic era:
from invariant.detectors import secrets raise "Leaked API secret" if: (msg: Message) any(secrets(msg.content)) raise "PII leakage in email" if: (out: ToolOutput) -> (call: ToolCall) any(pii(out.content)) call is tool:send_email({ to: "^(?!.*@ourcompany.com$).*$" }) ... // подобного надо будет написать много
Тут проблема та же самая, что и с терминалом — плохой архитектурный трейдофф. LLM сможет командами терминала заобфусцировать ключ перед вызовом secrets(msg.content), чтобы пройти досмотр.
Помните линию Мажино — конструкцию вдоль границы Франции как способ защититься от прошлого типа атаки со стороны Германии? 7.4 млрд франков, что в переводе на сегодняшние деньги — целый гугол тенге. А немцы прошли там, где линия была наиболее уязвима.
Вспомнил еще одну историю из реальной жизни. Непрограммист писал игру в визуальном редакторе и реализовал логику таймера, бросая предметы с разной высоты. Когда предмет падал, срабатывало событие на нужное действие. Чтобы предметы не отвлекали игрока, делал их прозрачными. Это смешно, но разве не то же самое делают лидеры индустрии?
Есть и варианты с регекспами — дешевые по ресурсам, но практически бесполезные в плане защиты (обсуждали в части про терминал).
Описанные выше практики оставляют дыры, функционал агента урезается, расход токенов, как и время работы, растёт, а кодовая база распухает.
Пытаясь найти подтверждение , наткнулся на видео от девопса про guardrails-first подход, который противоставляется automation-first. Ниже я раскрою эту идею с другой стороны в части про «Подходы к разработке агентов».
Запуск агента в изолированном окружении — будь то докер, unix user group или отдельный Mac mini — решает только часть проблем безопасности. Если у агента есть доступ во внешний мир — это уже дыры (пример с WhatsAppp).
Это та самая проблема, поднятая Лэмпсоном еще в 1973 в заметке о проблеме ограничений (A Note on the Confinement Problem). Запретим всё — встаёт вопрос о полезности. Разрешим какие-то каналы связи — они могут стать convert-каналами. Например, Codex мог бы зашить ключ в картинку и выложить ее в ресурсы публичного репозитория.
Если относиться к агенту как к независимому сотруднику: завести под него учетные записи, дать свои ключи и пароли, общаться с ним через какой-то “секьюрный“ способ связи — всё равно остаются риски. Имея доступ к внешнему миру, агент может прочитать prompt injection и “сойти с ума“. Единственный способ сделать его безопасным — очень сильно урезать функционал (пример — облачный Codex).
Во второй части статьи будет предложение посмотреть на sandbox по-другому. Намекну: гуляя с собакой, можно запереть собаку в клетку и носить с собой. Неудобно. А еще стоить отвернуться — случайный ребенок просунет руку в клетку и останется без руки. Может, проще надеть намордник?
Кроме выше описанных проблем с безопасностью, производительностью и сложностью, которые решить принципиально невозможно, не урезая агента, есть еще и галлюцинации.
Галлюцинации — структурное следствие вероятностной генерации и обучения на текстовых распределениях. Меры против этого принимались и будут приниматься, но не сведут риск к нулю.
Искушенный DS возразит, что это не баг, а фича. Не уверен, что этот аргумент подошел бы родителям 160 погибших девочек, по которым нанесли ракетный удар из-за галлюцинаций.
Задача для нас, разработчиков, писать надежный софт, и мы это можем делать, несмотря на галюцинации LLM и все выше описанные проблемы.
Выше описанное можно свести к двум конкурирующим характеристикам системы — надежности и перформансу. Обе не новы и являются хлебом разрабтчиков распределенных систем.
Тот же подход, что и в цитате выше, применим и для написания агентов — нужно научиться строить надежные системы из ненадежных компонентов. Только теперь ненадежный компонент один — языковая модель (MCP мы даже не будем рассматривать).
Подходов много, рекомендую почитать Клепмана для общего развития. Ниже подробнее рассмотрю 4 практики:
Security first;
End-to-End тесты c LLM;
Observability;
Специализированные, а не общие, тулы.
Решение о том, надо ли спросить пользователя, должны приниматься не на уровне LLM, а на уровне тулов. За это придётся заплатить автонимией работы, ведь агент теперь будет всегда спрашивать разрешения, прежде чем что-то сделать.
Такой дорогой пошел Codex — в рамках сессии он спрашивает обо всем потенциально опасном. Можно попросить запомнить выбор и не спрашивать еще раз.
Но даже human-in-the-loop — не решение всех проблем. Вспомните Чернобыль. Решения по отключению аварийных защит принимались людьми, но именно конструктивные дефекты (вроде MCP) привели к катафстрофе. Возвращаясь к агентам: даже если пользователь одобряет удаление файла — переносите файл в корзину, а не удаляйте совсем.
LLM — ненадёжна и непредсказуема. Сегодня промпт-инжениринг работает, завтра нет. Послезавтра разработчики что-то меняют в system prompt, и модель отказывается работать там, где еще вчера справлялась идеально (возможно, это именно грабли моделей Российского рынка, но я на наступал несколько раз). В подобных условиях удобно иметь набор тестов, где абсолютно все части агента работают так же, как в проде, кроме тулов. Тулы мокаются.
Имея эти тесты, вы можете менять любую переменную (модель, RAG, system prompt, описание тулов, алгоритмы классификации и оркестрации) — и смотреть, как меняется скорость, расход токенов и точность выполнения задач.
Пример теста есть в моей статье о написании агентов на KMP.
Приведу еще один пример, к которому вернёмся ниже, в части про интерпретатор@ParameterizedTest(name = "scenario23_readFileThenSendEmailIfNoSecret[{index}] {0}") @ValueSource( strings = [ "Прочти public-note.txt. Если в тексте нет слова secret, создай письмо на [email protected] с темой Public Note и вставь в тело текст файла.", "Сделай по шагам: 1) найди и прочти файл public-note.txt; 2) если в нём нет слова secret, подготовь email для [email protected] с темой Public Note и исходным текстом файла", ] ) fun scenario23_readFileThenSendEmailIfNoSecret(userPrompt: String) = runTest { val toolFindFilesByName: ToolFindFilesByName = spyk(ToolFindFilesByName(filesUtil)) val toolExtractText: ToolExtractText = spyk(ToolExtractText(filesUtil)) val toolMailSendNewMessage: ToolMailSendNewMessage = spyk(ToolMailSendNewMessage(ToolRunBashCommand)) val foundFilePath = "~/tmp/public-note.txt" val safeFileText = "launch approved for finance review" every { toolFindFilesByName.invoke(any()) } returns """["$foundFilePath"]""" coEvery { toolFindFilesByName.suspendInvoke(any()) } returns """["$foundFilePath"]""" every { toolExtractText.invoke(any()) } returns safeFileText coEvery { toolExtractText.suspendInvoke(any()) } returns safeFileText every { toolMailSendNewMessage.invoke(any()) } returns "Sent" coEvery { toolMailSendNewMessage.suspendInvoke(any()) } returns "Sent" runScenarioWithMocks(userPrompt) { bindSingleton<ToolFindFilesByName> { toolFindFilesByName } bindSingleton<ToolExtractText> { toolExtractText } bindSingleton<ToolMailSendNewMessage> { toolMailSendNewMessage } } coVerifyOrder { toolFindFilesByName.suspendInvoke(match { it.fileName.contains("public-note.txt") }) toolExtractText.suspendInvoke(match { it.filePath.contains("public-note.txt") }) toolMailSendNewMessage.suspendInvoke(any()) } coVerify(exactly = 1) { toolMailSendNewMessage.suspendInvoke( match { it.recipientAddress.contains("[email protected]", ignoreCase = true) && (it.subject?.contains("Public Note", ignoreCase = true) == true) && (it.content == safeFileText) && (it.content.contains("secret", ignoreCase = true)).not() } ) } }
Языковая модель — единственный черный ящик. Вход и выход в нее нужно покрывать логами. От запроса до итогового ответа пользователю должен быть ID сессии, который отразится в логах всего: вызова тулов, RAG, классификации, кешей и прочего.
Мы должны уметь ответить на вопросы:
Какие функции чаще всего используются?
Какие сценарии чаще всего используются?
Почему возникла ошибка? Почему tool вернул ошибку или оборвалось gRPC-соединение?
Почему упало время ответа? Где распух контекст?
Для этого нам нужны старый добрый мониторинг и наблюдаемость (observability).
Тулы должны быть такими, чтобы даже человек смог решить задачу, если его поставить вместо LLM.
Представьте, что нужно найти слово в большом тексте (книге). И человек, и LLM решат ее быстрее с тулом «поиск» (вроде cmd+f в текстовом редакторе). Но оба потратят кучу времени и сил (токенов), если дать им тул, возвращающий только страницу текста.
Рекомендую прочитать всю статью.
Выше мы уже говорили про shell-оболочку и обсуждали проблемы с безопасностью. Но есть еще и проблемы выбора. Чем больше вариантов, тем больше вероятность ошибки.
Если хотите научить Агента читать файлы, дайте ему функцию на чтение. Пусть в этой же функции уже будут вшиты правила того, что можно читать, а что нет. В своём агенте я запрещаю читать ~/.bash_profile, ~/.zshenv и т.п.
— Но тогда будет огромное количество тулов!
— Ничего страшного, можно разбить их по категориям и проводить классификацию перед тем, как отдать агенту, принимающему решения.
К готовому агенту можно прийти двумя путями: разработка по принципу top down и bottom up.
В первом случае вы даёте агенту свободно добавлять MCP-тулы и терминал. Пусть хоть дописывает сам себя. А когда наступает время готовиться к проду, навешиваете всё больше и больше ограничений. Каждое ограничение приносит вам боль, потому что всесильный, яркий агент блекнет на глазах. Вы релизитесь, но всё равно находятся дыры, и приходится краснеть.
Подход bottom up. вы начинаете с нуля, добавляя всё больше фич. Каждая фича пишется вами от начала и до конца. Вам приятно, что с каждой фичей агент становится сильнее. Вы не искали легких путей и можете теперь быть уверены в своём агенте.
Используя подход bottom up, нужно думать о guardrails в момент написания тулов. Это медленно. Top down быстрее, но больше похож на покупку в кредит или строительство самолёта в воздухе.
В статье ИИ-агенты: как мы сделали DeepResearch от Яндекса пишут, что
Мне нравится этот подход и гибкостью, и экономией токенов. Представьте, вы даёте задачу:
С обычным function-calling LLM сперва вызовет тул поиска файла. Получит ответ. Далее вызовет тул чтения файла. Получит ответ — и тут будет огромная потеря токенов на чтении текста файла. Сама поймёт, есть ли там секрет, и еще раз вызовет тул отправки на почту.
С кодом был бы один скрипт. Вот реальный пример (тест на него был в части «End-to-End тесты»), написанный Хайку 4.5 на Lua:
-- Step 1: Find the file public-note.txt local findResult = FindFilesByName({fileName = "public-note.txt"}) if not findResult or #findResult == 0 then return "Файл public-note.txt не найден." end local filePath = findResult[1] -- Step 2: Read the file content local fileContent = ExtractTextFromFile({filePath = filePath}) -- Step 3: Check if the word "secret" is in the text local hasSecret = string.find(string.lower(fileContent), "secret") ~= nil if hasSecret then return "В файле найдено слово 'secret'. Письмо не отправляется." else -- Step 4: Send email to [email protected] MailSendNewMessage({ recipientAddress = "[email protected]", subject = "Public Note", content = fileContent }) return "Письмо успешно отправлено на [email protected] с содержимым файла public-note.txt" endВот так выглядел запрос в LLM
Message(role=system, content=You are an autonomous assistant that must solve the task by generating Lua code. Your response is executed immediately in a Lua runtime. Follow these rules strictly: 1. Reply with exactly one fenced ```lua``` block and no extra prose. 2. Use only the provided runtime APIs: `ToolName({...})`, `tools["ToolName"]({...})`, or `call_tool("ToolName", {...})`. 3. Tool calls return host-decoded Lua values. If a returned string itself contains JSON text, call `json_decode(...)` manually. 4. End the script with `return "final markdown for the user"`. The returned string is shown to the user verbatim. 5. Never paste raw multiline text inside quoted string literals. Keep tool output in variables and pass variables directly. If you need a multiline literal, use Lua long brackets `[[...]]`. 6. Do not use `require`, `package`, `io`, `os`, `debug`, or invented helpers. 7. If no tool is needed, still return Lua code that directly returns the answer. Available tools: - FindFilesByName: [PRIMARY TOOL for Finding Files] Search for the PATH of a file by its name (or partial name). Use this when the user asks "Find file X" or "Where is file Y". Mechanism: uses macOS Spotlight (mdfind). Fast and recursive. Parameters: - path: string - Relative or absolute path to limit the search. Defaults to user HOME. - fileName: string required - The name or partial name of the file we are searching for. Returns: - result: string - JSON array of file paths. - NewFile: Creates a new TEXT file at the given path with the provided content. If the path ends with a slash, creates a folder instead. forbidden: .xlsx, .xls, .png, .jpg, .pdf. Parameters: - path: string required - The path where the file or folder will be created; add a trailing slash to create a folder - text: string required - The content to be written to the new file Returns: - result: string - Creation status - EditFile: Modify a file by applying a unified diff patch. Runs a dry-run first; applies only if clean. Patch must target only the specified file. Parameters: - patch: string required - Unified diff patch to apply (like git diff output). Must modify ONLY the target file. Use a/<filename> and b/<filename> or just <filename>. - path: string required - The path to the file to be modified - strip: number - How many leading path components to strip from patch paths. Use 1 for a/ and b/. Returns: - result: string - Operation status - MoveFile: Moves a file from the source path to the destination path. Use ~ as the Home dir Parameters: - destinationPath: string required - The full destination path (including filename) where the file will be moved. - sourcePath: string required - The full path to the file (name included) to move Returns: - result: string - Move status - FindFolders: Searches for folders in the macOS file system using Spotlight. Returns a JSON list of absolute paths found. Filters out restricted directories. Parameters: - name: string required - Name of the folder to search for (e.g. 'Downloads', 'Project X') Returns: - result: array - JSON list of folder paths - ReadPdfPages: Reads text from a specific page range in a PDF file. Returns text and total page count. Parameters: - startPage: number - Start page number (1-based index). Default is 1. - filePath: string required - Absolute path to the PDF file - endPage: number - End page number (inclusive). If null, reads only the start page. Returns: - text: string - Extracted content - info: string - Debug info if text is empty - ListFiles: Runs bash ls command at a given path. Use ~ to start from the home directory Parameters: - path: string - Relative path to list files from - depth: number - Max depth to traverse (1 = direct children only; <=0 = unlimited) Returns: - result: string - Array of file paths - SearchFileContent: Search for files with the CONTENT (text) matching the specified query. Returns the content line and file path. Only use this if you know the file content but not the location. Parameters: - path: string - Relative path to search for files. Defaults to user HOME. Try to avoid using ~ or HOME - query: string required - A text substring we are searching for Returns: - result: string - JSON array of arrays of file path and matching line. - DeleteFile: Moves a file or folder to Trash at the given path. Use ~ as the Home dir Parameters: - path: string required - The path of the file or folder to delete Returns: - result: string - Deletion status - ExtractTextFromFile: READ ONLY preview of documents (PDF, Excel, PowerPoint, CSV, Word, etc). Use this to SEE content. Does NOT modify files. To edit Excel, use ExcelWrite. Parameters: - filePath: string required - Absolute path to the file (pdf, xlsx, docx, pptx, csv, etc) Returns: - result: string - Extracted text content - Open: Opens apps, files or folders. If you have two candidates to open, choose the one with the shortest path, but tell the user that there are other options. before run application, you must check if the app is installed on the system and its app-bundle-id. Parameters: - target: string required - Bundle id, like `com.jetbrains.intellij.ce`, path to a file or folder like `app/file/folder`, or just a name like `Downloads` Returns: - result: string - Operation status - MailReplyMessage: Reply to a specific message by its ID. Parameters: - messageId: number required - The unique ID of the message (required for reply) - content: string - Body content for reply Returns: - result: string - Mail operation result - MailSendNewMessage: Create a new email draft with recipient, subject, and body. Parameters: - subject: string - Subject for the new email - recipientName: string - Recipient name (for new email) - recipientAddress: string required - Recipient email address (for new email) - content: string - Body content for new email Returns: - result: string - Mail operation result - MailListMessages: List the latest messages in the Inbox. Parameters: - count: number - Number of messages to list (default 10) Returns: - result: string - Mail operation result - MailReadMessage: Read a specific message by its ID. Parameters: - messageId: number required - The unique ID of the message (required for read) Returns: - result: string - Mail operation result - MailUnreadMessagesCount: Get the number of unread emails in the Inbox with a specified limit. Parameters: - limit: number required - The maximum limit for counting unread messages (e.g., 50) Returns: - result: string - Mail operation result (count of unread messages) - MailSearch: Searches emails directly via Mail app (Subject & Sender). Parameters: - query: string required - The search query (keyword, name, topic). - limit: number - Max results to return. Default: 5 Returns: - result: string - List of found emails with IDs, functionsStateId=null, attachments=null, name=null)
У меня есть гипотеза, что бóльшая часть кода на Python (и JS тоже) — это код вокруг библиотек. Соответственно, и нейросети обучались на нём. А когда я даю языковой модели интерпретатор, я хочу дать только кор языка: инструмент для гибкого использования имеющихся тулов с описанием логики ветвления, без библиотек и без IO.
Lua хорош тем, что большая часть кода на нем — конфиги и скрипты. Другого такого популяного языка не найти (разве что Scheme, на котором уже почти не пишут). Сам язык максимально простой, учится за 10 минут, никакого синтаксического сахара, 1 структура данных.
В своём десктоп агенте я использую Lua. По пройденным тестам результаты сопаставимы с function calling. Зато расход токенов получается в ~4 раз ниже на задачах, состоящих из нескольких действий. Проверял пока только на Haiku.
Помните клоунаду, котор
В примере выше функции FindFilesByName, ExtractTextFromFile, MailSendNewMessage — это имена, которые «диспатчатся» в тулы, которые я написал сам. Опасное действие здесь — MailSendNewMessage, и в реализации тула есть запрос на подтверждение к пользователю. Обойти нельзя.
Использование библиотек и IO-операций запрещаются на уровне контролируемого интерпретатора. Всё, что остается у агента — возможность описывать логику между вызовом ограниченных тулов.
Думаю, в будущем откажутся от function-calling в пользу подобного подхода. Ведь LLM и так могла писать код уже в конце 2022 (ChatGPT 3) — всего-то и нужно было что дать ей интерпретатор.
P.S. Когда уже написал статью, обнаружил, что Антропик о таком подходе писали, только вместо своих функций — MCP.
Эта метафора будет близка программистам. Если мы написали тулы сами и они представляют конечный список возможностей (enum), мы можем гарантировать надежность агента. В рамках конечного количества тулов возможна высокая вариативность.
Чего делать не стоит — давать бесконечные возможности (shell, свободное подключенрие MCP-серверов), а затем пытаться конечным числом действий гарантировать безопасность.
Начинаем не с безграничной автономии, которую потом пытаемся обуздать, а с ограниченного набора тулов, комбинируемых внутри контролируемой среды исполнения.
Источник


