Skip to content

Vaynerov Technologies

Мы не просто разрабатываем — мы создаём каждую строку кода и пиксель.

Все статьиИстории сборки

ИИ-архитектор с честными ценами

Опишите продукт абзацем, и Blueprint нарисует вам схему системы, план из трёх фаз, состав команды и цену. Всё в этом списке сгенерировано, кроме единственного пункта, который волнует людей больше всего, — и в этом весь замысел.

Edward AmirainОснователь Vaynerov Technologies
Опубликовано 8 мин чтения
На этой странице
  1. Модель предлагает, движок считает цену
  2. Структурированный вывод и где на самом деле живут границы
  3. Всё, что вокруг модели
  4. Демо-режим, который не врёт
  5. Рисуем схему вообще без canvas
  6. Предложение пересчитывается заново, на сервере

Языковая модель, которая придумывает вашу цену, — это не функция. Это обязательство с красивым шрифтом. Кто-то читает число на нашем сайте, делает скриншот и через две недели приходит с ожиданием, что мы его выполним, — и будет прав. Число стояло на нашей странице.

Поэтому, когда я строил Vaynerov Project Blueprint — вставьте идею, получите архитектуру, — я записал одно правило раньше, чем написал первую строку промпта, и всё остальное в этой функции вытекает из него: модель никогда ничего не оценивает. Она только предлагает выбор, да и он проходит очистку прежде, чем его увидит движок.

Дальше — история сборки ИИ-функции, в которой вся интересная инженерия находится почти целиком вокруг модели.

8–12
узлов минимум в 4 слоях
3
фазы, ровно — проверено дважды
$150–250
диапазон ставки за юнит, из калькулятора
5 / 15 min
генераций на один IP
Линия, которую я отказываюсь двигать. Всё слева — предложение; всё справа — арифметика, которую /pricing выдал бы для того же набора выборов.

Модель предлагает, движок считает цену

Калькулятор за /pricing старше Blueprint — он приехал вместе с самим сайтом, а в июне обзавёлся таблицей правил. Он примерно настолько неэффектен, насколько софт вообще бывает: каждая платформа, функция, ИИ-возможность, уровень дизайна и тир интеграций несёт базовое количество юнитов; они суммируются; сумма умножается на сложность продукта, затем на множитель QA, затем на множитель запуска; скорректированный итог встречается с диапазоном ставки за юнит и превращается в вилку в долларах.

ПлатформаБазовые юнитыБазовые недели
Веб-приложение508
iOS406
Android406
Бэкенд / API305
Админ-панель254

Пять правил движка по умолчанию. Функции считаются так же: авторизация — 8 юнитов, платежи — 15, реальное время — 10, админ-CMS — 20; ИИ с поиском по документам — 25, агенты — 30.

Сроки — та же арифметика. Считается, что платформы идут параллельно, поэтому они дают максимум своих базовых недель, а не сумму; всё, что не платформа, конвертируется по пятнадцать юнитов в неделю; затем две недели буфера, потому что две недели есть всегда.

Blueprint вызывает ровно этот движок, ровно с этими правилами, загруженными из той же таблицы calculator_rules, которую читает страница цен, — а если таблица отсутствует или недоступна, оба деградируют к одним и тем же зашитым значениям по умолчанию, а не к догадке. Модель вносит выбор: платформу, тип продукта, список функций. Затем, до всякого расчёта, запускается sanitizeSelections. Неизвестные идентификаторы отбрасываются, а не толкуются великодушно. Обязательные поля с единственным выбором, вернувшиеся пустыми, заполняются значениями по умолчанию.

Часть, которой я тихо доволен: enum'ы JSON-схемы, по которой генерирует модель, каталог опций, напечатанный в промпте, и allowlist санитайзера выведены из одного и того же объекта правил. Добавьте функцию в калькулятор — она появится во всех трёх. Разъехаться они не могут, потому что синхронизировать нечего.

Структурированный вывод и где на самом деле живут границы

Blueprint работает на Claude Opus 4.8 (claude-opus-4-8) со структурированным выводом: JSON-схема прикладывается к конфигурации вывода запроса вместо старой привычки объявить фиктивный инструмент и читать его аргументы. Ответ приходит в JSON, потому что декодер был ограничен и не мог выдать ничего другого, а не потому, что мы вежливо попросили и обернули парсер в try.

Но у этого ограничения есть форма, и это не форма библиотеки валидации. Схема, которая ведёт генерацию, обязана оставаться простой: наша, написанная руками, ставит additionalProperties: false, перечисляет каждое свойство как обязательное, выражает необязательные поля через anyOf: [{ type: "string" }, { type: "null" }] и нигде не содержит minItems, maxItems, minimum или maximum.

