eugenemind.com
← Назад к блогу

29 августа 2026 г. · 10 мин. чтения

Эксперименты с подсчётом токенов и их оптимизацией

#codex #claude-code #coding-agents #context-engineering #token-optimization #agents-md

Я довольно активно использую coding agents в повседневной разработке, и в какой-то момент из-за ограничений и лимитов мне стало интересно, насколько эффективно я вообще расходую токены.

Общий usage был очень большим, но сама по себе эта цифра мало что объясняла. Мне хотелось разобраться хотя бы на своём примере: на что уходит основная часть токенов? На мои prompts? На AGENTS.md и другие инструкции? На длинные сессии? Или на работу агента после того, как я уже отправил запрос?

Сейчас для этого есть намного больше готовых инструментов. Для Codex, например, есть ccusage, он читает локальные Codex session logs и умеет показывать usage по дням, месяцам и отдельным сессиям, включая cached tokens. Поддержка Codex пока отмечена как beta, поскольку формат логов Codex CLI продолжает меняться.

Для Claude Code тот же ccusage умеет читать локальные Claude logs и строить daily, weekly, monthly и session reports. Сам Claude Code также даёт больше готовых средств для работы именно с контекстом: /context, compaction, CLAUDE.md, skills и subagents.

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

Самописный tracker, не главная идея этой статьи. Сейчас для обычного подсчёта usage я бы скорее взял готовый инструмент. Интереснее оказалось то, что показали собранные данные.

До замеров я подозревал в первую очередь собственные prompts. Я часто довольно подробно описываю задачу. Кроме этого есть AGENTS.md, постоянные инструкции и контекст, оставшийся от предыдущих шагов. Всё это на первый взгляд выглядит дорогим.

Но после prompt агент тоже начинает собирать дополнительный контекст. Он делает grep, читает файлы, ищет usages, запускает build или тесты, получает логи. Сколько текста в итоге появляется на этой стороне, я до замеров не представлял.

Как я собирал статистику

Основной набор данных у меня получился из локальных Codex session logs. Мне хотелось не просто получить total usage, а иметь возможность посмотреть на отдельные turns и связать их с тем, что происходило в сессии.

Я сделал небольшой hook, который перед новым turn сохранял локальный snapshot: turn_id, текущий repository, hash prompt, несколько дополнительных полей и, при необходимости, raw payload.

Codex session

hook

capture metadata

Codex session logs

token_audit.py report

report.json / report.md

Сам hook я специально оставил максимально простым:

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
python3 "$SCRIPT_DIR/token_audit.py" capture \
  --repo-root "${PWD}" \
  --store-raw-payload

Я довольно быстро отказался от идеи считать что-то серьёзное прямо внутри hook. Удобнее было просто сохранить состояние, а потом отдельно строить отчёт по уже накопленным данным.

Упрощённый вариант capture выглядел примерно так:

import hashlib
import json
import sys
from datetime import datetime, timezone
from pathlib import Path

raw = sys.stdin.read()
payload = json.loads(raw) if raw.strip() else {}
prompt = payload.get("prompt", "")
repo_root = Path(payload.get("cwd") or ".").resolve()

record = {
    "captured_at": datetime.now(timezone.utc).isoformat(),
    "turn_id": payload.get("turn_id"),
    "repo_root": str(repo_root),
    "prompt_preview": prompt[:240],
    "prompt_hash": hashlib.sha1(prompt.encode("utf-8")).hexdigest() if prompt else "",
}

audit_dir = repo_root / ".token-audit"
audit_dir.mkdir(exist_ok=True)
with (audit_dir / "hook-captures.jsonl").open("a", encoding="utf-8") as handle:
    handle.write(json.dumps(record, ensure_ascii=False) + "\n")

В реальном скрипте полей было немного больше: branch, размер repo instructions, найденные рядом handoff-файлы и raw payload. Принцип оставался тем же: hook фиксирует состояние turn, а отдельный report уже работает с логами.

python3 ./tools/token_audit.py report --repo-root "$PWD"
.token-audit/
├── hook-captures.jsonl
├── raw-hook-payloads/
└── latest-report/
    ├── report.json
    └── report.md

report проходил по session logs, сопоставлял их с captures и раскладывал fresh input на user prompts, developer instructions, repo bootstrap, tool call arguments, tool outputs и assistant loopback. Cached input я считал отдельно.

