ИИ-архитектор с честными ценами
Опишите продукт абзацем, и Blueprint нарисует вам схему системы, план из трёх фаз, состав команды и цену. Всё в этом списке сгенерировано, кроме единственного пункта, который волнует людей больше всего, — и в этом весь замысел.
На этой странице
Языковая модель, которая придумывает вашу цену, — это не функция. Это обязательство с красивым шрифтом. Кто-то читает число на нашем сайте, делает скриншот и через две недели приходит с ожиданием, что мы его выполним, — и будет прав. Число стояло на нашей странице.
Поэтому, когда я строил Vaynerov Project Blueprint — вставьте идею, получите архитектуру, — я записал одно правило раньше, чем написал первую строку промпта, и всё остальное в этой функции вытекает из него: модель никогда ничего не оценивает. Она только предлагает выбор, да и он проходит очистку прежде, чем его увидит движок.
Дальше — история сборки ИИ-функции, в которой вся интересная инженерия находится почти целиком вокруг модели.
- 8–12
- узлов минимум в 4 слоях
- 3
- фазы, ровно — проверено дважды
- $150–250
- диапазон ставки за юнит, из калькулятора
- 5 / 15 min
- генераций на один IP
Модель предлагает, движок считает цену
Калькулятор за /pricing старше Blueprint — он приехал вместе с самим сайтом, а в июне обзавёлся таблицей правил. Он примерно настолько неэффектен, насколько софт вообще бывает: каждая платформа, функция, ИИ-возможность, уровень дизайна и тир интеграций несёт базовое количество юнитов; они суммируются; сумма умножается на сложность продукта, затем на множитель QA, затем на множитель запуска; скорректированный итог встречается с диапазоном ставки за юнит и превращается в вилку в долларах.
| Платформа | Базовые юниты | Базовые недели |
|---|---|---|
| Веб-приложение | 50 | 8 |
| iOS | 40 | 6 |
| Android | 40 | 6 |
| Бэкенд / API | 30 | 5 |
| Админ-панель | 25 | 4 |
Пять правил движка по умолчанию. Функции считаются так же: авторизация — 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, — предложение: набросок одной разумной формы, которую мог бы принять ваш продукт, а с наброском можно спорить. Ровно для этого и нужен первый разговор со студией. Цена — не набросок. Она вышла из того же движка и той же таблицы правил, что и любое другое число на этом сайте, и останется тем же числом, когда вы сделаете скриншот.