Ship 26 NYC — Воркшоп — Мини-воркеры
VVercel
Computing/SoftwareSmall Business/StartupsInternet Technology
Transcript
00:00:00Всем привет. Меня зовут Джонатан Клем, или можете звать меня Джей Клем, я разработчик программного обеспечения в
00:00:11Notion, где работаю над нашей платформой для разработчиков, но в основном я сосредоточен на более новом продукте,
00:00:17о котором вы, возможно, слышали, под названием Notion Workers. Сегодня на этом воркшопе я дам
00:00:23вам небольшой обзор того, что такое Notion Workers и почему мы построили их с использованием Vercel
00:00:28Sandbox. А затем я покажу вам своего рода мини-версию Workers,
00:00:36чтобы вы могли составить представление о том, как подобный продукт создается с помощью Vercel Sandbox.
00:00:43Итак, если вы не знакомы с тем, что такое Workers, — это SDK и среда выполнения, где вы можете писать
00:00:50пользовательский код и расширять с его помощью Notion. Вы можете, например, синхронизировать сторонние данные в Notion,
00:00:57писать кастомные вызовы инструментов для ваших агентов. Мы видели, как люди делали безумные веселые вещи, вроде заказа
00:01:04продуктов через Notion Workers или управления умным домом. Мы также видели очень сложные рабочие процессы,
00:01:11особенно из сферы IT и безопасности. Прелесть Workers в том, что здесь нет
00:01:17инфраструктуры, которой вам нужно управлять. Вы просто пишете код или заставляете AI-агента написать его за вас, а Notion
00:01:23берет на себя заботу о том, чтобы он всегда был доступен и работал. Это дало огромное преимущество пользователям Notion,
00:01:30особенно разработчикам. Им больше не нужно ждать, пока Notion создаст для них нативные
00:01:36интеграции. Все, что, по их мнению, Notion мог бы делать, но не делает, они могут написать
00:01:42самостоятельно. Поэтому, когда мы только приступили к созданию Notion Workers, моей главной заботой была
00:01:53безопасность. У меня есть небольшой опыт создания платформ, исполняющих ненадежный пользовательский код.
00:01:59Я, например, долгое время работал над GitHub Actions. И меня больше всего беспокоила
00:02:04сложность построения инфраструктуры для подобного продукта, и особенно безопасность. Есть масса
00:02:09вещей, о которых приходится переживать. Например, нужно убедиться, что код, написанный пользователями,
00:02:14не мог получить доступ к базам данных Notion или сервисам Notion, к которым у него не должно быть доступа.
00:02:20доступа. Также нужно гарантировать, что пользователи не смогут влиять на других пользователей. Вы же не хотите, чтобы код одного
00:02:26пользователя мог получить доступ к коду другого пользователя или, очевидно, к его секретам или чему-то
00:02:31в этом роде. Дело не только в безопасности. Речь идет также о справедливости и разделении ресурсов. Если какой-то
00:02:37пользователь делает что-то, что потребляет кучу CPU и кучу памяти, вы хотите быть уверены, что это не
00:02:42ущемляет других пользователей, пытающихся делать свои задачи в то же самое время. Я отвлекусь на минуту
00:02:48и расскажу небольшую историю. Разделение ресурсов — это действительно очень
00:02:54хитрая штука. Это, вероятно, займет часть моего времени, но мне нравится эта история. Когда мы строили
00:02:58GitHub Actions — и мы думали об этом же и с workers — как только у вас появляется платформа
00:03:02для выполнения произвольного кода, люди сразу же начинают пытаться майнить на ней криптовалюту.
00:03:07И пару недель назад у меня был разговор с человеком, который спросил: “Ну,
00:03:10как вы это определяете? Можно ведь просто увидеть, если процессор загружен на 100%, верно?”
00:03:14На самом деле нет, потому что как только ваша платформа становится успешной, майнеры крипты сразу же
00:03:20начинают делиться друг с другом скриптами, которые модифицируют то,
00:03:26как исполняются инструкции процессора, так что со стороны это выглядит как абсолютно безобидная активность, но при этом они
00:03:31используют максимально возможный объем ресурсов без вызова подозреаний в вашей системе.
00:03:37Так что это невероятно сложно, и требуются годы и множество уровней безопасности и
00:03:42наблюдаемости, чтобы сделать всё правильно. Другая проблема заключается в том, что когда у вас есть
00:03:48платформа для выполнения кода, вам нужно беспокоиться о том, что люди будут совершать DoS-атаки с ее помощью
00:03:54или использовать её для ботнетов и командных серверов. И мы просто не хотели переживать обо всех
00:04:00этих вещах с самого начала работы над Notion Workers. Мы хотели сосредоточиться на том, что любим делать больше всего —
00:04:07создавать продукт, которым нравится пользоваться нашим клиентам. Именно поэтому мы решили строить систему на базе Vercel Sandbox.
00:04:14Мы сразу увидели, что Vercel Sandbox — это очень надежная инфраструктура, которая решает
00:04:18большинство этих проблем прямо из коробки. Сейчас я покажу вам супербыстрое демо
00:04:25того, что представляют собой Notion Workers. У меня есть кастомный агент, и задача этого агента — сказать мне,
00:04:33проклят ли этот воркшоп. Я написал worker под названием Mercury Retrograde. Не знаю, знаком ли кто-то с этим,
00:04:40но когда Меркурий находится в ретроградном фазе, это обычно
00:04:47дурной знак. У этого worker есть один кастомный вызов инструмента, который использует найденный мной API, не делающий больше ничего,
00:04:54кроме ответа на вопрос, находится ли Меркурий в ретрограде. Я отправлю агенту промпт и спрошу: “Проклят ли my
00:05:01воркшоп?”, он подумает минуту, и затем, если интернет работает нормально,
00:05:14мы увидим, как он вызывает инструмент. Он использует мой worker ретроградного Меркурия, вызывая
00:05:22единственный инструмент, доступный в этом worker, чтобы выяснить, находится ли Меркурий в ретрограде, а затем
00:05:27агент ответит нам. Кажется, я подключил его к чему-то вроде GPT-54 nano. Так что обычно это намного
00:05:37быстрее. Думаю, дело в интернете, к сожалению. Если кто-то случайно проверял, находится ли Меркурий в
00:05:46ретрограде, я заранее знаю, что да. И, вероятно, именно поэтому так происходит. Я просто
00:05:53пропущу это. Мы вернемся к этому через минуту и посмотрим, получили ли мы в итоге ответ. Но думаю, ответ мы уже
00:05:59знаем. Итак, теперь вы представляете, что такое Notion Workers. Я покажу вам
00:06:05небольшой скрипт, который я написал, выполняющий основные задачи, необходимые для приема
00:06:13пользовательского кода, его деплоя, безопасного выполнения и передачи агенту информации об этом коде, чтобы он мог
00:06:20вызывать его инструменты. Завершилось ли это? О, да, вот. А, кажется,
00:06:28должно быть, я уронил API ретроградного Меркурия, потому что, судя по всему, API не ответил.
00:06:35В любом случае, придется где-нибудь открыть тикет. Отлично. У меня есть базовый скрипт. Я не жду,
00:06:40что вы будете полностью повторять за мной и писать код или что-то в этом роде. Я просто набросаю
00:06:44основные моменты, чтобы дать вам представление о некоторых проблемах, которые мы решили и которые вам придется решать при создании
00:06:49подобного продукта. Но если хотите, есть репозиторий make notion / vercel ship 2026 workers,
00:06:55содержащий весь код, который я буду запускать здесь.
00:07:01Супер. Позвольте мне открыть другие слайды.
00:07:08К чему мы стремимся? У нас есть потоковый чат-агент. Мы дадим ему возможность вызывать инструменты,
00:07:16определенные в кастомном пользовательском коде, которому мы не доверяем и содержание которого нам неизвестно.
00:07:22И мы построим это с помощью Vercel Sandbox и сервиса Blob-хранилища от Vercel.
00:07:26Сейчас я проведу быстрое демо, и надеюсь, мы всё же не прокляты.
00:07:31Вы можете просто отправить ему одно сообщение. Я напишу “Привет”, и, надеюсь, мы получим ответ.
00:07:36Это будет немного медленно, потому что, как вы увидите, в фоновом режиме происходит деплой.
00:07:40В этот раз вызвался инструмент. Вызвался инструмент под названием say hello, и он решил поприветствовать меня.
00:07:47Я отправлю ему обычное сообщение, просто спросив, сколько будет один плюс один, чтобы вы увидели,
00:07:52как он транслирует обычный ответ.
00:07:58Если вы использовали Vercel AI SDK, то здесь задействован стандартный цикл агента инструментов. Отлично, мы получили
00:08:04потоковый ответ. Можно также вызывать гораздо более сложные workers. У меня есть еще один пример, который я покажу.
00:08:14В этом примере мы говорим ему, что я нахожусь по адресу этого здания,
00:08:17у меня есть 90 минут, я хочу посмотреть какое-нибудь историческое место и не хочу идти пешком дольше 15 минут.
00:08:24Это наглядно иллюстрирует, почему такие решения иногда предпочтительнее MCP-серверов. В случае с MCP-
00:08:29сервером у вас есть куча разрозненных вызовов инструментов, которые можно сделать. И если вы пытаетесь сделать что-то
00:08:33сложное, вам нужно описать эти шаги агенту, и он будет выполнять шаг, тратить
00:08:38токены на размышления, затем делать следующий шаг. А этот вариант вызовет worker с очень большим и сложным
00:08:45алгоритмом поиска, который использует данные геолокации Нью-Йорка, время транзита и API планирования маршрутов.
00:08:52Он называется plan outing.
00:08:56И этот worker ответит предложенным историческим местом, которое мы можем посетить.
00:09:00И похоже, он нашел памятный знак “Дерево свободы”, мемориал MIPOW недалеко от
00:09:07Сити-Холл-Парка. Отлично. Давайте посмотрим, как это работает. Итак, что такое worker?
00:09:14Это код пользователя, который в данном случае просто определен в файле. Обычно он был бы определен
00:09:20вашими пользователями или на GitHub и разворачивался бы через CLI. Но у нас есть несколько примеров,
00:09:26закоммиченных в репозиторий. И каждый из этих workers должен предоставлять имя инструмента, описание инструмента, чтобы
00:09:34агент мог выбрать нужный инструмент, схему входных данных, чтобы агент знал, какие параметры ему нужно
00:09:38передавать при каждом вызове. И функцию исполнения, которая определяет, какой код запускается
00:09:44при вызове этого инструмента агентом. Давайте рассмотрим пример. Вот инструмент-приветствие, который вы видели
00:09:51ранее. Он очень простой. Этот worker представляет собой один JavaScript-модуль в файле index.ts. Он экспортирует один
00:09:59worker под названием say hello. Вы увидите, как работает этот процесс сборки, но в данном примере
00:10:04все ключи экспорта нашего модуля сопоставлены с названиями наших инструментов. Так что этот инструмент будет называться
00:10:09say hello. И у него есть простое описание, схема входных данных. В данном случае я использую Zod для определения
00:10:16схемы. А затем конвертирую ее в JSON-схему. Это тот формат, который ожидают эти агенты.
00:10:22И затем идет простая функция исполнения. Но вы можете создавать и гораздо более сложные варианты. Если бы я
00:10:27открыл процесс plan outing, вы увидели бы, что у него гораздо более длинное описание, рассказывающее агенту
00:10:34всё о том, как работает этот инструмент, когда его вызывать и что он делает. И скрипт для него гораздо, гораздо
00:10:39длиннее и сложнее. Я не буду разбирать его целиком. Я сам читал лишь небольшие его части,
00:10:45но работает он отлично. В таком мире мы сейчас живем. Итак, эти workers будут
00:10:54собираться и разворачиваться в Blob-хранилище Vercel. Затем мы каким-то образом узнаем о содержимом
00:10:59workers. И мы предоставим к ним доступ агенту. А затем эти инструменты будут
00:11:03безопасно выполняться в песочнице. Этап сборки мы пока пропустим. В Notion workers у нас есть
00:11:09облачный процесс деплоя и сборки. В данном же случае я просто предварительно собрал эти workers на диске. Так что у каждого
00:11:15worker есть tarball-архив, содержащий весь скомпилированный код TypeScript, зависимости и тому подобное.
00:11:22Это не самая интересная часть, поэтому я просто пропущу ее.
00:11:26Главный вопрос сегодняшнего дня: как нам перейти от пользовательского кода, который мы никогда не видели и которому не доверяем,
00:11:34к инструментам, доступным агенту и безопасно им исполняемым? Здесь есть две части.
00:11:39Первая часть: как только этот пользовательский код оказывается, скажем, в Blob-хранилище,
00:11:43как нам узнать, что именно в нем содержится? Ведь мы должны сообщить агенту еще до того,
00:11:48как он вызовет инструмент: каково имя инструмента, какова схема входных данных и каково описание
00:11:53инструмента. А второй вопрос: когда агент решает вызвать этот инструмент,
00:11:58как нам на самом деле выполнить его безопасно? Одна из вещей, которая мне очень нравится в нашем решении для
00:12:04Notion Workers SDK и которую мы применили и здесь, — это то, что код описывает сам себя. Чего мы
00:12:11не хотели при проектировании Notion Workers, так это того, чтобы пользователям приходилось писать TypeScript-
00:12:16код, определяющий инструмент, а затем говорить: “Так, теперь мне нужно создать статический манифест,
00:12:21описывающий мои workers”, по сути переписывая то же самое. Мы не хотели неудобного локального процесса сборки,
00:12:27при котором им приходилось бы запускать скрипт, делать анализ, компилировать на диск и затем деплоить. Мы хотели начать
00:12:34с того, что казалось оптимальным опытом разработчика: вы просто пишете инструмент, а дальше уже наша
00:12:40задача — решить сложную проблему извлечения информации из него.
00:12:46Это очень простая диаграмма, но прежде чем переходить к коду, я расскажу вам о том,
00:12:52как именно мы собираемся это решать. У нас есть скомпилированный код пользователя в Blob-хранилище.
00:12:59Мы используем этот код для создания песочницы. И когда мы запускаем команды в песочнице, код
00:13:04пользователя будет находиться в корне рабочей области. Мы импортируем написанный пользователем файл index.js.
00:13:11Это даст нам все имена экспортов, описания и схемы входных данных.
00:13:19Здесь всё становится немного странным, но это работает очень хорошо. Мы вызовем
00:13:23json.stringify для этого модуля, а затем выведем результат в stdout. Всё это происходит в
00:13:29песочнице, а затем наш скрипт деплоя считывает этот stdout, парсит его и фиксирует: “Отлично,
00:13:34теперь я знаю имена всех инструментов в этом worker, входные данные и описание”. В данном
00:13:40случае мы передадим эти данные напрямую агенту. Но в случае с Notion Workers, к примеру,
00:13:44это часть нашего пайплайна деплоя. Мы берем всю эту информацию и сохраняем в базу данных, чтобы
00:13:49при каждом запуске кастомного агента извлекать описания инструментов из
00:13:53базы данных. Имеет ли смысл то, как это работает, на данный момент? Хорошо. Давайте взглянем на скрипт
00:14:02деплоя. Мне нужно вернуться к моему первому коммиту. В этот скрипт встроены и деплой,
00:14:12и вызов агента. Он супер простой. В данном случае мы просто итерируемся по всем директориям
00:14:18и workers. Каждая из этих поддиректорий содержит код worker, как вы видели минуту назад.
00:14:23Наша задача — понять, как извлечь всю информацию об инструментах из каждого worker и заполнить
00:14:29этот объект tools. Затем он передается агенту с циклом вызова инструментов. Это часть AI SDK,
00:14:35созданного Vercel. Затем мы отправляем сообщение пользователя этому агенту, транслируем ответ и записываем
00:14:42его в stdout прямо из скрипта. Первое, с чем нам нужно разобраться — как просто загрузить
00:14:48исходный код. Эта часть довольно простая и быстрая. Вместо лайв-кодинга я буду просто
00:14:53перескакивать между фрагментами, чтобы вам не пришлось смотреть, как я печатаю. Обещаю, я умею писать
00:14:59код, но думаю, никто не хочет за этим наблюдать. Забегая немного вперед, у нас есть функция
00:15:06под названием upload source, к которой я перейду. Но как видите, я импортировал Vercel blob SDK. Здесь всё khá
00:15:13просто. Если посмотреть на функцию upload source, по сути мы просто вызываем функцию
00:15:20put. И указываем, что хотим сохранить бандл этого пользователя по пути имя_worker / bundle.tar.gzip.
00:15:29Мы будем передавать файл с диска в виде потока и сохраним его в Blob-хранилище. Как только это сделано,
00:15:35мы можем создавать песочницы из этого blob-объекта. Таким образом, задача загрузки решена.
00:15:43Следующее, что нам нужно сделать, — взять упакованный исходный код и создать
00:15:49на его основе песочницу. Для этого я импортировал Vercel sandbox SDK. Ниже есть еще одна вспомогательная
00:15:56функция под названием create sandbox. Я пропущу пару моментов. Но по сути,
00:16:03когда этот объект находится в Blob-хранилище, мы используем предварительно подписанные URL, которые
00:16:10передаем в сервис песочниц. Чтобы сервис песочниц мог (кажется, здесь установлен 10-минутный
00:16:16срок действия) подтянуть этот blob из хранилища и использовать его для наполнения песочницы. Это одна из
00:16:23фич, за которые мне очень нравится Vercel sandbox: он умеет работать с такими tarball-архивами. Легко
00:16:28собрать tarball с кодом пользователя, передать сервису песочниц для разворачивания,
00:16:35он забирает его из хранилища или по указанному URL и автоматически распаковывает в корень
00:16:40рабочей области. И все файлы сразу готовы к работе. Все это делается простым вызовом
00:16:46sandbox.create. Мы еще не дошли до самой сложной части, но скоро будем там.
00:16:52Еще один момент: в этом примере ради краткости я создаю свежие песочницы каждый раз,
00:16:57когда мы их запускаем. В реальности вам стоит использовать снапшоты, чтобы не создавать их постоянно — например,
00:17:02при каждом вызове инструмента заново деплоить песочницу или пересылать ее из Vercel blob
00:17:09storage или любого другого хранилища. В платформу встроен механизм кэширования.
00:17:15Здесь я просто пропустил его для простоты.
00:17:19Итак, мы создали песочницу, и вот теперь начинается самое интересное.
00:17:23Следующее, что нам нужно сделать, — извлечь информацию об инструментах,
00:17:31определенных в этом worker, из только что созданной песочницы.
00:17:36Я сделаю небольшую паузу. Тут есть пара советов, разбросанных по докладу.
00:17:40В целом, если вы строите продакшен-сервис такого уровня,
00:17:44всегда старайтесь прикладывать максимум усилий, чтобы останавливать ваши песочницы. В данном случае вы увидите,
00:17:51что делает extract tools, но завершив работу, мы явно удаляем песочницу.
00:17:56В реальном проекте вы также захотите ставить асинхронную задачу на очистку
00:18:00в очередь или делать что-то подобное. Вы не хотите, чтобы эти песочницы висели бесконечно.
00:18:06Итак, давайте посмотрим, что делает функция extract tools. У нас есть песочница, и мне очень нравятся эти
00:18:15примеры, потому что решение выглядит комично простым, но при этом работает невероятно хорошо. Мы запускаем
00:18:23команду node с помощью бинарника Node прямо в песочнице и в скрипте импортируем модуль,
00:18:30написанный пользователем, то есть index.js. Мы сериализуем объект в строку и затем вызываем
00:18:36console.log. Важно помнить, что песочницы — это не веб-сервер, куда можно
00:18:42отправить запрос и получить ответ. Весь ввод и вывод происходит через выполнение команды, а затем
00:18:48вы можете отправить ответ сервису, который опрашиваете, или, как здесь, просто сделать
00:18:52вывод в stdout. Несколько советов. Обычно не стоит слепо делать console.log и доверять
00:19:00всем данным из песочницы. Там могут быть сторонние пакеты, установленные пользователем, или
00:19:07другой код, который параллельно пишет лог-данные в поток stdout.
00:19:12Поэтому полезно оборачивать вывод в специальный тег, который потом легко распарсить.
00:19:18Что-то вроде XML-тега. Я просто не стал делать это в примере.
00:19:23Также необходимо ограничивать размер считываемых логов. В этом примере я просто
00:19:29собираю весь вывод в поток. В боевом приложении так делать нельзя, потому что
00:19:34вывод может занять гигабайт, и вы получите ошибку нехватки памяти (OOM) на серверах.
00:19:39В API песочницы есть команды для потоковой передачи логов. Именно так мы и поступаем
00:19:45в Notion Workers: считываем поток до тех пор, пока не увидим токен начала вывода,
00:19:53который нас интересует. Затем начинаем буферизовать описание — то, что мы намеренно залогировали.
00:19:58И прекращаем обработку потока, как только достигаем закрывающего тега. Так что, если в песочнице происходит
00:20:04что-то странное и логируется гигантский объем данных,
00:20:08они не будут накапливаться в памяти. Мы можем просто отбросить их и ждать нужную нам информацию.
00:20:14Запустив команду, мы дожидаемся ее завершения и забираем данные из stdout.
00:20:22И если вы раньше использовали, например, Zod, это должно выглядеть знакомо. Мы просто парсим эту строку как JSON.
00:20:27Затем у нас есть тип Zod,
00:20:31который проверяет, соответствует ли структура нужной нам форме.
00:20:34Очень важно не доверять этим данным слепо, потому что это ненадежный пользовательский код. Вы понятия не имеете,
00:20:39что именно пользователи могут логировать. Поэтому нужно убедиться,
00:20:43что вы валидируете размер и общую структуру. В данном случае мы ожидаем
00:20:50объект Record, где ключами (строкой) будут названия каждого из наших инструментов.
00:20:57А значениями будут описание и схема входных данных, то есть
00:21:02схема JSON. Вы заметите, что функции execute здесь нет. Она просто
00:21:06не возвращается, и это здорово, так как JSON.stringify пропускает несериализуемые
00:21:11данные. Так что она просто игнорируется. В итоге мы получаем только описание и схему входных данных.
00:21:18Возвращаясь к коду: у нас есть worker tools —
00:21:23запись со всеми инструментами, предоставленными этим воркером.
00:21:28Затем мы приводим эти данные к структуре, которую ожидает
00:21:36AI SDK. И нам нужно прикрепить функцию исполнения к каждому из этих инструментов.
00:21:42Разумеется, мы не получили функцию исполнения, когда просто выводили логи в standard out.
00:21:46И теперь возникает вопрос: имея описание инструмента, как передать
00:21:51функцию, которую SDK сможет вызывать каждый раз, когда потребуется обратиться к воркеру или инструменту,
00:21:58предоставленному этим воркером? Для этого у меня есть обертка execute tool. Давайте посмотрим,
00:22:04как она работает. Код выглядит знакомо: вызывается та же функция create sandbox.
00:22:10То есть создается чистая песочница. Даже если вы используете кэширование, в песочницах Vercel есть функция
00:22:16персистентности: при каждой приостановке или остановке песочницы кэшируется состояние диска.
00:22:23Для такой функциональности это не нужно. Вам нужен снимок исходного состояния,
00:22:28содержащий весь пользовательский код. Но в целом после этого нужно гарантировать, что каждый
00:22:33раз при запуске инструмента используется полностью чистый экземпляр. Благодаря этому, если при одном
00:22:38вызове что-то пойдет не так, это не загрязнит среду для следующего запуска.
00:22:43Итак, мы создаем новую песочницу и запускаем в ней еще один скрипт Node. Логика очень похожа,
00:22:49но есть небольшие отличия. Мы импортируем модуль, написанный пользователем. Извлекаем
00:22:56нужный инструмент из этого модуля по имени, полученному при создании обертки execute tool.
00:23:03Вызываем для него функцию execute и передаем в нее входные данные,
00:23:09предоставленные моделью. В данном случае я просто указываю тип unknown. Это вполне безопасно,
00:23:19потому что мы предоставили... Разве я не сделал это здесь? Похоже, я пропустил этот шаг в примере. Но
00:23:27обычно делается так... А, нет, сделал. Вернемся чуть выше. Да. Когда мы
00:23:34подготавливаем инструменты для отправки агенту, мы берем схему JSON и преобразуем ее
00:23:39обратно в тип Zod. Так что нам не нужно парсить данные вручную. AI SDK и код агента цикла инструментов
00:23:46будут сами валидировать входящие данные от агента при каждом вызове.
00:23:52Поэтому мы можем гарантировать, что полученное значение соответствует ожиданиям. Мы передаем его
00:23:56в функцию execute. Когда эта асинхронная функция завершается, мы
00:24:03сериализуем результат в строку, выводим в standard out, а затем ждем... Подробнее об этом
00:24:10я расскажу через секунду. Мы ждем завершения команды и снова
00:24:14удаляем песочницу, когда она больше не нужна. После этого парсим JSON с возвращаемым
00:24:21значением функции выполнения, написанной пользователем, и отправляем его обратно агенту
00:24:26цикла инструментов. Общий процесс выглядит так: агент говорит «Я хочу вызвать инструмент plan outing»,
00:24:35который мы видели ранее и который использует API транспорта. В итоге вызывается эта функция
00:24:40с входными данными, выбранными агентом. Мы создаем песочницу из блоба с скомпилированным
00:24:48кодом пользователя, который загрузили ранее. Затем выполняем команду в песочнице, вызывая
00:24:55функцию execute пользователя. Ждем возврата значения, выводим его в
00:25:00standard out, парсим и отправляем обратно агенту. А агент продолжат цикл инструментов:
00:25:05выполнит следующий вызов или ответит пользователю. Несколько полезных советов: те же
00:25:13правила проверки вывода применяются и в продакшене. Опять же, нужно убедиться, что пользователи не смогут
00:25:19вернуть объект размером в пару гигабайт, парсинг которого заблокирует систему. Также стоит
00:25:28предоставить SDK. В этом примере мы этого не делаем, но в Notion Workers SDK
00:25:35типы настроены так, чтобы возвращаемое значение функции execute обязательно сериализовалось в JSON.
00:25:41Распространенная грабля для пользователей: если разрешить функции
00:25:47возвращать что угодно, они начнут возвращать данные, которые нельзя сериализовать в JSON для передачи
00:25:52по сети через standard out и последующего парсинга. Они запутаются и не поймут, почему
00:25:57воркер не работает. И еще одна небольшая история из опыта разработки Notion Workers:
00:26:05всегда внимательно следите за тем, чтобы запущенный процесс Node
00:26:11обязательно завершался при любых условиях. У нас в Notion был баг, когда код пользователя исполнялся,
00:26:18доходил до конца, но по какой-то причине процесс Node не завершался. Он просто висел до конца
00:26:25жизненного цикла песочницы, то есть около пяти минут. Это было не катастрофично,
00:26:30но растрачивало ресурсы. Оказалось (я совершенно забыл,
00:26:38что это особенность среды Node), что пользователи запускали код, создающий таймеры через
00:26:44setInterval или setTimeout. И особенно интервалы (или зависшие промисы) вызывают такую проблему.
00:26:52Если скрипт выполнился, но остались активные интервалы, процесс Node
00:26:58никогда не завершится сам по себе, пока не истекут таймеры. Так что при наличии
00:27:03интервала он будет работать вечно, пока песочница не закроется. Поэтому нужно гарантировать,
00:27:08что по завершении вызова нужной функции вы явно даете команду
00:27:14процессу завершить работу. Тогда песочница остановится, и процесс завершит выполнение.
00:27:23Вот и весь рабочий процесс. Я запущу его еще раз с включенной
00:27:29отладкой, чтобы вы могли увидеть происходящее.
00:27:42Сначала деплоим воркер departures. Я разберу это через секунду. Мы развертываем
00:27:48воркер departures, загружая его бандл. Создаем песочницу для извлечения
00:27:54информации о воркере. Извлекли инструменты: вот что выводится при логировании
00:28:01содержимого модулей в standard out. Мы получаем ключи каждого инструмента, описание и схему ввода.
00:28:08Делаем то же самое для воркера simple greeter. Затем передаем всё агенту,
00:28:13чтобы он мог вызывать инструменты и отвечать пользователю. Вот и всё. Это довольно простой
00:28:19пример того, как взять ненадежный пользовательский код, сохранить его, безопасно изучить,
00:28:26чтобы поместить в надежное хранилище или передать агентам, а затем дать агентам возможность безопасно
00:28:31выполнить этот код. У нас осталось около 10 минут. Если есть вопросы по
00:28:39Notion Workers, работе с песочницами Vercel или выполнению стороннего кода,
00:28:46буду рад ответить. Спасибо.
00:28:56Да. И если вы захотите обсудить платформу разработки Notion в целом,
00:29:01можете обратиться ко мне или к моей коллеге Эм-Джей. Она продакт-менеджер платформы разработки в Notion.
00:29:08Здравствуйте. Вопрос по эксплуатации: как вы ограничиваете пользователей? Ведь они могут делать что угодно.
00:29:15Да. Разные пользователи могут написать похожий инструмент.
00:29:20Разные пользователи что? Напишут похожий инструмент. Например, для планирования поездки в Нью-Йорк.
00:29:24Да. У 10 пользователей может быть 10 разных вариантов кода,
00:29:29делающих одно и то же. Вы как-то блокируете это или всё зависит от агента?
00:29:33Нет. Мы не запрещаем: если несколько пользователей делают одно и то же, мы это позволяем.
00:29:37Это отчасти продуктовый вопрос. Если речь идет о нескольких пользователях в одной организации,
00:29:42хочется убедиться, что... Так что это и операционный, и продуктовый вопрос.
00:29:46Нужно убедиться, что у вас есть хорошие механизмы совместного доступа. Чтобы я мог
00:29:50поискать и проверить: «А есть ли уже воркер для этой задачи?» И чтобы они не
00:29:55писали всё заново. Но в целом на уровне платформы мы ничего не делаем, чтобы... Маловероятно, что
00:30:02пользователи развернут идентичный код. И пытаться дедуплицировать это просто не стоит.
00:30:19Так, отлично. Кажется, у нас есть еще пара вопросов. Не знаю, у кого микрофон.
00:30:24Я не услышал. Ох, да, прошу прощения.
00:30:29Я думал, звук пошел в наушники.
00:30:33Да, вопрос был следующий: если у вас много пользователей разворачивают один и тот же код,
00:30:40делаем ли мы что-то для оптимизации этого? Нет, не делаем. Это скорее продуктовый вопрос.
00:30:45Мы хотим, чтобы пользователи не делали одну и ту же работу. Поэтому нужны удобные инструменты шеринга,
00:30:50над которыми мы сейчас и работаем для Notion Workers. Но операционно на уровне платформы,
00:30:54если люди задеплоят один код 50 раз, нам всё равно.
00:30:56Бывают ли у вас проблемы с таймаутом воркеров из-за того, что песочница
00:31:04просто выполняет задачу и слишком рано завершается?
00:31:07В чем именно состоял вопрос про таймауты?
00:31:10Есть ли у вас проблемы с таймаутами между Vercel, другими продуктами и воркфлоу,
00:31:15например, функциями и вашей песочницей? Или всё работает нормально?
00:31:20Да, у нас с этим проблем не было. Платформа пока показывает себя
00:31:25очень стабильно. Я не рекламирую, абсолютно серьезно. Вполне отлично работает.
00:31:32Мы гораздо чаще сталкиваемся с тем, что пользователи случайно делают что-то не так. Так что
00:31:36со временем наша задача — убрать такие «грабли» и сделать платформу всё более удобной
00:31:42как для разработчиков, так и для остальных. Супер, отлично. Я хотел спросить про...
00:31:48Думаю, про момент, когда вы переводите исходный код пользователя в строку. Полагаю, вы пытаетесь убедиться,
00:31:52безопасен ли он и можно ли его запустить? Хотел спросить чуть подробнее.
00:31:57Вы сказали, что сериализуете код в строку и получаете теги воркера для нужных входных данных
00:32:04пользовательского кода, а затем описание инструмента. Это для Notion,
00:32:10чтобы ваш агент мог исполнить код? Я заметил, что вы уже запускаете исполнительный код
00:32:15или функцию-исполнитель пользователя. И мне стало любопытно, почему вам важен — ну, или нужен —
00:32:21этот дополнительный шаг для получения входных данных и описания самого инструмента.
00:32:24Хороший вопрос. В реальном продукте это более очевидно, но здесь я упростил.
00:32:29Вопрос, по сути, в том, зачем я выполняю пользовательский код один раз для получения информации о
00:32:34воркере, а затем снова, когда вызывается код инструмента? Причина в том, что до того, как агент
00:32:39сможет вызвать инструмент или вообще узнать о его существовании, мы должны узнать,
00:32:45что находится внутри, и передать это агенту через SDK. Например, в Notion
00:32:50Workers, когда вы запускаете `NTN Workers Deploy`, мы проходим через пайплайн сборки,
00:32:55песочница, выполняющая сборку, берет tarball и сохраняет его в хранилище. Затем
00:33:02мы в новом воркере извлекаем названия инструментов, описания и
00:33:09схемы, сохраняя их в DynamoDB. Таким образом, после этого мы больше никогда не запускаем
00:33:14песочницы, пока инструмент реально не будет вызван. Вы упомянули систему tarball. Кажется, именно это
00:33:21мне интереснее всего. Как именно устроена дистрибуция с использованием tarball?
00:33:26Есть ли у вас открытый маркетплейс прямо сейчас или как устроен процесс распространения?
00:33:31А, в смысле, что именно входит в сам tarball? Да, да.
00:33:33У нас нет... То есть, с точки зрения механики того, что попадает в tarball:
00:33:38мы сейчас используем просто esbuild. Реальный пайплайн деплоя работает так: вы запускаете
00:33:47`NTN Workers Deploy`, вызываете эндпоинт API, ваш компьютер получает подписи URL, упаковывает
00:33:52весь ваш исходный код, мы создаем tarball с исходным кодом, запускаем процесс сборки с esbuild в
00:33:58этой песочнице, и результат сохраняется обратно в blob-хранилище, откуда мы и запускаем
00:34:04итоговый процесс. Полноценного маркетплейса для воркеров у нас пока нет. Сначала мы работаем над шерингом
00:34:11внутри рабочего пространства Notion, но в планах точно есть создание маркетплейса воркеров в будущем.
00:34:18Пока же вы можете просто распространять их через GitHub, и это отлично работает. Люди так и делают.
00:34:23Да, просто публикуете репозиторий, кто-то может его склонировать, выполнить `NTN Workers Deploy` и использовать.
00:34:29Да.
00:34:34Кажется, там в конце зала еще вопрос.
00:34:37Да, у меня вопрос по поводу биллинга.
00:34:40По поводу биллинга?
00:34:40Да, я тут посмотрел... конечно, не раскрывайте секретов, но
00:34:44я быстро поискал в Google. Похоже, кастомные агенты работают по кредитной системе. Я
00:34:48полагаю, это связано с потреблением ресурсов.
00:34:51Да-да.
00:34:51Как это работает вместе с платформой Vercel, если в общих чертах?
00:34:54Итак, вопрос в том, как устроена оплата с учетом платформы Vercel?
00:35:02Одно из преимуществ воркеров в том, что многие команды смогли перейти от
00:35:08огромных наборов инструкций с MCP-серверами, которые они давали агентам. И при каждом вызове задачи
00:35:16этот агент тратит кучу токенов на «рассуждения», по сути, делая одно и то же снова и снова.
00:35:21И если вы можете взять эти повторяющиеся задачи агента и задеплоить их как воркер,
00:35:33вы всё еще платите за время выполнения воркеров, но это выходит намного дешевле ИИ-токенов.
00:35:40Так что при запуске кастомного агента он рассуждает, вызывает инструмент и снова рассуждает.
00:35:48С вас списываются средства за токены кастомного агента, которые он потратил до вызова инструмента.
00:35:53А во время работы инструмента тарификация идет по совершенно другому, значительно более низкому тарифу списывания кредитов Notion AI.
00:36:01И затем снова обычные ИИ-кредиты, когда агент окончательно выдает ответ.
00:36:05Таким образом, с помощью кастомных агентов Notion вы можете сильно снизить расходы на повторяющиеся задачи.
00:36:16И это тоже правда. Не обязательно использовать именно Notion AI — мы тут показываем вызовы инструментов с интеграцией в Notion AI,
00:36:24но воркеры умеют делать и стороннюю синхронизацию в Notion. Для этого вообще не нужны ИИ-функции.
00:36:31Так что это не только про ИИ. Люди используют их просто для синхронизации.
00:36:36У меня есть воркеры, которые синхронизируют ленту Letterboxd с Notion. Для меня лично это самый важный воркер.
00:36:46Хорошо. Что-нибудь еще?
00:36:52Есть еще один вопрос?
00:37:06В смысле, предоставлять доступ к кастомным агентам Notion через ваш собственный интерфейс?
00:37:13Сейчас такой возможности нет.
00:37:16Извините. Спасибо, ЭмДжей. Вопрос был: если у вас есть свои воркеры и кастомные агенты,
00:37:22можно ли предоставить к ним доступ через ваше собственное приложение для ваших пользователей?
00:37:28На сегодняшний день такой возможности нет. У нас есть альфа-версия API кастомных агентов, где это
00:37:35можно было бы реализовать. Если настроить кастомных агентов и воркеров в рабочем пространстве,
00:37:40можно вызывать их через API и получать потоковый ответ. Так что это потенциально возможно.
00:37:46Не думаю, что сейчас этим многие пользуются. Это пока очень ранняя, пусть и публичная, но альфа-фича.
00:37:57Другие вопросы?
00:38:00Что ж, отлично. Спасибо всем за то, что выделили время
00:38:04и пришли послушать сегодня. И да, если у вас есть вопросы по Notion, Notion Workers
00:38:08или Vercel Sandbox — подходите ко мне или ЭмДжей. Спасибо.