Полгода назад я начал писать веб-лист персонажа для D&D 5e. Сейчас это D1MANYCH/dnd-app: ванильный JS без сборщика, 44 модуля, ~70 000 строк, 597 коммитов, версия 3.91.0. Код в нём почти целиком написан Claude Code — я держу архитектуру, ревью и решения по контенту. Эту статью модель тоже вычитала. Раз уж за такое иногда прилетает, говорю сразу, в первом абзаце.
Проблема, ради которой всё затевалось: лимиты подписки заканчивались к обеду. Не потому, что я много просил, а потому, что просил неправильно. Ниже — как я это измерил, что нашёл и что поменял.
Сначала замер, потом выводы
Ощущения тут бесполезны: «кажется, много читаю» — не диагноз. У Claude Code все сессии лежат в ~/.claude/projects/**/*.jsonl, и в каждой строке ассистента есть message.usage с реальным биллингом. Тридцать строк на Node дают полную картину:
// usage.js — расход по всем транскриптам: node usage.js const fs = require('fs'), path = require('path'); const root = path.join(require('os').homedir(), '.claude', 'projects'); const files = []; (function walk(d) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); if (e.isDirectory()) walk(p); else if (e.name.endsWith('.jsonl')) files.push(p); } })(root); let req = 0, read = 0, write = 0, out = 0, sessions = 0; const bySession = []; for (const f of files) { let n = 0, r = 0; for (const line of fs.readFileSync(f, 'utf8').split('\n')) { if (!line.includes('"cache_read_input_tokens"')) continue; if (line.includes('"isSidechain":true')) continue; // сабагенты — отдельный контекст let u; try { u = JSON.parse(line).message.usage; } catch (e) { continue; } if (!u) continue; n++; r += u.cache_read_input_tokens || 0; read += u.cache_read_input_tokens || 0; write += u.cache_creation_input_tokens || 0; out += u.output_tokens || 0; } if (n) { sessions++; req += n; bySession.push({ n, r }); } } bySession.sort((a, b) => b.n - a.n); const long = bySession.filter(s => s.n > 300); console.log('сессий:', sessions, '| запросов:', req); console.log('кеш-чтение:', (read / 1e9).toFixed(2) + 'B', '| вывод:', (out / 1e9).toFixed(2) + 'B'); console.log('средний контекст запроса:', Math.round(read / req / 1000) + 'k'); console.log('сессий >300 запросов:', long.length, '| их доля расхода:', Math.round(long.reduce((a, s) => a + s.r, 0) / read * 100) + '%');
Что он мне выдал:
Метрика | Значение |
|---|---|
Сессий / запросов к модели | 169 / 28 649 |
Чтение из кеша | 5,75 млрд токенов |
Из них на этот проект | 3,64 млрд (63%) |
Запись в кеш | 0,20 млрд |
Выход (ответы модели) | 0,03 млрд |
Средний контекст одного запроса | 201k |
Сессий длиннее 300 запросов | 22 штуки — 67% всего расхода |
Самая длинная сессия | 886 запросов |
Если выложить сессии в ряд, отсортировав по расходу, картина становится неприличной:

