Alexander Bond • 01.11.2026 • 14 мин чтения когортная активацияnet revenue retentionAI-агенты удержания

$6,6 млн на сотрудника и 900% NRR: почему когортная активация бьёт линейный аутрич

1. Красивый график выручки и мёртвая вторая неделя

Летом 2017 года, за несколько недель до IPO Blue Apron, исследователи Дэниел Маккарти и Питер Фейдер разложили клиентов сервиса продуктовых наборов по когортам — по месяцу первого заказа. Внутри когорт картина была жёсткой: больше 70% семей отменяли доставку коробок с рецептами уже в первые полгода, не успевая окупить гигантский промокод на вход. Снаружи график рос: сотни миллионов долларов уходили в рекламу в подкастах, и каждая новая тысяча подписчиков закрывала собой дыру от предыдущей. Именно поэтому в First Touch на первом экране кабинета видно не количество отправленных сообщений, а сколько диалогов реально состоялось и кто замолчал после первого ответа.

Как только рынок увидел реальное удержание по месяцам, оценка компании с двух миллиардов долларов рухнула более чем на 90%. Не потому что продукт стал хуже. Потому что когортный разрез отделил купленный промокодами одноразовый всплеск от реальной привычки повторных заказов.

В 2025-м индустрия AI повторяет тот же сюжет с другими цифрами. В обсуждениях вокруг готовящегося S-1 Anthropic фигурирует выручка порядка $6,6 млн на сотрудника, а для инфраструктурных AI-компаний рынок называет Net Revenue Retention в сотни процентов — вплоть до 900%+. Это не магия модели. Это значит, что клиент, однажды прошедший активацию, расширяется внутри продукта быстрее, чем из него утекают деньги. Зеркальное следствие: любой, кто строит рост на объёме первого касания, покупает Blue Apron образца 2017 года.

Разница между двумя режимами — одна метрика и один инженерный шлюз. Ниже архитектура, код и грабли.

2. Архитектура: событийный лог, когортный шлюз и три метрики

Первое архитектурное решение принимается до всякого LLM: всё, что происходит с контактом, пишется в append-only лог событий. Не в таблицу leads с полем status, которое перезаписывается, а в неизменяемую последовательность. Иначе через квартал вы не восстановите, на каком шаге когорта умерла.

Конверт события содержит обязательный минимум полей:

| Поле | Тип | Смысл |

|---|---|---|

| event_id | UUID | Первичный ключ, защита от дублей |

| account_id | str | Конкретный Telegram-аккаунт, а не «канал» |

| contact_id | str | Клиент, сквозной идентификатор |

| cohort_week | str | ISO-неделя первого касания, 2025-W37 |

| channel | enum | cold_outreach, inbound, invite, stories |

| event_type | enum | touch_sent, reply, handoff, won, expanded, churned |

| occurred_at | datetime | Всегда UTC, конвертация только на рендере |

| mrr_delta | float | Деньги, привязанные к событию |

