Как встроить AI‑чат в Next.js‑SaaS и не убить маржу
Пошаговая архитектура, контроль стоимости токенов, кэш, RAG, провайдер‑фолбэки, лимитирование и продакшен‑ловушки — чтобы AI‑чат приносил выручку, а не сжигал её.

Если добавить AI‑чат в SaaS «как получится», вы быстро получите счета за токены больше выручки, рассерженных клиентов из‑за таймаутов и утечки данных между арендаторами. Эта статья — про архитектуру и дисциплину, которые позволяют запустить AI‑чат на Next.js и сохранить маржу.
Если коротко: встраивайте чат как потоковый Edge‑роут (Next.js Route Handler), используйте адаптер над несколькими LLM‑провайдерами с деградацией, ограничивайте контекст и кэшируйте RAG, измеряйте токены на каждый запрос и применяйте бюджет/квоты. Добавьте пер‑тенантные лимиты, наблюдаемость затрат и fallback‑модели. Ниже — конкретный стек, код и продакшен‑паттерны.
Базовая архитектура AI‑чата в Next.js
Как выглядит минимально жизнеспособная архитектура, которая не убивает бюджет и не ломается под нагрузкой:
- UI: Next.js (App Router), React Server Components + клиентский компонент для стриминга ответов (SSE/Web Streams). Tailwind/Chakra — по вкусу.
- API:
app/api/chat/route.ts(Edge runtime) — принимает сообщения, дергает провайдер через адаптер, стримит ответ в UI. - Память чата: Redis (например, Upstash) для кратковременного состояния беседы; долгосрочная — периодические саммари и хранение в Postgres.
- Данные для RAG: Postgres + pgvector или управляемый Qdrant/Weaviate (зависит от объема и SLO). Ингест фоновой джобой (Queue: Cloudflare Queues, Sidekiq, BullMQ).
- Авторизация/тенантность: JWT/NextAuth, таблицы с
tenant_id, желательно Row‑Level Security в Postgres. - Наблюдаемость: OpenTelemetry (трейсы), Sentry (ошибки), ClickHouse/BigQuery (сырые логи чатов и затрат), алерты.
Поток данных:
- Клиент отправляет prompt →
POST /api/chat. - Route Handler валидирует, вынимает контекст (история, RAG), режет по бюджету токенов.
- Адаптер выбирает модель (по тарифу/нагрузке), делает запрос как стрим.
- Параллельно считаем токены и стоимость, логируем метаданные (но не персональные данные — см. PII‑редакцию).
- Отдаем стрим в UI. По завершении — саммари беседы в Redis→Postgres.
Пример минимального Route Handler с потоковой выдачей и грубым контролем токен‑бюджета:
// app/api/chat/route.ts
import { NextRequest } from 'next/server'
export const runtime = 'edge'
// Псевдо‑адаптер над несколькими провайдерами
import { pickModel, streamLLM, countTokens } from '@/lib/ai'
import { getTenant, assertQuota, trackUsage } from '@/lib/billing'
import { buildContext } from '@/lib/context'
export async function POST(req: NextRequest) {
try {
const { messages } = await req.json()
const tenant = await getTenant(req)
await assertQuota(tenant.id) // hard/soft cap
// Сбор контекста (история + RAG) под бюджет токенов
const ctx = await buildContext({ tenantId: tenant.id, messages, maxInputTokens: 6_000 })
// Выбор модели c fallback по тарифу, нагрузке и SLA
const model = pickModel({ tier: tenant.tier, prefer: 'fast', maxPricePer1M: tenant.cap })
// Черновой расчет бюджетов
const estInput = countTokens(ctx)
if (estInput > 8_000) {
ctx.truncate() // отрезаем историю/контекст
}
const { readable, usageMeta } = await streamLLM({ model, messages: ctx.toMessages(), temperature: 0.2 })
// Фоновая фиксация затрат/логов (не блокируем стрим)
trackUsage({ tenantId: tenant.id, model, ...usageMeta }).catch(() => {})
return new Response(readable, {
headers: { 'Content-Type': 'text/event-stream' },
})
} catch (e: any) {
return new Response(JSON.stringify({ error: e.message ?? 'unknown' }), { status: 400 })
}
}
Пара заметок:
- Edge‑рантайм сокращает холодные старты и даёт приемлемые задержки для стриминга.
- Контекст формируйте функцией, которая умеет саммаризовать историю, резать RAG и соблюдать жёсткий потолок токенов на вход.
- Логи затрат не должны блокировать выдачу — отправляйте асинхронно.
Управление стоимостью: токены, бюджеты, кэш
Ключ к марже — не «умная модель», а дисциплина по токенам.
- Ограничение входа: жёсткий cap на
input_tokensper request. Историю саммаризуем в 300–800 токенов, остальное — архив. - Ограничение выхода: просим модель отвечать кратко, а UI раскрывать детали по follow‑ups. «Разделяй и отвечай коротко» — это про деньги.
- Тарифные бюджеты: у каждого тарифа свой дневной/месячный кап. Пер‑тенантные алерты при 80/100%.
- Кэш RAG: кэшируем с учётом версии документа и эмбеддингов, TTL — по данным (документы — дольше, ответы — короче). Хэш
prompt+retrieved_ids. - Эмбеддинги пакетом: объединяйте 32–128 чанков в один батч, удаляйте дубликаты по хэшу контента.
Как считать себестоимость разговора:
- Для каждой модели у провайдера есть цена за 1М входных/выходных токенов. Методология:
cost_in = (input_tokens / 1_000_000) * price_incost_out = (output_tokens / 1_000_000) * price_outcost_total = cost_in + cost_out + cost_embeddings (если был RAG)
- Маржинальность с учётом прочих COGS:
margin = revenue_per_chat - (cost_total + infra + support_share). - Не подставляйте «среднюю температуру по больнице» — логируйте фактические токены на каждый запрос.
Пример учёта токенов на ответе (зависит от SDK провайдера; многие возвращают usage):
type Usage = { input_tokens: number; output_tokens: number }
export async function trackUsage({ tenantId, model, usage }: { tenantId: string; model: string; usage: Usage }) {
// Сохраняем в ClickHouse таблицу ai_usage (tenant_id, model, in, out, ts)
await clickhouse.insert('ai_usage', [{
tenant_id: tenantId,
model,
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
ts: new Date().toISOString(),
}])
}
Пороговые защиты:
- Если оценка входных токенов > лимита — синхронно режем контекст и уведомляем UI.
- Если выход превысил лимит — прерываем стрим (UI должен это уметь) и предлагаем продолжить.
- При превышении месячного капа тарифа — блокируем генерацию и предлагаем апгрейд/кредиты.
Небольшая инженерная ирония: лучшие «оптимизации LLM» — это обычные if и «не класть в промпт всё, что знаем».
Выбор провайдера и fallback‑стратегия
Не завязывайтесь на одного провайдера — нужны хотя бы два и чёткие правила деградации. Типичный набор: одна быстрая/дешёвая модель для большинства запросов и одна «мозговитая» для сложных кейсов и VIP‑тарифов. Плюс возможность локальной/самостоятельной инференс‑ноды при особых требованиях к данным/латентности.
Сравнение стратегий:
| Стратегия | Качество | Задержка | Стоимость | Управление рисками | Когда уместно |
|---|---|---|---|---|---|
| Одна быстрая модель для всех | Среднее | Низкая | Низкая | Низкое | MVP, хелп‑центр, FAQ |
| Две модели: fast + smart | Выше | Средняя | Средняя | Выше | Продакшен чат, опции тарифов |
| Мульти‑провайдер с маршрутизацией | Высокое | Средняя/Высокая | Средняя/Высокая | Высокое | Гео‑SLA, критично к даунтаймам |
| Локальная LLM (vLLM) + облачный fallback | Зависит | Низкая/Средняя | Контролируемая | Среднее | Низкая стоимость, приватность |
Маршрутизация запросов:
- По тарифу: base — fast; pro — smart или re‑ask на сложные промпты.
- По сложности: эвристики (длина, количество инструментов), классификатор сложности (дешёвая модель решает, что отправлять на «умную»).
- По задержке: если провайдер «тупит», переключаемся на альтернативу.
В адаптере держите нейтральный интерфейс: complete({ model, messages, tools }), а конфигурации цен/лимитов — отдельно. Это позволит при изменении цен провайдера просто подкрутить правила.
RAG без лишних счетов: ingest, хранение, запрос
Чат без доменных знаний — игрушка. Но RAG легко превращается в «пылесос токенов».
Ингест:
- Чанкуйте документы 400–800 токенов с overlap 50–100.
- Очистка: убирайте boilerplate, сжимайте таблицы до Markdown‑представления.
- Эмбеддинги: используйте одну компактную модель эмбеддингов для всего проекта; меняя модель, версионируйте в схеме.
- Дедупликация: хэш текста чанка, не индексируйте одинаковые куски.
Хранилище:
- До ~5–10 млн чанков хорошо живёт Postgres + pgvector (удобно, транзакции, RLS). Дальше — Qdrant/Weaviate.
- Метаданные:
tenant_id,doc_id,version,tags,updated_at.
Запрос:
- Hybrid search: BM25/FTS + ANN по эмбеддингам; интерсектируйте по
tenant_idи фильтрам. - Re‑rank: дешёвая доранжировка 10–20 кандидатов локальной моделью/правилами.
- Ограничение контекста: 3–6 чанков, сверху — саммари чанков (ультра‑дёшево).
Пример upsert/query в Postgres + pgvector (TypeScript + SQL):
// псевдокод: генерация эмбеддингов и апсерта
import { sql } from '@vercel/postgres'
import { embedMany } from '@/lib/embeddings'
export async function upsertChunks(tenantId: string, docId: string, chunks: string[]) {
const vectors = await embedMany(chunks) // батч 64–128
await sql.begin(async (tx) => {
await tx`delete from rag_chunks where tenant_id=${tenantId} and doc_id=${docId}`
for (let i = 0; i < chunks.length; i++) {
await tx`insert into rag_chunks (tenant_id, doc_id, content, embedding) values (${tenantId}, ${docId}, ${chunks[i]}, ${vectors[i]})`
}
})
}
export async function search(tenantId: string, query: string) {
const [qv] = await embedMany([query])
// ANN + простая фильтрация
const { rows } = await sql`
select content, doc_id
from rag_chunks
where tenant_id=${tenantId}
order by embedding <-> ${qv}
limit 5
`
return rows.map(r => r.content)
}
В продакшене добавьте:
- Взвешивание источников (политика доверия).
- «Citation mode»: LLM обязан сослаться на
doc_id. - TTL на кэш ответов с ключом
tenant_id:hash(prompt+sources).
Безопасность, мультиарендность и лимитирование
Мультиарендность — главный риск утечек. Мы видим типовые ошибки:
- Запрос к векторному хранилищу без фильтра
tenant_id. - Тестовые ключи в проде, общий кэш на всех.
- Неизолированная история чатов.
Практики из коробки:
- Postgres: включите RLS и проверяйте политику на каждую таблицу (чаты, RAG, usage). Похожую проблему разбирали в нашем разборе утечек: исследование утечек Supabase.
- Ключи: храните провайдерские ключи в KMS/Secrets Manager, не в ENV статично, если нужен ротационный доступ пер‑тенант.
- Redis‑ключи с префиксом
tenant_idи строгими TTL. - Rate limit: скользящее окно в Redis, ключ —
tenant_id:route, санкции при аномалиях.
Пример лимитера:
import { kv } from '@vercel/kv'
export async function rateLimit(tenantId: string, limit = 60, windowSec = 60) {
const key = `rl:${tenantId}:${Math.floor(Date.now()/1000/windowSec)}`
const n = await kv.incr(key)
if (n === 1) await kv.expire(key, windowSec)
if (n > limit) throw new Error('rate_limited')
}
Модерация и защита промптов:
- Модерация входных/выходных сообщений дешёвой моделью‑классификатором; при срабатывании — нейтрализация/блокировка.
- Против prompt‑injection: чёткие системные инструкции, whitelisting функций, JSON‑схемы для tool calling, валидация по схеме Zod.
- PII‑редакция журналов: заменяйте e‑mail/телефон/номера на токены перед отправкой в логи.
Наблюдаемость и финансы: что и как мерить
Без чёткой телеметрии по токенам маржу вы не увидите. Схема наблюдаемости:
- Трейсы: один
trace_idна чат‑запрос, спаны на: RAG поиск, LLM вызов, модерация, пост‑процессинг. - Метрики: гистограммы задержек, доля успешных стримов, частота фолбэков, средние
input/output_tokensпо тарифам. - Логи: промпты/ответы с редакцией PII, usage‑события с ценовыми коэффициентами провайдера.
- Дашборды маржи:
ARPU,COGS LLM,Infra,Gross Margin, «стоимость на диалог», «стоимость на функцию». - Алерты: 95‑й перцентиль задержки, рост
output_tokens, аномальный всплеск RAG‑запросов, «провайдер 5xx».
Про инфраструктурные счета и почему они вас удивят — см. наш разбор: ваш счёт AWS врёт вам. В AI‑функциях добавьте ещё один слой контроля: «блокировка при x3 от медианы затрат на диалог за сутки».
Что ломается в продакшене
- Таймауты провайдера и обрыв стриминга через корпоративные прокси. Лечится retry с backoff, фолбэк на альтернативного провайдера, опция «продолжить ответ» в UI.
- Бесконечные циклы tool calling. Ограничивайте количество шагов, валидируйте намерение, кэшируйте результаты инструментов.
- Вспухание памяти чата (история растёт, токены — тоже). Периодический autosummarize и строгий бюджет истории.
- Непредсказуемый формат JSON‑ответов. Требуйте
strict JSONв system prompt и валидируйте Zod‑схемой перед выполнением. - Векторное хранилище: рассинхронизация версий эмбеддингов. Версионируйте поле
embedding_version, делайте миграции пакетами. - Утечки между арендаторами в кэше. Всегда включайте
tenant_idв ключи, изолируйте namespace. - Перекос расходов на эмбеддинги при большом ingest. Дедупликация, батчи, ночные окна, контроль версий.
Бизнес‑контекст: стоимость и маржинальность
Как не утонуть в затратах и не испортить опыт пользователя:
Ценообразование и квоты:
- Кредиты/квоты вместо «безлимита»: пользователю прозрачно, вам — предсказуемость COGS.
- Тарифы отличайте не только лимитами, но и моделью/скоростью: base — fast, pro — smart + приоритетная очередь.
- Нестандартные клиенты: выделенный budget‑пул и договорённости о провайдерах (вплоть до on‑prem vLLM).
Финансовые правила:
- Жёсткий бюджет на разговор и на день: отсекайте хвосты, предлагайте «продолжить» за кредит.
- Дифференциация RAG: сложные/дорогие контексты — только для pro/enterprise.
- Мониторинг токенов по функциям: какие промпты дорогие, какие инструменты «жрут» больше всего.
Как считать экономику без гаданий:
- Смоделируйте 3 корзины использования: «короткий ответ», «с контекстом», «длинная сессия». Для каждой — диапазон
in/out tokensи долю трафика. Умножьте на актуальные расценки провайдера — получите себестоимость, а затем заложите целевую валовую маржу. Наша рекомендация по дисциплине: держать долю LLM‑COGS в выручке функции в планке, зафиксированной в вашей финансовой модели, и реагировать автоматикой, а не вручную.
UX и маржа дружат:
- Стриминг коротких ответов ощущается «быстро» и экономит токены.
- Последующие уточнения дешевле «одного гигантского ответа».
- FAQ/готовые команды в UI снижают токены и повышают конверсию в решённую задачу.
Минимальный план внедрения (2 недели)
- День 1–2: каркас Next.js Route Handler (Edge), клиент для стриминга, адаптер провайдеров.
- День 3–4: Redis‑память чата, autosummary, бюджет истории, модерация.
- День 5–6: RAG MVP: ingest, pgvector, поиск, ограничение контекста.
- День 7: учёт токенов, логирование usage, первичные дашборды.
- День 8: квоты/кредиты, rate limit, алерты.
- День 9–10: fallback‑маршрутизация, ретраи, «продолжить ответ» в UI.
- День 11–12: PII‑редакция, RLS, нагрузочное тестирование, хаос‑тест фолбэков.
- День 13–14: прайсинг и A/B: fast vs smart, caps, онбординг с лимитами.
Пример клиентского компонента для стриминга
'use client'
import { useState } from 'react'
export default function ChatBox() {
const [msg, setMsg] = useState('')
const [lines, setLines] = useState<string[]>([])
async function send() {
const res = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ messages: [{ role: 'user', content: msg }] }) })
const reader = res.body!.getReader()
const dec = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = dec.decode(value)
// сервер шлёт SSE; упрощённо воспринимаем как текст
setLines(prev => [...prev, chunk])
}
}
return (
<div>
<textarea value={msg} onChange={e => setMsg(e.target.value)} />
<button onClick={send}>Send</button>
<pre>{lines.join('')}</pre>
</div>
)
}
FAQ
Можно ли обойтись без RAG и всё равно быть полезными?
Да, если сценарии узкие (FAQ, короткие команды) и вы тщательно пишете системные инструкции. Но без RAG модель будет «галлюцинировать» про ваш домен. Для продуктивных сценариев добавьте хотя бы минимальный поиск по базе знаний с 3–5 чанками контекста.
Как измерять токены, если провайдер не даёт usage?
Используйте локальные токенайзеры (например, совместимые с выбранной моделью), чтобы оценить вход/выход. Это не 100% идеально, но достаточно для бюджетов и алертов. Логи фиксируйте на завершении стрима, чтобы знать output_tokens фактически.
Где хранить историю чата?
Оперативно — в Redis (быстро для стриминга), долговременно — саммари в Postgres, полную историю — по согласию пользователя/политике приватности. Не забудьте RLS и PII‑редакцию в логах.
Что выбрать для векторного поиска: pgvector или отдельную базу?
До миллионов чанков и умеренных SLO подойдёт pgvector (транзакции, простота, один бэкап). При росте и необходимости тонкой настройки ANN/шардирования — Qdrant/Weaviate. Ключевой фактор — требования к латентности и рост индекса.
Как избежать дорогих «простыней» ответов?
Управляйте форматом: просите модель отвечать кратко, выносите примеры/таблицы в follow‑up. Делите сложную задачу на шаги. Ограничивайте max_tokens на ответ и прерывайте стрим по лимиту с предложением продолжить.
Как тестировать fallback‑маршрутизацию?
Проведите хаос‑тест: искусственно возвращайте ошибки/таймауты от провайдера и проверяйте переключение, сохранение контекста и корректность биллинга/логов. Имейте ручной «переключатель» провайдера на уровне конфигурации.
Key takeaways
- Архитектура: Edge‑стриминг + адаптер провайдеров + строгий токен‑бюджет и кэш RAG — база для маржинального чата.
- Маржа рождается из ограничений: режьте контекст, саммаризуйте историю, используйте дешёвые модели там, где это допустимо.
- Мульти‑провайдер и fallback обязательны: даунтаймы и всплески задержек в LLM — не редкость.
- Наблюдаемость затрат — часть продукта: логируйте токены и стройте алерты по экономике, а не только по ошибкам.
- Мультиарендность требует дисциплины: RLS, tenant‑scoped кэш и строгие фильтры в RAG.
- План внедрения на 2 недели реалистичен, если не пытаться решить всё одним промптом.
Если вы строите AI‑чат в Next.js‑SaaS и хотите сохранить маржу, MTBYTE поможет спроектировать архитектуру, внедрить адаптеры, телеметрию и квоты. Напишите нам: /contact.
СЛЕДУЮЩИЙ ШАГ
Понравилось как мыслим?
Применяем те же принципы в клиентских проектах: AI, автоматизации, продукты, которые не умирают после релиза.