ВЫБЕРИ, КАК ЧИТАТЬ
Выпуск идёт в двух версиях. Выбери одну — выбор сохранится при возврате, переключиться можно сверху страницы в любой момент. Можно прочитать обе, если решение принимать вместе с командой.
ОБЗОР · ЦЕНА · ВЫПИЛИЛИ · СРОКИ · ПРОВАЛЫ
OUTPUT ВЫПУСК №004
МОЗГ КОМАНДЫ В TELEGRAM
Бот, который читает рабочий чат, кодовую базу, git-коммиты, документацию и БД проекта одновременно. Собирает граф из 2199 сущностей, сам спрашивает уточнения, ночью разбирается с дублями. После миграции с GPT на китайские API — $8 в месяц.
В рабочем Telegram-чате сидит бот. Точнее, не в чате — он сидит во всём проекте сразу. Читает Telegram-форум, кодовую базу, git-коммиты, документацию, ходит в БД проекта смотреть данные. Из всего этого собирает граф знаний: 2199 сущностей, 110 кластеров, 19 фоновых процессов.
Бот не сервис. Он спорит, отказывается соглашаться без аргументов (отдельный аудитор-агент Grok обязан давать контраргументы), сам встревает когда видит висяк старше 14 дней. Сегодня всё это стоит $8 в месяц после миграции с $82.
Что значит «жить в проекте сразу»
- Telegram-чат — 11 тематических топиков, текст, голосовые, фото, файлы
- Git-репозиторий — коммиты падают в граф как entities типа
git_commit, обновляется каждые 30 минут - Документация — markdown-файлы режутся на чанки, индексируются в pgvector, опционально извлекаются в граф как entities
- БД проекта — бот ходит туда читать данные для аналитических вопросов («сколько активных пользователей», «какие подписки купили вчера»)
- HTTP notify — события от прод-сервера падают в чат с подписью
Чат — только один из входов. Граф знаний собирает структуру поверх всех источников: коммит про фикс бага связывается с упоминаниями этого бага в чате и с описанием в документации. На вопрос «что у нас по теме X» бот отвечает не «вот три похожих сообщения», а «вот открытая задача, вот связанные коммиты, вот решение в документации».
Память живёт в чате, и её не теряет
На вопрос «что мы решили по теме X две недели назад» бот отвечает за
5 секунд с конкретной формулировкой решения и ссылкой на оригинальное
обсуждение. Найти руками через ctrl+F в Telegram — 10 минут,
и не факт что найдёшь.
Никто не забывает
Задача висит без активности 14 дней? Бот молча пишет в общий топик: «эта задача без движения две недели — в работе или дропаем?». Это не назойливость, это синхронизация. Дважды в день в 11:00 и 18:00 бот шлёт батч уточняющих вопросов про неясные задачи — те, где контекст в исходном сообщении был слабый.
Утренняя сводка без вранья
В 9:00 в общий топик прилетает короткое сообщение из четырёх блоков: активные задачи, открытые вопросы по двум направлениям, расход за вчера по AI-провайдерам. Сводка собирается не моделью, а SQL прямо из БД — числа гарантированно правильные. Раньше она шла через большую модель, иногда писала «закрыли 5 задач» когда было 2.
$1.89 в неделю, $8 в месяц. На объёме крупнее (10+ человек) — реалистично $15-25/мес: фоновые процессы (граф, ночной ревизор, кластеризация) не зависят от размера команды, растут только эмбеддинги новых сообщений и частота вызовов чата.
Реальные затраты из ai_usage_daily
За последнюю неделю (9-15 мая 2026):
| Провайдер | Что делает | $/нед |
|---|---|---|
| GLM 5.1 (Z.AI direct) | Ночной ревизор (merge + status, batch 15 пар) | $0.92 |
| Claude Sonnet 4.6 | Чат при @bot — 17 ответов за неделю | $0.42 |
| MiMo Pro (OpenRouter) | Commit matcher — 253 коммита | $0.42 |
| Grok-4.3 | Когда зовут @grok — 20 ответов за месяц | $0.04 |
| OpenAI embeddings | text-embedding-3-large 1536d | $0.04 |
| Gemini 2.5 Flash | STT голосовых | $0.03 |
| Qwen 3.6 Plus/Flash | 1616 вызовов — KG-extractor, doc-extractor, community, gatekeeper, linker | $0.000 |
| Итого | $1.89/нед ≈ $8/мес | |
Qwen 3.6 на DashScope international (dashscope-intl.aliyuncs.com)
даёт ~1M токенов в день free на каждую модель. KG-экстрактор делает
~200 вызовов/день × 10K токенов = 2M токенов/день — попадаем в free quota.
Если кончится — встанет cost-enforcer ($30/мес потолок).
B02·2 · Живые диалоги
Как это выглядит в чате каждый день. Механика реальная, домен изменён.
Бот зовёт list_open(category='support') и whats_new(period='week')
под капотом. Ответ — не пересказ похожих сообщений, а состояние на
момент запроса. Каждая упомянутая задача — конкретная entity в графе.
Через 5 секунд после голосового бот кладёт текст реплаем. Никаких команд — это работает на каждое голосовое во всех топиках. ~$0.0005 за минуту аудио.
Никто не звал. Бот увидел висяк в графе (last_touched_at > 14 дней
+ статус open + есть связанный коммит) и встревает по rate-limit'у
2 раза в час.
Grok работает с явной инструкцией «обязан дать минимум 1-3 контраргумента, согласие без аргументов запрещено». На любой вопрос «как лучше — A или B?» нужно второе мнение, потому что основной AI почти всегда соглашается с первым предложенным вариантом.
Команда теряет контекст в чате, никто не ведёт формальный таск-трекер, голосовые остаются голосом, ключевые решения теряются между топиками. Если в команде один человек — не нужно (помнить за одного проще). Если есть формальный PM-процесс — этот бот его не заменяет, он живёт рядом.
Поток данных через систему — суточный цикл и непрерывные процессы.
19 фоновых job'ов в APScheduler. kg_extractor каждые 4 минуты —
основной pipeline памяти. commit_matcher каждый час — автозакрытие
задач от fix-коммитов (89 закрытых на сегодня). night_review
в 3:00 — мёрж дублей и пересчёт статусов batch'ами через GLM 5.1.
clarify_morning и clarify_evening в 11:00 и 18:00 —
батч уточняющих вопросов. daily_summary в 9:00 — компактный
дайджест прямо из БД, без LLM.
B04·1 · Граф знаний вместо RAG
Классический RAG (retrieval-augmented generation) работает так: разрезали все сообщения на чанки, посчитали эмбеддинги, на вопрос пользователя считаем эмбеддинг вопроса, ищем top-N похожих чанков, скармливаем модели как контекст. Это полнотекстовый поиск с подменой смысла на семантический.
Для команды этого мало. На вопрос «что у нас по дайджесту» классический RAG вернёт три похожих сообщения за разные дни, и придётся собирать картинку: какие из них релевантны сейчас, какие устарели, что было решено, что висит.
Нужно вместо: отвечать на вопрос состоянием, а не цитатами. «По дайджесту: одна задача в работе, один открытый вопрос ждёт ответа, три решения за последний месяц». Это не поиск — это запрос к структурированной памяти.
Для этого сообщения нужно превратить в сущности. Сущность — не сообщение, а понятие: задача, баг, фича, вопрос, решение. Имеет имя, описание, тип, статус, категорию, дату создания, дату изменения статуса, упоминания в разных сообщениях. Сообщения становятся «уликами», а не первичной памятью.
Сделать это руками невозможно. Делает LLM в фоне: каждые 4 минуты модель читает новые сообщения и решает «здесь упомянута новая задача? здесь баг закрыли? здесь два сообщения про одну сущность?». Получается живой граф из тысяч сущностей, который команда никогда не вела руками.
Минимум для графа знаний:
CREATE TABLE entities (
id BIGSERIAL PRIMARY KEY,
canonical_name TEXT NOT NULL,
description TEXT,
type TEXT NOT NULL, -- task/bug/feature/question/decision/risk/deadline/person
status TEXT NOT NULL, -- open/done/dropped/locked
category TEXT, -- dev/product/marketing/analytics/ops/finance/general
confidence NUMERIC, -- 0..1
needs_clarification BOOL,
community_id INT,
valid_from TIMESTAMPTZ, -- bi-temporal: с какого момента эта версия действует
valid_until TIMESTAMPTZ,
recorded_at TIMESTAMPTZ, -- когда эта версия записана
name_embedding VECTOR(1536),
description_embedding VECTOR(1536),
context_embedding VECTOR(1536),
aliases TEXT[],
merged_into_id BIGINT,
source_context JSONB
);
CREATE TABLE entity_mentions (entity_id BIGINT, message_id BIGINT, char_start INT, char_end INT);
CREATE TABLE entity_relations (
source_id BIGINT, target_id BIGINT,
relation_type TEXT -- blocks/relates_to/part_of/resolved_by/caused_by/...
);
CREATE TABLE events (
entity_id BIGINT, event_type TEXT, actor_name TEXT, payload JSONB,
valid_from TIMESTAMPTZ, recorded_at TIMESTAMPTZ
);
Без полей valid_from / valid_until / recorded_at вопрос
«каким был статус две недели назад» не имеет ответа. Добавить потом —
это миграция всего графа.
PgVector держит эмбеддинги в трёх местах: имя сущности, описание, контекстная фраза вокруг первого упоминания. Это нужно для дедупа в multi-signal linker.
Neo4j, FalkorDB, Qdrant — не нужны на нашем масштабе. 2199 entities,
110K векторов — pgvector в обычном Postgres закрывает
с запасом. Один движок проще поддерживать, бэкапить, мигрировать.
B04·2 · KG-экстрактор каждые 4 минуты
Каждые 4 минуты бот забирает все необработанные сообщения и для каждого собирает большое контекстное окно:
- Само сообщение
- Цепочка реплаев на два уровня выше
- 15 ближайших сообщений в том же топике за 6 часов
- 5 последних сообщений того же автора за 2 часа
- 3 семантически близких сообщения за 7 дней (cross-topic recall)
- Top-3 чанка документации по теме
- Top-10 уже существующих открытых сущностей в этом чате
- Top-5 семантически родственных существующих сущностей
Это окно скармливается модели с инструкцией «извлеки сущности, события, связи». Модель возвращает JSON. Бот ставит каждую сущность через multi-signal linker: проверяет, нет ли уже похожей.
Похожесть — по четырём сигналам: имя (cosine similarity эмбеддингов),
описание, контекст, структурные связи. Композитный скор:
0.30 × имя + 0.30 × описание + 0.20 × контекст + 0.20 × структура.
Веса подобраны вручную по precision/recall на golden-наборе.
Решение: скор больше 0.80 — автоматический мёрж. 0.60-0.80 — отдельный LLM-вопрос «это одно или разное?». Меньше 0.60 — создаём новую.
Большая часть работы — на дешёвой китайской модели Qwen 3.6 Plus. На нашем объёме (1616 вызовов в неделю) — попадает в free quota. Качество замерили: на golden-наборе работает идентично OpenAI-моделям, которые стоили $30+/мес.
async def composite_score(new_entity, candidate) -> float:
return (
0.30 * cosine(new_entity.name_emb, candidate.name_emb) +
0.30 * cosine(new_entity.desc_emb, candidate.desc_emb) +
0.20 * cosine(new_entity.ctx_emb, candidate.ctx_emb) +
0.20 * structural_overlap(new_entity, candidate)
)
async def link_or_create(new_entity):
candidates = await fetch_top_k_by_name(new_entity.name_emb, k=20)
scored = [(composite_score(new_entity, c), c) for c in candidates]
best_score, best_candidate = max(scored, key=lambda x: x[0])
if best_score > 0.80:
return await merge(new_entity, best_candidate)
elif best_score > 0.60:
decision = await llm_disambig(new_entity, best_candidate)
if decision == "same":
return await merge(new_entity, best_candidate)
return await create_new(new_entity)
extra_body={"enable_thinking": False}— Qwen-3.6 по умолчанию reasoning model, на «PONG» жжёт 200+ completion-токеновresponse_format={"type": "json_object"}— иначе оборачивает в markdown-кодблок- Слово «
json» обязательно в одном из messages — валидация DashScope, иначе 400. Добавляем в хвост user-prompt'a
DashScope international endpoint (dashscope-intl.aliyuncs.com),
не China — ключи между ними не взаимозаменяемы.
У OpenAI было response_format={"type":"json_schema","strict":true,"schema":...} —
модель гарантированно даёт валидный JSON по схеме. У китайцев только
json_object — модель даёт какой-то JSON, структуру
валидируем сами через .get() / setdefault()
с дефолтами. Graceful degradation в коде обязательна.
B04·3 · Ночной ревизор на GLM batch
В графе со временем накапливается шум: дубли (две сущности про одну задачу), застрявшие задачи (статус «open», но в чате её закрыли неделю назад без явной формулировки), сущности без человеческой активности (только git-коммиты — это шум).
Каждую ночь в 3:00 запускается ревизор. Сравнивает похожие сущности попарно, спрашивает модель «это одно или разное», мёржит. Пересчитывает статусы для застойных задач. Дропает то, что висит больше 30 дней без человеческой активности.
Раньше работало на большой модели и было медленно: 55 кандидатов на мёрж занимали 7 минут (каждый кандидат — отдельный запрос). Переехали на китайскую модель GLM 5.1 и переписали в batch-режим: 15 пар в одном запросе.
Эффект: те же 55 пар обрабатываются за 4.9 минут вместо 7. Цена упала в 8 раз. GLM кэширует префикс — на повторных запросах cache hit 96%, ещё в 6 раз дешевле. Итого ночной ревизор стоит ~$0.10 за прогон, $3-5 в месяц.
async def _ask_llm_batch(items: list[tuple]) -> list[tuple[bool, str]]:
"""Send N pairs to GLM in one request, get N decisions back."""
blocks = [
f"Пара #{i}: A=<{a.canonical_name}>, B=<{b.canonical_name}>\n"
f"A.desc: {a.description}\nB.desc: {b.description}"
for i, (a, b) in enumerate(items)
]
user_prompt = (
f"Тебе даны {len(items)} пар. Для КАЖДОЙ реши: "
f"same_entity = true/false, и краткая причина.\n\n"
+ "\n\n".join(blocks)
+ '\n\nВерни строго JSON: '
+ '{"results":[{"index":int,"same_entity":bool,"reason":str},...]}'
)
response = await chat_completion(
provider="glm",
model="glm-5.1",
messages=[{"role": "user", "content": user_prompt}],
response_format={"type": "json_object"},
extra_body={"thinking": {"type": "disabled"}},
)
data = json.loads(response.choices[0].message.content)
by_index = {r["index"]: (r["same_entity"], r["reason"])
for r in data.get("results", [])}
return [by_index.get(i, (False, "missing_in_batch"))
for i in range(len(items))]
1. Greedy split на batch'и без пересекающихся entity_id.
Если в одном batch'е будут пары (A↔B) и (A↔C),
и обе LLM решит «same», получим race на последующем merge. Splitter
гарантирует, что в одном batch'е каждый id встречается максимум один раз.
2. Missing indices в batch-ответе обрабатываем явно.
LLM иногда возвращает не все индексы. Без проверки — тихие пропуски.
Логируем: merge batch: LLM returned X/N decisions, missing: [...].
B04·4 · Чат-агент с 12 tools и AI-team awareness
В чате может стоять два AI: основной (Claude Sonnet) и независимый
аудитор (Grok-4.3). Зовут разными словами: @bot или реплай —
отвечает Claude. Слово «грок» или @grok в тексте —
отвечает Grok.
Зачем второй: главное противоречие в работе с LLM — модель почти всегда соглашается. Спрашиваешь «как лучше — A или B?» — она радостно описывает преимущества того, на который ты сам намекнул. Решения принимать так нельзя. Grok-аудитор работает с инструкцией «обязан дать 1-3 контраргумента, согласие без аргументов запрещено».
За последние 30 дней Grok ответил 20 раз — нечастый, но точечный сценарий. Зовём когда нужно второе мнение, не «давай поболтаем».
Два AI в одном чате — отдельная инженерная задача. Без специальных мер
они начинают эхо-комментировать друг друга: бесконечный цикл.
Решение — каждое исходящее сообщение бота получает метку
ai_source (claude / grok / null для человеческих), и в
контекст имя автора подменяется на [CLAUDE] / [GROK].
Каждая модель знает, какие сообщения её, какие чужого AI, какие
человеческие.
System prompt разбит на два блока через Anthropic prompt caching:
SYSTEM_CHAT_BASE = """
Ты — AI-помощник команды. Не автоответчик, а участник:
обсуждаешь идеи, споришь, напоминаешь про незакрытое.
Стиль: живой коллега в чате. Кратко, по сути, на русском.
Инструменты:
• graph_query, list_open, whats_new — для статусов и «что по X»
• search_history — для цитат
• check_commits — git-история проекта
• search_docs, read_project_file, grep_project — документация и код
Когда спрашивают про статусы/прогресс — ОБЯЗАТЕЛЬНО сначала graph_query
или list_open. Контекст последних сообщений всегда неполный.
Формат — HTML для Telegram: <b>, <i>, <code>, <pre>.
Markdown не работает.
"""
def _system_chat_blocks() -> list[dict]:
now = datetime.now(MSK)
return [
{
"type": "text",
"text": SYSTEM_CHAT_BASE,
"cache_control": {"type": "ephemeral"}, # ← кэшируется 5 мин
},
{
"type": "text",
"text": f"Текущее время: {now.strftime('%A, %d.%m.%Y, %H:%M')} МСК.",
},
]
AI-team awareness — в общей утилите bot_outgoing.save_outgoing():
async def save_outgoing(sent: Message, ai_source: str | None, **kwargs):
"""All AI outgoing MUST go through this — Telegram polling
doesn't return our own messages."""
await db.save_message(
message_id=sent.message_id,
chat_id=sent.chat.id,
topic_id=sent.message_thread_id,
text=kwargs.get("text"),
ai_source=ai_source, # 'claude' | 'grok' | None
)
# В контекст-форматтере:
def format_message_for_context(msg):
name = msg.full_name or msg.username or "?"
if msg.ai_source == "claude":
name = "[CLAUDE]"
elif msg.ai_source == "grok":
name = "[GROK]"
return f"{name}: {msg.text}"
KG-экстрактор пропускает AI-сообщения (WHERE ai_source IS NULL) —
иначе граф быстро забьётся сущностями, которые сам же бот и придумал.
B04·5 · Clarifier — бот, который сам спрашивает
KG-экстрактор при создании сущности оценивает: понятно ли из контекста,
что это за задача? Если контекст слабый (артефакт без действия, голая
глагольная фраза, мутная привязка) — ставит флаг needs_clarification.
Раз в день в 11:00 и 18:00 бот забирает до 3 неясных задач старше 4 часов, формулирует короткий вопрос на каждую и шлёт батч одним сообщением. Ответ — обычный реплай. Бот распознаёт reply на свой clarification и парсит через LLM: либо обогащает description сущности, либо закрывает как dropped (если ответ «забей»), либо помечает done. Если 48 часов без ответа — entity автоматически дропается.
Это редкий UX-паттерн: AI сам видит когда не понял, и сам спрашивает, а не молча угадывает.
Большая часть задач быстро уточняется самой командой в чате, не дожидаясь батча. Не нужно делать это первым приоритетом — добавляйте после того как граф стабилизировался и появилась реальная очередь непонятных задач.
CREATE TABLE entity_clarifications (
id BIGSERIAL PRIMARY KEY,
entity_id BIGINT NOT NULL REFERENCES entities(id),
status TEXT NOT NULL, -- pending/asked/answered/timeout
reason TEXT, -- почему KG-экстрактор пометил
question_text TEXT, -- сформулированный ботом вопрос
asked_message_id BIGINT, -- ID сообщения бота с вопросом
asked_at TIMESTAMPTZ,
answered_at TIMESTAMPTZ,
decision TEXT -- clarify/done/drop/skip
);
async def send_batch_clarifications(bot: Bot):
pending = await db.get_pending_clarifications(
min_age_hours=CLARIFY_MIN_AGE_HOURS, # 4
limit=CLARIFY_BATCH_SIZE, # 3
)
if not pending: return
questions = await asyncio.gather(*[
_formulate_question(item) for item in pending
]) # Qwen 3.6 Plus json_object
text = format_clarification_batch(questions, owner=CLARIFY_OWNER)
sent = await bot.send_message(BOT_TOPIC, text, parse_mode="HTML")
await bot_outgoing.save_outgoing(sent, ai_source="claude", embed=False)
await db.mark_clarifications_asked(
[item.entity_id for item in pending],
asked_message_id=sent.message_id,
)
B04·6 · Commit matcher — 89 задач закрыты автоматически
Каждый час бот идёт в git-репозиторий проекта, забирает свежие коммиты
с fix: / bugfix / fix(...) /
почини в subject, и для каждого:
- Делает
git show --stat <sha>чтобы взять полный body + diff stat - Идёт в граф, ищет открытые задачи/баги через semantic search (top-7 кандидатов с cosine > 0.25)
- Один LLM-запрос на коммит против всех N кандидатов сразу — модель возвращает массив решений
- Для каждого
YES— закрывает задачу + создаётevent 'resolved'+relation 'resolved_by' - Для
MAYBE— комментарий с флагом «нужна проверка». ДляNO— пропускает
За время работы — 89 задач автоматически закрылись через коммиты. Это разгружает руки: не нужно каждый раз искать в Telegram «а эта задача закрылась?» и закрывать руками.
Если коммит закрывает известный баг — упомяните симптом из задачи
в body коммита. Commit matcher полагается на semantic
similarity между описанием бага и текстом коммита; при разрыве между
«add missing category» и «HTML-символы в тегах» связь не устанавливается.
B04·7 · Cost-enforcer и health-check
Когда бот сделан из шести разных провайдеров, и каждый считает расход по-разному — нужен один центр контроля. Cost-enforcer перед каждым вызовом дорогостоящих моделей (китайские провайдеры) проверяет суммарный расход за месяц против потолка $30. Если потолок достигнут — бросает исключение.
Это не «потому что больно платить» — потолок ни разу не пробит. Это защита от багов: если кто-то случайно поставит интервал KG-экстрактора 4 секунды вместо 4 минут, потолок сработает раньше, чем счёт на $1000.
Параллельно работает health-check: раз в час проверяет, что всё функционирует. Жив ли KG-экстрактор (за час было ≥5 сообщений, ожидаем ≥1 entity). Отработал ли ночной ревизор. Не превышен ли бюджет. Алёрты с дедупликацией 6 часов на тип — чтобы не получать 100 одинаковых уведомлений.
CHINESE_MONTHLY_CAP_USD = float(os.getenv("CHINESE_MONTHLY_CAP_USD", "30"))
@cached_with_ttl(ttl_seconds=60)
async def _current_month_total_usd() -> Decimal:
"""Cached 60s — don't hit DB on every LLM call."""
row = await pg.fetchrow("""
SELECT COALESCE(SUM(cost_usd), 0) AS total
FROM ai_usage_daily
WHERE day >= date_trunc('month', CURRENT_DATE)
AND provider IN ('deepseek', 'qwen', 'glm', 'openrouter')
""")
return Decimal(str(row["total"] or 0))
async def assert_within_cap(provider: str) -> None:
if not is_chinese(provider):
return
total = await _current_month_total_usd()
if total >= Decimal(str(CHINESE_MONTHLY_CAP_USD)):
raise CostCapExceeded(
f"Monthly cap ${CHINESE_MONTHLY_CAP_USD} reached: ${total}"
)
Без неё первый раз когда KG-экстрактор «застрянет» — получите 24 одинаковых сообщения в bot-топик за сутки. Окно дедупа должно быть кратное частоте проверки + 1 (проверяем раз в час, окно 6 часов).
B04·8 · Эволюция $82 → $8
Месяц назад бот стоил $82/мес. Сегодня — $8. Снижение в 10 раз за одну неделю миграции.
| Что было | Что стало | Экономия |
|---|---|---|
| GPT-5.4-mini экстрактор | Qwen 3.6 Plus free quota | $30 → $0 |
| Большая модель polish дайджеста | SQL + HTML, без LLM | $2.1 → $0 |
| GPT-5.4 batch merge | GLM 5.1 batch | $25 → $3 |
| Bug Triage Sonnet | выключено | $111 → $0 |
| Чат + Vision (остался) | Claude Sonnet 4.6 + Haiku | $4.5 → $2 |
| Итого было / стало | $82 → $8/мес | |
Три ключевых хода:
- Bug Triage отключён — стоил $111/мес сам по себе, никто не читал отчёты (подробнее в B05·1)
- KG-инфраструктура переехала на Qwen 3.6 Plus — у DashScope international жирные free quotas, на нашем объёме $0
- Ночной ревизор переписан в batch — 15 пар на запрос вместо одной, GLM 5.1 на месте GPT-5.4
11 ground-truth пар «коммит ↔ баг», прогнали 5 топ-моделей. Точность на YES-truth + MAYBE-truth парах:
| Модель + режим | Точность | Latency | Cost/тест |
|---|---|---|---|
| GLM 5.1 без thinking | 3/3 | 2.8 с | $0.005 |
| GLM 5.1 thinking | 3/3 | 9.7 с | $0.015 |
| Sonnet 4.6 thinking | 2/3 | 4.4 с | $0.042 |
| Kimi K2.6 thinking | 2/3 | 18.5 с | $0.055 |
| DeepSeek V4 Pro thinking | 1/3 | 12.1 с | $0.005 |
GLM 5.1 без thinking — единственная модель с 3/3 точности при latency 2.8 с и $0.10/мес на нашем объёме. Заменили DeepSeek V4 Pro thinking (1/3, 12.1 с) — был выбран первым по интуиции, оказался хуже после замера.
- Qwen DashScope требует слово «json» в одном из messages для
response_format=json_object - Qwen 3.6 — reasoning by default,
enable_thinking=Falseобязательно - Strict
json_schemaне поддерживается у Qwen/DeepSeek/GLM — толькоjson_object - DashScope China endpoint ≠ international, ключи не взаимозаменяемы
- MiniMax без
json_objectвозвращает пустой content - OpenRouter Kimi K2.6 —
reasoning.exclude=Trueне отключает thinking. Нужен direct Moonshot API - DeepSeek V4 — toggle thinking через
extra_body={"thinking":{"type":"disabled"}} - DeepSeek V4 — 75% скидка до 31.05.2026, после list price ×4 дороже. TODO на пересчёт цен в коде учёта расходов перед концом мая
B05·1 · Что мы выпилили
Самая ценная часть опыта — не «что построили», а «что построили и потом удалили». AI-фичи плодятся легко, выпиливать сложно.
Случай 1: Bug Triage отчёты, которые никто не читал
Специальный агент: каждые 5 минут проверял новые баги, делал multi-round анализ с tools (grep_project, read_project_file, graph_query, check_commits). Писал в чат структурированный отчёт типа «#1834 онбординг — severity med, repr=true, suspected files: ..., similar past bugs: ...».
Стоил $111/мес — фича в одиночку в 14 раз дороже, чем сейчас весь бот. Никто не читал. Технические детали бесполезны нечитающему код. Чат-агент Claude при ремонте сам грепает код, в БД не лезет.
Триаж писал в дыру. Отключили. Код сохранили — вернуть можно когда появится потребитель.
Случай 2: Polish утреннего дайджеста большой моделью
Дайджест собирался ночью в JSONB-черновик, утром модель делала из него связный текст. Стоил $0.07/день — мелочь. Но был эффект галлюцинаций числами: «закрыли 5 задач» когда было 2. Команда перестала доверять цифрам.
Переписали в детерминированный SQL → 4 фиксированных HTML-блока. Без LLM. Числа гарантированно из БД. Экономия копейки, но надёжность важнее.
Случай 3: KG-дашборд, который был шумом
В 9:30 отдельным сообщением: «KG-стата за вчера. Создано: 12 entities, 8 событий, 4 связи. Распределение типов: ...». Инфра-телеметрия для разработчика, не помощь команде.
Job снят. Расходы переехали в утренний дайджест блоком. Остальная статистика заменена на алёрты health-check.
Случай 4: Локальная Ollama + Gemma 4 26b MoE
Подняли локально для AI-фильтра ложных тревог в соседнем проекте. Замеры через сутки:
- Точность 12/12 — совпадала с DeepSeek V4 Flash через API
- Latency: Gemma на CPU — 5-12 секунд. DeepSeek API — 1-1.5 секунды
- Память: Gemma — 23.7 GiB always-on RAM. DeepSeek — 0
- Стоимость: DeepSeek API — <$0.10/мес на нашем объёме
Снесли через сутки. Освободили 23.7 GiB. DeepSeek API за <$0.10/мес даёт лучше всё.
Фича запускалась «потому что выглядело круто». Прошло время — никто не читает выход / результат хуже простой альтернативы / стоимость не оправдывает пользу.
Самое дорогое в AI-инфраструктуре — не токены, а поддержка ненужного. Каждая активная фича — код для обновления при миграциях, промпт для итераций, расход для мониторинга, внимание на её выход.
Выпилить — лучшее, что можно сделать с фичей, которая не работает. Не «улучшить», не «настроить лучше», а удалить.
B05·2 · Out of scope и триггеры активации
В ROADMAP.md проекта зафиксирован список «что решено
не делать». Это не «когда-нибудь дойдём», это не дойдём никогда,
пока не появится радикально новая причина пересмотреть.
| Чего НЕ берём | Почему |
|---|---|
| LangChain / LangGraph / CrewAI | Обёртки поверх простых SDK, потеря контроля |
| Neo4j / Qdrant | Наш масштаб (2K entities) pgvector закрывает |
| LiteLLM | 113 строк ai_providers.py делают то же |
| Pydantic везде | Пока не прижал второй раз баг из-за опечатки в dict-ключе |
| Docker / Kubernetes | 1 сервис на 1 машине |
| CI/CD GitHub Actions | Тесты прогоняются руками, code review через AI |
| Pytest везде | Регрессии не повторяются — пока |
| Sentry | Жалоба «бот тупит» решается в journalctl за 5-10 минут |
Параллельный список — триггеры активации:
| Инструмент | Триггер активации |
|---|---|
| Автобэкап БД | 2 месяца без инцидентов ИЛИ потеря данных ИЛИ 2-й разработчик |
| Sentry | Жалоба «бот тупит» + не нашли причину за 10 мин в journalctl |
| Pydantic везде | 2-й баг из-за опечатки в dict-ключе |
| Pytest | Одна регрессия вернулась дважды |
| Docker | 2-й сервис или deploy на другой сервер |
| Prometheus/Grafana | Нужно ответить «медленнее ли стал бот» |
| MCP-server | 3+ агентов нуждаются в одних и тех же tools |
| LiteLLM | А/Б тестирование моделей или частый swap |
Принципы, которые поддерживают этот подход:
- Продукт > инфра. Приоритет — функциональность, не идеальный фундамент
- Новая зависимость — только под конкретную текущую боль. Не «на будущее», не «для гибкости»
- Открытые стандарты > vendor lock-in. MCP, JSON Schema, OpenAPI — когда придёт их время
- Каждый модуль обратим. Можно выключить фичу без ломки остального
- Метрики, а не сроки. Переход между фазами по готовности
- Если решение меняется в пределах недели — красный флаг. Возвращаемся к критериям
Файл ROADMAP.md читается перед каждым крупным решением.
Без этого происходит: «давайте на всякий случай поставим Sentry /
напишем тесты / переедем в Docker». Каждое по отдельности обоснованно,
но в сумме они съедают команду.
Out of scope — не лень. Это бюджет внимания.
B05·3 · Как применить к своему проекту
Главные уроки.
- Граф, не RAG. Сообщения → entities → отдельный pipeline для дедупа, апдейта статусов, истории
- Bi-temporal модель — сразу. Поля
valid_from,valid_until,recorded_atс первого дня - Multi-signal linker важнее экстрактора. Дедуп решает живучесть графа. Multi-signal даёт меньше 1% дублей
- Cost-enforcer до миграции на дешёвые модели, не после. На API китайцев счётчик надо вести самому
- Замеряй кто реально пользуется. Каждые 30 дней спрашивай по каждой AI-фиче: «если выключить — кто заметит?»
- Скучные процессы — без LLM. Утренний дайджест, расход за вчера, статусы — это SQL, не LLM
- Триггеры, не календарь. Список «что не делать» с триггерами активации работает в разы лучше «возможно когда-нибудь»
B05·4 · Сколько это реально занимает
В одном из ревью этой статьи предложили раздел «MVP за выходные» с чек-листом из 7 шагов: aiogram + pgvector + один промпт + одна таблица + дедуп по имени → первая ценность за 2-3 вечера. Это преуменьшение.
Запустить первую версию (просто save сообщений в БД + семантический поиск по @упоминанию) — действительно 1-2 дня работы.
Превратить это в рабочую систему — два месяца периодической работы. Возвращался, дорабатывал, иногда. Не full-time, не каждый день. Но два месяца от первого save до текущего состояния: граф 2199 сущностей, ночной ревизор, clarifier, два AI с awareness, миграция с GPT, бенчмарки 9 моделей.
Что съело это время:
- Итерации промптов KG-экстрактора. Первая версия выдавала по 5 «сущностей» на каждое сообщение — модель видела в любом упоминании задачу. Пришлось писать рубрику что считать сущностью
- Multi-signal linker появился не сразу. Первая версия дедупа была по exact match имени. За неделю получили 50% дублей. Переписали в multi-signal, пересобрали граф с нуля — отдельный спринт на 2-3 дня
- Полный ресет графа. К одному моменту в графе накопилось 948 entities, из них 11 пар явных дублей и ~50% «мусорных». Сделали
TRUNCATE entities CASCADE, обновили линкер, пересобрали с нуля 1079 сообщений и 194 git-коммита — полный день работы - Миграция с GPT на китайские API. Одна сессия 10 часов: 3 фазы swap'а, отлов подводных камней DashScope/Qwen/GLM, бенчмарк 9 моделей, batch-refactor
- Bug Triage прожил месяц от запуска до выпиливания. За это время сжёг ~$100
Это не «вечер кодинга». Это систематические возвраты с разными вопросами: «почему граф разъезжается», «почему дайджест врёт», «куда уходят деньги», «что выпилить».
B05·5 · Провалы
Не всё работало с первого раза.
Граф разъехался на дублях за первую неделю
Первый дедуп был по exact-match имени. Сразу же 50% дублей: модель писала «онбординг для преподавателей» и «онбординг преподавателей» как две разные сущности. Решение — multi-signal linker (4 сигнала + LLM-disambig в серой зоне). Пришлось два раза пересобрать граф с нуля.
Qwen 3.6 жгла бюджет на тривиальном «PONG»
Smoke-тест после переключения KG-экстрактора на Qwen 3.6 Plus: prompt
«Reply: PONG» давал completion 213 токенов. С enable_thinking=False —
2 токена. Без этой настройки cost-enforcer был бы пробит за пару дней.
MiniMax возвращала пустой content без json_object
Первая попытка использовать MiniMax M1 для kg_linker disambig — модель
отвечала text=null на короткие prompt'ы. С
response_format=json_object — нормальный JSON. Добавили
smoke-тест на каждого нового провайдера.
DeepSeek V4 цены недосчитал в 2×
В коде учёта расходов поставил цены $0.27/$1.10 per 1M
по аналогии с V3. Реально оказалось $0.435/$0.87 со скидкой
и $1.74/$3.48 после 31.05.2026. Cost-enforcer недосчитывал
в 2× прямо сейчас и в 4-5× после конца мая.
Замена grok-4.20 на grok-4.3 за 7 дней до retire
grok-4-1-fast-reasoning уходит 15.05.2026 — через 7 дней.
Прогнали robust-бенчмарк grok-4.3 — 99 вызовов на 11 кейсов × 3 efforts ×
3 sample. Открыли что reasoning_effort="none" у Grok-4.3 —
единственное реально-выключающее значение. Перекинули три pipeline,
cache-render на Qwen 3.6 Flash. Сэкономили в 1.6-2.4× на стоимости,
не сломались 15 мая.
Модели редко документируют свою реальную работу полно. Reasoning toggles, json modes, страницы прайсинга меняются. Smoke-тест каждого провайдера на минимальном prompt'е перед production — обязательно. Прайсинг — TODO на каждой временной скидке.
Если хотите завести у себя похожий список «out of scope» с триггерами активации — скажите своему AI-агенту в CLI:
Промпт «составь Out of Scope для своего проекта»
Прочитай README, ROADMAP.md и текущий код проекта.
Составь список «Что мы НЕ делаем»: инструменты, фичи, переезды,
которые часто хочется добавить «на всякий случай», но они не
закрывают конкретную текущую боль. Для каждого пункта:
1. Что отказываемся брать (название инструмента или фичи)
2. Почему сейчас не нужно (1-2 строки, конкретно)
3. Триггер активации, какое СОБЫТИЕ должно произойти, чтобы
пересмотреть решение и добавить это в проект
Триггеры должны быть конкретные и измеримые: «жалоба не решена
за 10 минут», «второй разработчик», «потеря данных»,
«регрессия повторилась дважды». Не «когда-нибудь», не «через год»,
не «если будет нужно».
Сформулируй 8-12 пунктов на основе моего проекта. Сохрани в
ROADMAP.md в раздел «Что решено НЕ делать». Этот файл я буду
читать перед каждым крупным архитектурным решением.
Адаптируйте под свой стек. После 2-3 итераций списка получите рабочий «бюджет внимания» — документ, который держит проект в рамках.
Бот в чате — не новый чат-бот и не RAG-поиск. Это структурированная память (граф сущностей) плюс проактивные процессы вокруг неё. Без структуры — просто поиск по чату, есть и без бота. С графом + процессами — третий участник работы с памятью на полгода назад.
Главная цена — не токены, а поддержка ненужного. Каждые 30 дней спрашивай по каждой фиче: «если выключить — кто заметит?».
// Обсуждение
Можно писать анонимно. Укажите email, чтобы получать уведомления об ответах.