{
  "type": "object",
  "additionalProperties": false,
  "required": ["id", "label", "lane", "tech", "note"],
  "properties": {
    "id": { "type": "string" },
    "label": { "type": "string" },
    "lane": { "enum": ["client", "edge", "services", "data", "infra"] },
    "tech": { "type": "array", "items": { "type": "string" } },
    "note": { "anyOf": [{ "type": "string" }, { "type": "null" }] }
  }
}

Один узел схемы графа. Необязательность выражена через anyOf с null, потому что в грамматике нет понятия отсутствующего обязательного ключа; количества и длины отсутствуют намеренно.

Жёсткие границы живут слоем ниже, в zod, уже после разбора: от трёх до четырнадцати узлов, не больше тридцати двух связей, массив фаз длиной ровно три, от трёх до шести ролей в команде, от двух до четырёх рисков, число недель от одной до сорока и идея длиной от десяти до тысячи символов. То есть схема гарантирует форму, zod гарантирует вменяемость, а промпт между ними просит об эстетической цели: восемь–двенадцать узлов минимум в четырёх слоях, хотя бы один инфраструктурный, восемь–шестнадцать связей, фазы по две–десять недель с двумя–четырьмя результатами в каждой. Единственный пример в промпте — намеренно маленькая система из шести узлов, потому что пример работает как гравитационная яма, и я предпочитаю, чтобы она тянула вниз.

Системный промпт несёт ещё и наш стоп-лист по стилю: два маркетинговых слова, которых на этом сайте не бывает, названы модели и запрещены. Сложной частью промпта это никогда не было, но проверяю я это первым делом.

Всё, что вокруг модели

Запрос намеренно имеет форму, удобную для кеша. Кеширование промпта — это совпадение по префиксу, то есть оно выживает, только если ведущие байты каждый раз идентичны, — поэтому системный промпт, каталог опций и пример стабильны побайтно и упорядочены детерминированно, а всё изменчивое (локаль посетителя, его идея) едет в пользовательском сообщении, где ему и место. Недетерминированная сортировка каталога опций тихо стоила бы нам кеша на каждом запросе, и при этом ничто не выглядело бы сломанным.

Идея посетителя оборачивается в теги <idea> с инструкцией, что её содержимое — данные, а не команды. Это не решённая проблема, и делать вид, что решённая, я не стану. Это заметное снижение доли лёгких атак на поверхности, где худший исход — глупая схема, а цена, то есть единственное, ради чего стоило бы атаковать, из текста недостижима в принципе.

Лучшая защита от инъекции в промпт — не хитрый промпт. Это отказ пропускать через промпт что-либо ценное.

На стороне ответа мы смотрим stop_reason раньше, чем на содержимое. Отказ становится кодом refused; упор в потолок 16,000 токенов — кодом truncated; каждый внутренний класс ошибки отображается в маленький безопасный для клиента код, а сырой текст ошибки никогда не покидает сервер: сообщение вышестоящего провайдера — деталь нашей инфраструктуры, а не чтение для посетителя. Всё стримится, но лишь затем, чтобы держать соединение живым в пределах двухминутного максимума; финальное сообщение мы берём и валидируем целиком.

Затем sanitizeBlueprint, снисходительный по замыслу, потому что цель — граф, который можно нарисовать, а не безупречный граф. Он обрезает всё сверх потолка в четырнадцать узлов, схлопывает дубликаты id, убирает петли на себя и связи, ведущие в несуществующие узлы, вычищает ссылки фаз на узлы, которых больше нет, и приписывает осиротевшие узлы к объявленной фазе, чтобы ничего не висело в воздухе. Он возвращает null, только когда результат структурно непригоден. Цикла повторов в v1 нет — это ограничение я выбрал, а не обнаружил: повтор удвоил бы задержку в худшем случае ради спасения вывода, который мы не сумели описать заранее.

Транспорт — server-sent events, и маршрут всегда отвечает **HTTP 200 с text/event-stream**: отказы приезжают событием error внутри потока, так что клиент в любом случае потребляет ровно один протокол, и второй ветки кода, которой можно протухнуть, у него просто нет. Четыре кадра статуса срабатывают на настоящих вехах, а не по таймеру: очередь, черновик, проверка, оценка. Читалка SSE на клиенте написана руками поверх fetch и ReadableStream, с разбиением по пустым строкам, потому что браузерный EventSource не умеет POST.

Демо-режим, который не врёт