Хвост из полутора сотен нормальных сессий — треть расхода. Слева — два десятка марафонов, которые съели остальное.
Первая же строчка таблицы отменяет половину советов из интернета. Выход — 0,03B против 5,75B входа, это полпроцента. Всё, что можно выгадать, укорачивая свои промпты и прося «отвечать кратко», лежит в этих полпроцента. Деньги — во входе, то есть в том, что тащится в каждый запрос.
Почему длинная сессия дороже нескольких коротких
Механика простая, но неинтуитивная. Каждый запрос к модели отправляет весь диалог заново: системный промпт, CLAUDE.md, все прочитанные файлы, все результаты команд, всю переписку. Prompt caching делает это дешевле, но не бесплатно — вы платите за чтение кеша, и платите за него на каждом шагу.
Контекст при этом только растёт. Если считать грубо, что каждый вызов добавляет d токенов к контексту S на старте, стоимость сессии из N запросов — это площадь под прямой:
Стоимость ≈ N·S + d·N²/2
Квадрат — вот вся суть. Сессия на 800 запросов стоит не вдвое, а вчетверо дороже четырёх сессий по 200 при том же объёме сделанной работы. Мои 22 «марафонские» сессии съели две трети всего, что я потратил, — ровно поэтому.
Второе следствие того же неравенства — слагаемое N·S. Стартовый контекст умножается на количество запросов. Каждая лишняя тысяча токенов в CLAUDE.md — это тысяча, умноженная на 150 запросов сессии, умноженная на все сессии. Мой CLAUDE.md ужат до 74 строк и написан по-английски (на 15–20% короче того же текста по-русски), при том что весь проект, UI, коммиты и чат — русские.
Куда конкретно уходил контекст
Отдельным проходом я разложил накопленный контекст по источникам:
Источник | Доля |
|---|---|
Стартовый контекст × число запросов | 39% |
Результаты инструментов | 32% |
Сама переписка | 28% |
Внутри инструментов: чтение файлов — 43%, команды оболочки — 25%, картинки — 13%. Дальше пошли неприятные подробности.
32% всего объёма чтения — повторные чтения одного и того же файла в одной сессии. style.css (647 КБ, 16 123 строки) был прочитан 266 раз. Модель читает файл, что-то делает, через двадцать шагов «на всякий случай» читает снова — хотя первое чтение никуда не делось, оно лежит в контексте и оплачивается на каждом запросе. За второй Read я плачу дважды: за сам факт и за то, что он теперь навсегда в этой сессии.
459 скриншотов, около 130 миллионов токенов. Я проверял вёрстку через браузерное превью прямо в основном чате. Один скриншот — это единицы тысяч токенов, но он остаётся в контексте до конца сессии и едет в каждый следующий запрос.
Что я поменял
1. Жёсткий лимит сессии и Stop-хук, который о нём напоминает
Правило: 150 запросов или 200k контекста — и сессию режем, даже посреди задачи. Соблюдать это на глазок нельзя, поэтому счётчик повесил на Stop-хук — он срабатывает, когда модель закончила ход:
// tools/check-session-size-hook.js (сокращённо) const WARN_REQ = 120, HARD_REQ = 150, WARN_CTX = 150000, HARD_CTX = 200000; const lines = fs.readFileSync(input.transcript_path, 'utf8').split('\n'); let req = 0; for (const l of lines) { if (!l.includes('"cache_read_input_tokens"')) continue; if (l.includes('"isSidechain":true')) continue; // сабагенты не влияют на цену главной сессии req++; } // контекст — из последней строки с usage const u = JSON.parse(last).message.usage; const ctx = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0); if (req >= HARD_REQ || ctx >= HARD_CTX) console.error('⛔ Сессия разрослась: ' + req + ' запросов, контекст ' + Math.round(ctx / 1000) + 'k. Закрыть: /carry → /clear.');
Хук ничего не блокирует (exit 0), просто печатает строку на пороге. Этого достаточно: раньше я не знал, что сижу в 600-запросной сессии, теперь узнаю на 120-й.
2. /carry — ручная передача смены вместо автосуммаризации
Автоматическое сжатие контекста спасает от переполнения, но это не экономия: до момента сжатия вы уже оплатили всю дорогу. Поэтому у меня своя слэш-команда /carry: модель пишет пять строк — что сделано, в каком состоянии файлы, следующий шаг, открытые вопросы, — я делаю /clear и начинаю новую сессию с этих пяти строк вместо ста тысяч токенов истории.
Это же лечит и другую болезнь. Работа нарезана на фазы, каждая начинается с «начать фазу X-N» и заканчивается /carry. Одна задача — одна сессия.
3. Карта кода вместо разведки грепом
Классический сценарий: «поправь отступ в шапке» → три Grep, чтение 400 строк не туда, потом ещё 400 туда. Всё это остаётся в контексте до конца сессии.
Решается генератором: node tools/gen-map.js строит docs/map.md — 259 строк, где перечислены секции style.css с диапазонами строк, блоки верхнего уровня index.html, индекс функций по файлам и индекс констант в файлах данных. Правило в CLAUDE.md: перед правкой большого файла сначала оглавление карты (первые 20 строк), потом нужный раздел карты, потом чтение файла с точным offset/limit. Вместо тысячи строк разведки — сорок.
Это стоит делать, только если в проекте есть монстры вроде моих: style.css 647 КБ, data.js 610 КБ, spells.js 562 КБ, build-notes-data.js 545 КБ. На проекте из аккуратных файлов по 300 строк карта не окупится.
4. Прямой запрет на повторное чтение
Формулировка в CLAUDE.md: «Никогда не читай один файл дважды за сессию. Прочитанное всё ещё в контексте — прокрути назад. Если файл изменился, читай только изменённый диапазон». Звучит как очевидность, но без явного запрета модель перечитывает — ей так надёжнее, а цену этой надёжности видит только владелец аккаунта.
5. Скриншоты и браузер — только в сабагенте
Ключевое свойство сабагента: у него свой контекст, а в главную сессию возвращается только финальный ответ. Вся визуальная проверка ушла в отдельного агента verifier — он поднимает превью, кликает, читает консоль и сеть, делает скриншоты, а мне отдаёт пять строк вердикта. 459 картинок больше не оседают в главном треде.
Та же логика для широкого поиска по репозиторию: пусть агент прочитает двадцать файлов у себя и вернёт «вот эти три места».
6. Рутину — дешёвым моделям
Основной тред я веду на Opus, но релизная рутина в Opus не нуждается. Модель сабагента задаётся во фронтматтере .claude/agents/*.md:
Агент | Модель | Что делает |
|---|---|---|
| Sonnet | тесты → бамп версии → проверка инвариантов → коммит → пуш → ожидание CI |
| Haiku | собирает текст анонса релиза |
| Sonnet | добавляет игровой контент по готовому шаблону |
| Sonnet | проверка UI в браузере |
Планирование, архитектура, разбор багов остаются в главном чате на старшей модели. Смысл разделения в том, что механическая работа по написанной инструкции не становится лучше от более умной модели, а стоит заметно дороже.
7. Батчинг вызовов и фон вместо polling
Цена растёт с количеством запросов, а не с количеством слов в них. Значит, независимые действия надо складывать в один ход: три чтения и два грепа в одном сообщении — это один запрос вместо пяти, и пять раз по 200k превращаются в 200k один раз.
Из того же: длинные команды — в фон, и ни в коем случае не «поспать и проверить». Каждый цикл «sleep → проверил → ещё не готово» — это полноценный оплаченный запрос с полным контекстом.
8. Детерминированные проверки — хуками, а не моделью
Всё, что можно проверить программой, проверяет программа, а не модель, которая для этого читает файлы. На PostToolUse у меня висят пять хуков: два блокирующих (exit 2) — «правил sw.js, но не бампнул CACHE_NAME» и рассинхрон версии с чейнджлогом, плюс node --check на любой изменённый JS; два предупреждающих — тесты и контраст темы.
Хук стоит ноль токенов и ловит ошибку в ту же секунду. Тот же класс проблем, пойманный моделью через три хода, стоит трёх полных контекстов.
Что не сработало
Сабагент ради одной команды. Агент стартует холодным: заново читает CLAUDE.md, заново ищет файлы. На короткой задаче это дороже, чем сделать самому. Сабагент окупается там, где он прочитает много, а вернёт мало.
Локальные модели как замена сабагентам. У меня RTX 4060 Ti на 8 ГБ — влезает 7–8B в четырёхбитном кванте. Для рутины в реальном коде этого мало, а Claude Code к локальным моделям и не подключается.
Просить «отвечай короче». Выход — 0,5% расхода. Экономить на нём — это торговаться за копейки, пока в соседней комнате горит проводка.
Чеклист
Приём | Что лечит |
|---|---|
Лимит 150 запросов / 200k + | квадратичный рост стоимости сессии |
| оплату всей истории до момента сжатия |
Короткий | стартовый контекст × число запросов |
Карта кода с диапазонами строк | разведочные |
Запрет повторного чтения | 32% объёма чтения |
Браузер и скриншоты — в сабагента | картинки, оседающие в главном треде |
Рутина на Sonnet/Haiku | переплату за механику |
Батчинг независимых вызовов, фон вместо polling | лишние запросы, каждый с полным контекстом |
Проверки хуками, а не моделью | циклы «ошибся — прочитал — исправил» |
Честный финал
Цифр «после» у меня пока нет: правила и хуки введены недавно, а сравнивать надо на дистанции в пару недель работы, иначе намеряешь шум. Перезамерю тем же скриптом; цель — средний контекст запроса ниже 150k и полное отсутствие сессий длиннее 300 запросов.
Но одну вещь можно сказать, не дожидаясь замера. Работа с кодовым агентом — это не «попросил и получил». Это управление одним-единственным ресурсом: тем, что модель тащит с собой на каждом шагу. Всё остальное — производные.
Если у вас есть свои приёмы — особенно если вы дошли до цифр, а не ощущений, — напишите в комментариях, интересно сравнить методики замера.

