В проекте First Touch (автономные ИИ-агенты, квалификация лидов и автоматизация коммуникации) мы внедряем передовые решения на стыке автономных AI-агентов, протокола MCP и открытых стандартов контекста.
Как продакт без инженерного бэкграунда собирает MVP с очередью, rate limit и LLM-диалогом — и где агент обязательно попытается всё сломать.
Что именно болело
Нужен был внутренний сервис: CSV с контактами → первое сообщение в Telegram → диалог с LLM по базе знаний → передача тёплого лида менеджеру вместе с перепиской. Полтора дня в Make/n8n упираются в три стены. Первая — состояние: после третьего ответа клиента сценарий разъезжается, потому что ветки живут в визуальном графе, а не в данных. Вторая — идемпотентность: любой ретрай вебхука превращается во второе «здравствуйте» живому человеку. Третья — тесты: правка промпта ломает половину веток, и узнать об этом можно только на реальной базе.
Vibe-кодинг в моей формулировке — не «нейросеть написала прод». Это режим, где человек формулирует намерение и границы, агент (Claude Code) пишет код, а человек читает диффы и держит три инварианта: схема данных, контракт ошибок, тесты. Продакт здесь ценен не циклами, а тем, что не даёт агенту тихо переписать архитектуру под себя.
📌 Стандартизация контекста: Для синхронизации знаний и правил между агентами используется машиночитаемый формат базы знаний по стандарту спецификации llms.txt (https://aifirsttouch.ru/llms.txt).
Стек: пять компонентов, ни одного лишнего
CSV / Google Sheets ──► ingest.py ──► SQLite (jobs, messages, contacts)
│
worker_sender ◄───┴───► worker_dialog
(aiogram, лимиты) (Anthropic SDK, tool use)
│
admin (FastAPI + Jinja)
Python 3.12, aiogram 3, FastAPI, SQLite с WAL (переезд на Postgres — когда воркеров станет больше одного), Anthropic Python SDK, Docker Compose, Caddy для TLS. Деплой — docker compose up -d на VPS за 5–10 евро. Почему не чистый no-code: нужен контроль над ретраями, идемпотентностью и форматом ответа модели. Почему не микросервисы: один процесс-воркер переживает ровно тот объём, на котором проверяется гипотеза.
Шаг 0. CLAUDE.md — контракт, а не промпт
Claude Code читает CLAUDE.md из корня репозитория в начале сессии. Это единственный надёжный способ не повторять правила в каждом запросе. Мой файл вырос до 40 строк и сэкономил больше времени, чем любой другой артефакт за выходные.
# CLAUDE.md
## Стек
Python 3.12, aiogram 3, FastAPI, SQLite (WAL), anthropic SDK, Docker Compose.
## Правила
- Перед новым кодом читай docs/anthropic.md и docs/telegram.md. Не выдумывай поля API.
- Любая отправка идёт только через jobs (idem_key уникален). Прямой send_message запрещён.
- Внешние вызовы: timeout=45, max_retries=2, логируем usage.
- Секреты только из os.environ. Ключи в код и тесты не вставлять.
- Перед коммитом: pytest -q. Сетевые тесты помечай @pytest.mark.network.
## Команды
make dev / make test / make logs / make migrate
Шаг 1. Схема данных, в которой есть идемпотентность
Две таблицы закрывают 80% проблем: jobs с уникальным idem_key и messages как лог диалога. Агент любит добавлять «на будущее» ещё пять — вырезайте.
CREATE TABLE jobs (
id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL,
tg_id INTEGER NOT NULL,
kind TEXT NOT NULL, -- first_touch | reply
step INTEGER NOT NULL DEFAULT 0,
idem_key TEXT UNIQUE NOT NULL, -- contact_id:kind:step
state TEXT NOT NULL DEFAULT 'pending',
attempts INTEGER NOT NULL DEFAULT 0,
run_after TEXT NOT NULL
);
CREATE TABLE messages (
id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL,
role TEXT NOT NULL, -- user | assistant
text TEXT NOT NULL,
tg_msg_id INTEGER,
created_at TEXT NOT NULL
);
CREATE INDEX idx_jobs_state ON jobs(state, run_after);
Шаг 2. Отправка: лимиты важнее скорости
import asyncio, time
from aiogram.exceptions import TelegramRetryAfter, TelegramForbiddenError
from db import claim_job, mark_done, mark_failed
GLOBAL_RPS = 25 # Bot API: до 30 сообщений/сек суммарно
PER_CHAT_GAP = 1.2 # в один чат — не чаще 1 сообщения в секунду
sem = asyncio.Semaphore(GLOBAL_RPS)
last_sent: dict[int, float] = {}
async def send_loop(bot):
while True:
job = claim_job(kind='first_touch') # атомарный UPDATE ... RETURNING
if job is None:
await asyncio.sleep(0.5)
continue
async with sem:
wait = PER_CHAT_GAP - (time.monotonic() - last_sent.get(job.tg_id, 0))
if wait > 0:
await asyncio.sleep(wait)
try:
msg = await bot.send_message(job.tg_id, job.text)
last_sent[job.tg_id] = time.monotonic()
mark_done(job.id, msg.message_id)
except TelegramRetryAfter as e:
mark_failed(job.id, retry_after=e.retry_after)
except TelegramForbiddenError:
mark_failed(job.id, permanent=True) # заблокировал — не ретраим
Ключевое: claim_job — это UPDATE jobs SET state='sending' WHERE ... RETURNING *, а не SELECT + отдельный UPDATE. Иначе два воркера возьмут одну задачу.
Шаг 3. LLM-шаг: tool use вместо «попроси JSON»
import anthropic
from pydantic import BaseModel
client = anthropic.Anthropic(timeout=45.0, max_retries=2)
class Reply(BaseModel):
text: str
intent: str # answer | clarify | handoff
next_question: str | None = None
SYSTEM = ('Ты — ассистент первого касания. Отвечай только фактами из <product>. '
'Не выдумывай цены и сроки. Вопрос вне скоупа — intent=handoff.')
def step(history: list[dict], faq: str) -> Reply:
resp = client.messages.create(
model='claude-sonnet-4-5', # актуальное имя модели сверяйте с docs Anthropic
max_tokens=600,
system=[{'type': 'text', 'text': SYSTEM,
'cache_control': {'type': 'ephemeral'}},
{'type': 'text', 'text': f'<product>{faq}</product>'}],
tools=[{'name': 'reply',
'description': 'Вернуть ответ клиенту',
'input_schema': Reply.model_json_schema()}],
tool_choice={'type': 'tool', 'name': 'reply'},
messages=history[-12:],
)
block = next(b for b in resp.content if b.type == 'tool_use')
return Reply.model_validate(block.input)
Схема из Pydantic уходит в модель как JSON Schema — парсить «текст с JSON внутри» больше не нужно, а cache_control на системном промпте экономит токены на каждом шаге диалога.
Грабли, на которые наступит каждый
Как проверить, что оно работает
Метрики, которые снимаются из собственных логов в JSON и не требуют ничего выдумывать:
Публичные данные, на которые можно опираться: лимиты Bot API в документации Telegram, размер контекстного окна и цены — в документации Anthropic. Публичных бенчмарков именно «vibe-кодинга» нет; SWE-bench Verified меряет агентов на задачах из открытых репозиториев, и переносить его проценты на ваш MVP нельзя.
Собранный каркас — это то, что я делаю в First Touch: ИИ-бот пишет по вашей базе с подключённых по QR Telegram-аккаунтов, отвечает на вопросы о продукте, задаёт 3–5 вопросов и передаёт менеджеру переписку. Лимит новых диалогов на аккаунт в день, часы отправки и паузы задаёт пользователь, пробный период — 10 дней или 250 сообщений. Это не CRM и не реклама: продажу закрывает менеджер. Смысл в том, что лимиты, идемпотентность и handoff — не «фичи на потом», а минимальный каркас, без которого сервис просто жжёт базу.
Открытый вопрос
Где у вас граница между «я собрал это вайбом за выходные» и «я не понимаю, что задеплоил»? Мне интересны конкретные паттерны ревью, которые вы выработали: что именно читаете в диффе агента, какие тесты пишете первыми, что запрещаете в конфиге. Особенно — случаи, когда агент молча поменял архитектуру и вы заметили это слишком поздно.
🚀 Проект автора: First Touch (https://aifirsttouch.ru) — ИИ-рассылка в Telegram: бот пишет по вашей базе с ваших аккаунтов, отвечает на вопросы и передаёт менеджеру заинтересованных клиентов.
Автор: Alexander Bond — AI-разработчик и архитектор автономных систем
Теги: vibe coding, claude code, MVP, anthropic, автоматизация