Когда ключ API не настроен, Blueprint всё равно работает. Сервер сообщает, что генерация недоступна, клиент вообще не вызывает маршрут, а четыре написанных вручную эталонных чертежа — SaaS-продукт, маркетплейс, инструмент поддержки, IoT-платформа — подбираются по ключевым словам из того, что вы написали. Прогресс инсценирован, чтобы страница всё-таки казалась думающей.

И вот что здесь важно: цены в демо настоящие. Они не взяты из файла с эталонами. Они проходят через calculateEstimate с теми же правилами по умолчанию, поэтому числа, которые посетитель видит при выключенной модели, — это числа, которые /pricing выдал бы для той же конфигурации. Демо, показывающее ненастоящие деньги, учит посетителя, что и деньги у нас ненастоящие.

Средства против злоупотреблений дешёвые и слегка злые. Есть honeypot-поле, припаркованное далеко за левым краем вьюпорта: бот, который его заполнит, получит совершенно убедительный успех — готовый чертёж, корректные цены, анимированную схему — без единого потраченного нами токена, а если он потом запросит коммерческое предложение, ему выдадут правдоподобный токен, который отдаёт 404. Живой трафик ограничен пятью генерациями на адрес за пятнадцать минут; считает это атомарная функция фиксированного окна в Postgres, а если база недоступна — резервная карта в памяти.

Рисуем схему вообще без canvas

Вывод Blueprint — схема системы, и после долгого сезона WebGL мне было приятно собрать эту на обычном SVG. Ни canvas, ни three.js, ни прохода измерений. Раскладка — чистый модуль без React и без доступа к DOM: пять фиксированных слоёв (клиент, край, сервисы, данные, инфраструктура), узлы 148 на 54 единицы, колонки шириной 176, минимум четыре колонки.

Эти три константы — доказательство, а не предпочтение. Ширина колонки больше ширины узла, узлы сортируются по фазе, затем по исходному порядку и равномерно распределяются по доступным колонкам, поэтому два узла не могут перекрыться ни при каком их количестве вплоть до потолка: прохода разрешения коллизий нет, потому что коллизии невозможны. Каждый баг раскладки, который я когда-либо выпускал, жил в проходе разрешения коллизий.

Связи — кубические кривые Безье. Соединения внутри одного слоя выгибаются над ним на 85% высоты узла, чтобы никогда не проходить сквозь коробки между ними; межслойные оттягиваются минимум на 26 единиц, пропорционально пройденному расстоянию по вертикали. Три типа связей различаются штрихом, а не цветом — синхронные сплошные, асинхронные длинным пунктиром, поток данных мелкой точкой, — и к ним есть легенда, потому что цвет сам по себе доходит не до всех.

Пять слоёв, связи с кодировкой штрихом, жёсткая гарантия отсутствия перекрытий из арифметики колонок. Цвет слоя — украшение; данные несёт штрих.

Несколько решений помельче, которые я принял бы снова. Прозрачность несёт смысл: узел, отфильтрованный по фазе, сидит на 0.14, не-сосед наведённого узла — на 0.22, узел вне наведённой фазы — на 0.30: три градации не сейчас. Ширина чипов с технологиями считается арифметически по числу символов, чипов не больше двух, обрезка на тринадцати символах, потому что в SVG нет переноса, и текст, вылезший за коробку, просто едет дальше. Узлы — настоящие кнопки для клавиатуры, с Enter и пробелом, а импульсы, бегущие по связям, отключаются полностью, если посетитель попросил уменьшить движение; полоса прогресса тоже меняет неопределённое сканирование на статичную пропорциональную заливку.

Предложение пересчитывается заново, на сервере

Когда посетителю нравится увиденное и он просит настоящее коммерческое предложение, браузер отправляет выборы. Числа он тоже отправляет, и сервер их полностью игнорирует. Honeypot, затем валидация схемы, затем рейт-лимит, затем повторная очистка выборов, затем повторная загрузка правил, затем повторный расчёт оценки — на сервере, с нуля, — и только этот результат попадает в запись предложения. Комментарий над этим кодом длиной в четыре слова и делает своё дело: никогда не верь клиентским числам.

Что возвращает функцию туда, откуда она началась. Схема, которую выдаёт вам Blueprint, — предложение: набросок одной разумной формы, которую мог бы принять ваш продукт, а с наброском можно спорить. Ровно для этого и нужен первый разговор со студией. Цена — не набросок. Она вышла из того же движка и той же таблицы правил, что и любое другое число на этом сайте, и останется тем же числом, когда вы сделаете скриншот.