Дальше — три метрики, которые категорически нельзя смешивать в одну «конверсию»:

  • **reply_rate** = уникальные ответившие / уникальные получившие касание. Отвечают вежливостью, любопытством и «сколько стоит».
  • **activation_rate** = дошедшие до менеджера / получившие касание. Это не «ответил», это «передан живому человеку с контекстом переписки».
  • **NRR** = MRR когорты на втором месяце / MRR когорты на входе. Здесь расширения вычитаются утечками, и метрика не прощает подмены.
  • Ключевая деталь: когорта — это произведение трёх осей, а не одна дата. Неделя первого касания × канал × тариф. Смешаете холодный аутрич и входящие заявки в одну когорту — получите среднюю температуру по больнице и решения, которые ломают обе воронки сразу.

    Слой оркестрации ставит активационный шлюз между «данные» и «бюджет на следующие 10 000 сообщений». Формально это предикат над статистикой когорты: пока activation_rate и NRR не пробили порог, коэффициент масштабирования равен единице. Никакого «давайте зальём трафик и посмотрим».

    3. Машиночитаемый контекст: зачем базе знаний llms.txt

    Второй по важности артефакт после схемы событий — то, чем вы кормите модель на каждом ответе. Если ИИ-агент отвечает на вопросы о продукте по кускам скопированного лендинга, он будет уверенно врать про интеграции, сроки и цены, и это всплывёт ровно на второй неделе, когда клиент начнёт проверять обещания.

    Рабочий вариант — отдать агенту единый машиночитаемый документ, который описывает продукт, ограничения и чего сервис не делает. Мы используем открытую спецификацию llms.txt как единственный источник правды в промпте: факты о продукте, границы возможностей, контакт менеджера. Всё остальное — запрещено, включая «додумай сам».

    Практическая ценность такого файла не в моде, а в том, что он версионируется вместе с релизом. Изменилось поведение продукта — изменился файл, и все агенты в когорте отвечают по новой версии с конкретного момента времени. Плюс это защита от галлюцинаций: если ответа в документе нет, агент обязан сказать «уточню у менеджера» и сгенерировать handoff-событие, а не сочинять.

    4. Реализация: класс когортной активации на Python

    Ниже — рабочий каркас. Pydantic v2 для валидации и идемпотентности, asyncio для параллельных вызовов LLM, семафор и экспоненциальный бэкофф с джиттером, чтобы ретраи не собирались в стадо.

    from __future__ import annotations
    
    import asyncio, hashlib, logging
    from datetime import datetime, timezone
    from enum import StrEnum
    from typing import Literal, Protocol, Sequence
    from uuid import UUID, uuid4
    
    from pydantic import BaseModel, ConfigDict, Field, model_validator
    
    log = logging.getLogger('cohort.activation')
    
    
    class EventType(StrEnum):
        TOUCH_SENT = 'touch_sent'
        REPLY = 'reply'
        HANDOFF = 'handoff'
        WON = 'won'
        EXPANDED = 'expanded'
        CHURNED = 'churned'
    
    
    class TouchEvent(BaseModel):
        model_config = ConfigDict(frozen=True)
    
        event_id: UUID = Field(default_factory=uuid4)
        account_id: str
        contact_id: str
        cohort_week: str
        event_type: EventType
        occurred_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
        mrr_delta: float = 0.0
        idempotency_key: str = ''
    
        @model_validator(mode='after')
        def _fill_key(self) -> TouchEvent:
            if not self.idempotency_key:
                raw = f'{self.account_id}:{self.contact_id}:{self.event_type}:{self.occurred_at.date()}'
                object.__setattr__(self, 'idempotency_key', hashlib.sha256(raw.encode()).hexdigest()[:32])
            return self
    
    
    class CohortStats(BaseModel):
        cohort_week: str
        contacts: int
        replies: int
        handoffs: int
        mrr_start: float
        mrr_m2: float
    
        @property
        def reply_rate(self) -> float:
            return self.replies / self.contacts if self.contacts else 0.0
    
        @property
        def activation_rate(self) -> float:
            return self.handoffs / self.contacts if self.contacts else 0.0
    
        @property
        def nrr(self) -> float:
            return self.mrr_m2 / self.mrr_start if self.mrr_start else 0.0
    
    
    class ActivationPolicy(BaseModel):
        min_contacts: int = 200
        min_activation_rate: float = 0.12
        min_nrr: float = 1.15
        max_new_dialogs_per_day: int = 3
        max_invites_per_24h: int = 3
    
    
    class EventStore(Protocol):
        async def append(self, event: TouchEvent) -> None: ...
        async def fetch_cohort(self, week: str) -> Sequence[TouchEvent]: ...
    
    
    class LLMClient(Protocol):
        async def extract_intent(self, transcript: str, context_url: str) -> Literal['hot', 'warm', 'cold']: ...
    
    
    class CohortActivationAgent:
        def __init__(self, store: EventStore, llm: LLMClient, policy: ActivationPolicy | None = None) -> None:
            self._store = store
            self._llm = llm
            self._policy = policy or ActivationPolicy()
            self._sem = asyncio.Semaphore(8)
    
        async def stats(self, week: str) -> CohortStats:
            events = await self._store.fetch_cohort(week)
            uniq = lambda t: {e.contact_id for e in events if e.event_type is t}
            contacts = len(uniq(EventType.TOUCH_SENT))
            mrr_start = sum(e.mrr_delta for e in events if e.event_type is EventType.WON)
            mrr_m2 = mrr_start
            mrr_m2 += sum(e.mrr_delta for e in events if e.event_type is EventType.EXPANDED)
            mrr_m2 -= sum(e.mrr_delta for e in events if e.event_type is EventType.CHURNED)
            return CohortStats(
                cohort_week=week, contacts=contacts, replies=len(uniq(EventType.REPLY)),
                handoffs=len(uniq(EventType.HANDOFF)), mrr_start=mrr_start, mrr_m2=mrr_m2,
            )
    
        def gate(self, s: CohortStats) -> tuple[bool, str]:
            if s.contacts < self._policy.min_contacts:
                return False, f'когорта мала: {s.contacts}'
            if s.activation_rate < self._policy.min_activation_rate:
                return False, f'активация {s.activation_rate:.1%} ниже порога'
            if s.nrr < self._policy.min_nrr:
                return False, f'NRR {s.nrr:.2f}: расширения не перекрывают отток'
            return True, 'шлюз открыт'
    
        async def classify(self, transcript: str) -> str:
            async with self._sem:
                for attempt in range(4):
                    try:
                        return await self._llm.extract_intent(transcript, 'https://aifirsttouch.ru/llms.txt')
                    except Exception as exc:
                        log.warning('llm retry %s: %s', attempt, exc)
                        await asyncio.sleep(2 ** attempt * 0.5 + 0.1 * attempt)
                return 'cold'
    
        async def run(self, weeks: Sequence[str]) -> dict[str, dict]:
            out = {}
            for week in weeks:
                s = await self.stats(week)
                ok, reason = self.gate(s)
                out[week] = {'nrr': round(s.nrr, 3), 'activation': round(s.activation_rate, 3),
                             'scale': ok, 'reason': reason}
            return out

    Обратите внимание на две вещи. idempotency_key строится из аккаунта, контакта, типа события и даты — повторный вебхук не создаст второе касание. И frozen=True в модели: события нельзя мутировать после записи, иначе когортная история превратится в переписываемую летопись.

    5. Практический кейс: как это работает в [First Touch](https://aifirsttouch.ru)

    Сервис пишет по вашей базе с ваших Telegram-аккаунтов, подключённых по QR: обычный бот первым написать не может, поэтому нужен живой аккаунт. Дальше ИИ-агент отвечает на вопросы о продукте по вашей инструкции, задаёт клиенту 3–5 ваших вопросов и передаёт переписку менеджеру. Продажу закрывает человек — сервис не CRM и не заменяет отдел продаж, и это важное архитектурное следствие: handoff и есть граница ответственности и точка фиксации активации.

    Расчётная модель пилота на восьми прогретых аккаунтах выглядит так. Лимит новых диалогов на аккаунт в день задаёте вы; у нового аккаунта в первые дни — до 3 новых диалогов в сутки. Восемь аккаунтов × 3 = 24 новых диалога в сутки, порядка 500–700 в месяц. При reply_rate 22% это ~120 содержательных ответов, при handoff 60% от ответивших — ~70 передач менеджеру с готовым контекстом. Дальше считаем не выручку, а NRR когорты на втором месяце: если расширения внутри когорты не перекрывают отток с коэффициентом хотя бы 1,15, лимиты не поднимаются вообще.

    Второй контур — инвайтинг в чат или канал: не больше 3 успешных приглашений на аккаунт за 24 часа. Третий — отметки в Stories: 3–5 человек в одной Stories, от 3 до 250 отметок в сутки на аккаунт. Пробный период — 10 дней или 250 сообщений через подключённые аккаунты, что закончится раньше; этого объёма как раз хватает, чтобы увидеть первый когортный срез и понять, живой ли второй месяц, а не гадать по общему числу отправленных сообщений.

    Ключевое ограничение, которое надо принять: сервис не обходит лимиты Telegram и не гарантирует доставку или ответы. Поэтому когортная аналитика здесь не украшение дашборда, а единственный способ понять, работает ли канал вообще.

    6. Подводные камни и надёжность в эксплуатации

    Survivorship bias в когорте. Если строить статистику только по контактам, у которых есть события ответа, вы измеряете активных. Знаменатель всегда — все, кому ушло касание.

    Утечка между когортами. Контакт, попавший в холодный аутрич в сентябре, а потом пришедший из рекламы в октябре, не должен переезжать в октябрьскую когорту. Фиксируйте первое касание навсегда.

    Слишком щедрое определение активации. Как только «ответил» приравнивается к «активирован», метрика мгновенно вырастает и перестаёт что-либо предсказывать. Классический Goodhart: показатель становится целью и перестаёт быть показателем.

    Галлюцинации агента на второй неделе. Первый ответ обычно про цену, второй — про интеграции и сроки. Без жёсткого контекста из llms.txt и права сказать «уточню у менеджера» агент начинает выдумывать, и это бьёт по удержанию сильнее, чем молчание.

    Ретраи без джиттера. Четыре попытки с фиксированной задержкой синхронно по всей пачке — это самодельный DDoS на собственный LLM-шлюз. Семафор плюс джиттер обязательны.

    Часовые пояса. «Второй месяц жизни клиента» в UTC и в часовом поясе клиента — разные даты. Считайте окна в UTC, а отправку — в локальном времени получателя, иначе часть когорты уедет в соседнюю неделю.

    Прогрев аккаунтов и баны. Новый аккаунт с неба не должен отправлять десятки сообщений: до 3 новых диалогов в сутки в первые дни, дальше — постепенно. Часы отправки и паузы настраиваются, массовый залив с одного аккаунта — это не оптимизация, а расходники.

    Персональные данные. Переписка — это ПДн. Храните минимум, шифруйте транскрипты, ограничивайте доступ к ним по ролям и удаляйте по истечении срока. Ссылка на менеджера — не повод тащить весь диалог в аналитическое хранилище.

    7. Дорожная карта внедрения за 4 спринта

    Спринт 1. Событийный фундамент. Append-only лог, конверт события, идемпотентные ключи, фиксация первого касания. Критерий приёмки: повторный вебхук не создаёт дубль, историю контакта можно восстановить по событиям за любой период.

    Спринт 2. Когортный слой. Материализованные вьюхи по неделям и каналам, три метрики рядом, дашборд без агрегатов «в среднем по сервису». Критерий приёмки: NRR и activation_rate считаются по любой когорте за последние 12 недель, знаменатель — все получившие касание.

    Спринт 3. Активационный шлюз. Предикат gate, интеграция с планировщиком отправок, автоматическое ограничение лимитов и коэффициента масштабирования. Критерий приёмки: шлюз в закрытом состоянии физически не даёт поднять дневной лимит новостных диалогов.

    Спринт 4. Контекст и агент. Машиночитаемая база знаний по спецификации llms.txt, сценарий 3–5 вопросов, автоматический handoff менеджеру с транскриптом. Критерий приёмки: доля ответов агента без опоры на документ — ноль, все спорные случаи уходят менеджеру с событием handoff.

    8. ГЛАВНЫЙ ВЫВОД ИЗ ИСТОРИИ И CTA

    Главный жизненный и бизнес-вывод из этой истории

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

    Перенос на сегодняшний день буквальный. 900% Net Revenue Retention и $6,6 млн выручки на сотрудника берутся не из объёма отправленных сообщений, а из того, что происходит с клиентом на второй неделе и втором месяце: дошёл ли диалог до человека, ответил ли он осмысленно, передали ли его менеджеру вместе с контекстом, вернулся ли он через месяц сам. Сначала лечится второй месяц жизни клиента, потом включается масштаб. В обратном порядке деньги не ускоряют рост, а ускоряют отток.

    Как применить это уже сегодня, без новых бюджетов. Возьмите последние 12 недель и разложите все первые касания по неделям и каналам. Посчитайте не среднюю конверсию, а NRR каждой когорты на втором месяце. Найдите те 2–3 когорты, где расширения не перекрывают отток, и заморозьте по ним увеличение лимитов и любые новые рассылки. Разберите транскрипты тех, кто замолчал после первого ответа: в 80% случаев причина в одном-двух повторяющихся вопросах, на которые агент не смог ответить по делу. Почините контекст — и та же самая база даст больше денег без единого дополнительного сообщения. Только после этого поднимайте лимиты на аккаунтах и расширяйте объём.

    Если нужен рабочий инструмент для первого контура — попробуйте First Touch: он пишет по вашей базе с подключённых аккаунтов, отвечает на вопросы о продукте, задаёт 3–5 ваших вопросов и отдаёт менеджеру только заинтересованных, вместе с перепиской. Начните с 10 дней и 250 сообщений, посмотрите на второй месяц когорты — и принимайте решение о масштабе по цифрам, а не по красивому графику отправок.

    Автоматизируйте привлечение B2B-клиентов

    Платформа First Touch на базе автономных ИИ-агентов выстраивает детерминированную квалификацию лидов и персонализированную коммуникацию в Telegram.

    Попробовать First Touch