Это не production-grade observability и не система биллинга. Формат логов может меняться, часть классификации зависит от конкретного клиента, сам скрипт нужно поддерживать. Для моей задачи этого было достаточно.

В итоге набралось 5 420 turns за 120 дней использования.

Что получилось

Источник Fresh input


Tool outputs 67,4% Assistant loopback 17,9% Tool call arguments 8,3% User prompts 4,6% Developer instructions 1,43% Repo bootstrap 0,34%

Я ожидал увидеть другое. На мои prompts пришлось всего 4,6%. Даже гипотетическое сокращение каждого prompt в два раза не особенно меняло общую картину.

При этом 67,4% fresh input приходилось на tool outputs.

В обычной сессии агент постоянно получает обратно текст. Он открыл файл, получил текст. Сделал grep, получил результаты. Запустил тесты, получил ещё один кусок текста. Иногда небольшой, иногда очень большой. До замеров я воспринимал это скорее как внутреннюю работу агента, хотя весь этот текст тоже попадает в контекст и часть его остаётся там в следующих turns.

Обычный grep тоже имеет цену

Особенно заметно это было на больших рефакторингах и модуляризации. Когда файлы перемещаются между модулями и меняются зависимости, fresh agent вполне логично начинает с rg "Checkout", затем ищет protocol, implementation, usages, открывает router, view model и соседний модуль.

Всё это нормальные действия. Я бы и сам примерно так исследовал незнакомый участок кода. Только для агента результаты этих поисков ещё и становятся частью контекста.

Я попробовал убрать часть повторяющегося исследования с помощью небольшой карты модулей. Не документации всего проекта, а короткого файла с ориентирами:

## Checkout

Responsibility:
Checkout owns the flow from cart confirmation
to the final order result.

Entry points:
- CheckoutView.swift
- CheckoutViewModel.swift
- CheckoutRouter.swift

Navigation:
- CheckoutRouter.swift
  Owns checkout navigation and creates child flows.

State:
- CheckoutViewModel.swift
  Handles UI actions and checkout state updates.

Payment:
- RetryPaymentAction.swift
  Retry entry point after a failed payment.
- PaymentGatewayClient.swift
  Payment API only. Does not own navigation.

Boundaries:
- CheckoutRouter owns navigation.
- Payment module owns payment implementation.
- Cart owns price calculation.

If payment retry is broken:
1. Check RetryPaymentAction.
2. Check state update in CheckoutViewModel.
3. Check routing in CheckoutRouter.
4. Only then search the whole Checkout module.

Конкретные имена здесь условные. Такой файл не должен объяснять агенту каждый класс. Он просто отвечает на несколько вопросов до широкого поиска: где entry point, кто отвечает за navigation, где находится state и куда смотреть первым.

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

С AGENTS.md получилось немного иначе

До замеров я был почти уверен, что заметную часть токенов съедает AGENTS.md: его содержимое попадает в контекст каждой сессии.

Matt Pocock в материале про AGENTS.md советует относиться к нему как к brief, а не как к документации: держать коротким и декларативным, а специализированную информацию раскрывать только тогда, когда она нужна.

Но в моих данных:

developer instructions, 1,43%

repo bootstrap, 0,34%

Итого меньше двух процентов fresh input. Это не означает, что AGENTS.md можно делать любого размера. Для меня результат означал, что не было большого смысла начинать оптимизацию именно оттуда, когда рядом было 67,4% tool outputs.

Тем не менее файл я позже почистил. Инструкции, которые нужны только для отдельных типов задач, я частично перенёс в skills.

У Matt Pocock есть отдельный материал Writing for Agents и набор AI Skills for Real Engineers. Часть этого подхода нормально переносится между Claude и Codex.

Мой критерий для AGENTS.md стал простым: не «можно ли удалить ещё десять строк», а «эта информация действительно нужна в каждой сессии?».

С длинными сессиями всё было предсказуемее

Сессия постепенно накапливает историю: поиски, гипотезы, ошибки, предыдущие варианты и tool outputs. В моменте всё это было нужно, но после решения конкретной части задачи большая часть истории уже может не помогать.

Для длинных задач у меня был context.md. Сначала я превратил его в журнал:

## Session 1
Checked payment flow.
Looked at CheckoutViewModel and PaymentService.
First assumption was wrong because...

## Session 2
Moved implementation.
Found another issue in...

В итоге новая сессия снова получала большую часть старого контекста, только уже в другом формате.

Поэтому context.md я стал не дописывать, а пересобирать:

## Current state
Payment retry works, but checkout confirmation
is not refreshed after successful retry.

## Decisions
- Retry logic stays in Checkout.
- Payment module only performs payment operations.
- Navigation stays in CheckoutRouter.

## Important
Refreshing state directly from the view caused
duplicate requests. Do not repeat this approach.

## Relevant files
- CheckoutViewModel.swift
- CheckoutRouter.swift
- RetryPaymentAction.swift

## Next
Trace state update after successful RetryPaymentAction.

Такого текста новой сессии обычно достаточно. Если агент уже пошёл по очевидному, но неправильному пути и есть риск повторить его, я оставляю короткую заметку, но не переношу весь процесс исследования.

Позже я увидел практически ту же идею у Matt Pocock в его /handoff skill. Matt Pocock использует хорошую формулировку: portability, not compression. Задача handoff, передать другому агенту достаточно информации, чтобы продолжить работу, а не сохранить всю предыдущую сессию.

Когда проще начать новую сессию

После этого я стал чаще начинать fresh sessions. После завершения отдельного этапа обычно нужна только небольшая часть накопленного контекста.

research
→ root cause found
→ implementation
→ validation

Перед implementation часто достаточно такого:

Root cause is in RetryPaymentAction.
State must be updated through CheckoutViewModel.
CheckoutRouter owns navigation.
Do not refresh directly from the view: it causes duplicate requests.
Next: implement state refresh after successful retry.

Я не делаю это после каждых нескольких сообщений. Иногда продолжить текущую сессию проще. Но теперь я не продолжаю старый thread только потому, что не хочу потерять уже накопленную историю.

Что происходило с usage

За период наблюдений средний fresh input на turn заметно снизился: примерно с 54,6K до 29,7K fresh tokens на turn. Разница получилась около 46%.

Cached input изменился намного меньше: примерно 652K → 583K на turn.

Но определить вклад каждого изменения отдельно по этим данным нельзя. Это не A/B-тест: задачи менялись, сессии были разной сложности, количество использования тоже отличалось.

Поэтому 46% я воспринимаю как общую динамику, а не как доказанную эффективность конкретной техники. Breakdown для меня оказался полезнее: при 67,4% tool outputs логичнее было экспериментировать с поиском, чтением файлов и историей сессии, чем пытаться сэкономить ещё немного токенов на prompt.

А что с Claude

Основные числа в этой статье получены из Codex session logs. Статистику одного агента нельзя автоматически переносить на другой, но сами идеи достаточно хорошо переиспользуются.

У Claude Code есть /context, compaction, skills и subagents. Для usage можно использовать тот же ccusage.

Ещё одна полезная вещь, Claude Code status line от Matt Pocock. Она показывает процент занятого context window прямо в терминале. Это помогает решить, продолжать текущую сессию, делать compact или готовить handoff.

/handoff от Matt Pocock решает передачу состояния fresh agent, а специализированные инструкции можно держать в skills, вместо того чтобы постепенно превращать CLAUDE.md или AGENTS.md в документацию обо всём проекте.

Часть этих идей я потом использовал и с Codex, не один в один на уровне hooks или конкретных команд, а как общий подход к организации контекста.

Стал бы я сейчас писать такой tracker?

Наверное, нет, если задача просто посмотреть общий расход. Для Codex и Claude Code ccusage уже умеет читать локальные данные, показывать usage и экспортировать JSON.

Мой скрипт оказался полезен потому, что мне хотелось ответить не только на вопрос:

Сколько токенов ушло?

Мне хотелось понять:

Из чего этот расход складывается?

До замеров я почти наверняка начал бы с сокращения prompts и AGENTS.md.

После 5 420 turns получилось:

  • user prompts: 4,6%;
  • developer instructions + bootstrap, меньше 2%;
  • tool outputs: 67,4%.

Это мои цифры, а не универсальная статистика для любого coding agent. В другом проекте или при другом стиле использования распределение вполне может быть другим.

Но после этого я намного спокойнее отношусь к длинному prompt, если он действительно нужен. Если несколько дополнительных абзацев сразу объясняют агенту, что начинать нужно с CheckoutRouter.swift, а не с широкого поиска по всему проекту, сокращать такой prompt только ради количества токенов особого смысла нет.

А вот за размером результатов поиска, чтения файлов и других tool outputs я теперь слежу намного внимательнее.