eugenemind.com
← Înapoi la blog

29 august 2026 · 14 min. de citit

Experimente cu măsurarea și optimizarea consumului de tokeni

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

Folosesc destul de mult coding agents în dezvoltarea de zi cu zi și, la un moment dat, pe măsură ce limitele au început să se simtă mai mult, am devenit curios cât de eficient folosesc de fapt tokenii disponibili.

Usage-ul total era foarte mare, dar cifra în sine nu explica mare lucru. Voiam să înțeleg, măcar pentru modul meu de lucru, unde se duc cei mai mulți tokeni. În prompturile mele? În AGENTS.md și alte instrucțiuni? În sesiunile lungi? Sau în ceea ce face agentul după ce trimit cererea?

Acum există mult mai multe instrumente gata făcute pentru asta. Pentru Codex, de exemplu, există ccusage. Acesta citește logurile locale ale sesiunilor Codex și poate afișa usage-ul pe zile, luni și sesiuni individuale, inclusiv cached tokens. Suportul pentru Codex este încă marcat ca beta, deoarece formatul logurilor Codex CLI continuă să se schimbe.

Pentru Claude Code, același ccusage poate citi logurile locale Claude și genera rapoarte daily, weekly, monthly și pe sesiuni. Claude Code are și mai multe mecanisme proprii pentru lucrul cu contextul: /context, compaction, CLAUDE.md, skills și subagents.

Când am început să strâng statisticile, existau mai puține opțiuni gata făcute pentru un breakdown atât de detaliat. În plus, eu nu voiam doar total usage. Voiam să înțeleg nu doar câți tokeni se consumă, ci și pe ce anume sunt consumați în interiorul unei sesiuni.

Așa că o parte din audit am făcut-o singur.

Trackerul scris de mine nu este ideea principală a articolului. Astăzi, dacă aș vrea doar să număr usage-ul, probabil aș folosi un instrument existent. Mai interesant a fost ce au arătat datele adunate.

Înainte să măsor ceva, primul lucru pe care îl suspectam erau propriile prompturi. De multe ori descriu o sarcină destul de detaliat. În plus există AGENTS.md, instrucțiuni permanente și contextul rămas din pașii anteriori. La prima vedere, toate acestea par scumpe.

Dar după prompt, agentul începe și el să adune context. Face grep, citește fișiere, caută usages, rulează build-uri sau teste și primește loguri. Înainte de măsurători nu aveam o imagine clară despre cât text se acumulează de fapt din partea asta.

Cum am strâns statisticile

Setul principal de date a venit din logurile locale ale sesiunilor Codex. Nu voiam doar total usage, ci să pot analiza turns individuale și să le leg de ceea ce se întâmpla în sesiune.

Am făcut un hook mic care salva local un snapshot înainte de fiecare turn nou: turn_id, repository-ul curent, hash-ul promptului, câteva câmpuri suplimentare și, atunci când era nevoie, raw payload-ul.

Fluxul arăta aproximativ așa:

Codex session

hook

capture metadata

Codex session logs

token_audit.py report

report.json / report.md

Am păstrat intenționat hook-ul cât mai simplu:

#!/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

Destul de repede am renunțat la ideea de a face calcule serioase direct în hook. Era mai simplu să salvez starea și apoi să construiesc separat raportul din datele acumulate.

O versiune simplificată a comenzii capture arăta cam așa:

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")

În scriptul real mai existau câteva câmpuri: branch-ul, dimensiunea repo instructions, fișierele handoff găsite în apropiere și raw payload-ul. Principiul rămânea același: hook-ul fixează starea turn-ului, iar o comandă separată report lucrează apoi cu logurile.

Raportul îl generam separat:

python3 ./tools/token_audit.py report \
  --repo-root "$PWD"

Rezultatul avea aproximativ structura următoare:

.token-audit/
├── hook-captures.jsonl
├── raw-hook-payloads/
└── latest-report/
    ├── report.json
    └── report.md

Comanda report parcurgea session logs, le asocia cu captures și împărțea fresh input-ul în mai multe categorii:

  • user prompts;
  • developer instructions;
  • repo bootstrap;
  • tool call arguments;
  • tool outputs;
  • assistant loopback.

Cached input îl calculam separat.

Nu era production-grade observability și nici un sistem de billing. Formatul logurilor se poate schimba, o parte din clasificare depinde de client, iar scriptul trebuie întreținut. Pentru ce aveam eu nevoie era suficient. Voiam să văd structura usage-ului și să pot compara perioade diferite.

În total am strâns 5.420 de turns în 120 de zile de utilizare.

Ce a rezultat

Cel mai interesant breakdown a fost acesta:

Sursă 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%

Mă așteptam la altceva.

Prompturile mele reprezentau doar 4,6%. Chiar dacă, ipotetic, aș fi redus fiecare prompt la jumătate, imaginea generală nu s-ar fi schimbat prea mult.

În schimb, 67,4% din fresh input venea din tool outputs.

După ce vezi cifra, explicația pare destul de simplă. Într-o sesiune obișnuită, agentul primește permanent text înapoi. Deschide un fișier și primește text. Face grep și primește rezultate. Rulează teste și primește încă un bloc de text. Uneori este puțin, alteori foarte mult.

Înainte de măsurători, priveam lucrurile astea mai degrabă ca pe munca internă a agentului. Dimensiunea promptului este ușor de observat, pentru că eu îl scriu. Un search care întoarce câteva sute de linii nu se simte în același fel ca un cost separat.

Dar tot textul respectiv intră și el în context, iar o parte rămâne acolo pentru turns următoare.

Chiar și un grep obișnuit are un cost

Asta se vedea mai ales în refactorizări mari și în lucrul cu modularizarea.

Dacă structura este stabilă și agentul știe deja unde se află codul relevant, lucrurile sunt destul de simple. Dar când fișierele sunt mutate între module și dependențele se schimbă, un fresh agent începe, destul de logic, cu ceva de genul:

rg "Checkout"

Primește câteva zeci de rezultate, apoi caută protocolul:

rg "PaymentRouting"

După aceea caută implementation, usages, deschide router-ul, view model-ul și poate un modul vecin ca să înțeleagă cine deține de fapt flow-ul.

Toate acestea sunt acțiuni normale. Și eu aș explora aproximativ la fel o zonă necunoscută din codebase. Diferența este că, pentru agent, rezultatele acestor căutări devin și ele parte din context.

Am încercat să elimin o parte din explorarea repetată păstrând o hartă mică a modulelor importante. Nu documentație pentru întregul proiect, ci un fișier scurt cu repere pentru zonele în care agentul începea în mod repetat aceleași căutări de la zero.

De exemplu:

## 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.

Numele sunt doar exemple. Importantă este structura.

Un astfel de fișier nu trebuie să explice fiecare clasă agentului. Codul tot trebuie citit. Fișierul răspunde doar la câteva întrebări înainte de o căutare largă: unde este entry point-ul, cine deține navigation, unde este state-ul și unde merită să te uiți prima dată?

Pentru modulele mai complicate, asta s-a dovedit util. Agentul continua să exploreze codul, dar nu mai trebuia să înceapă de fiecare dată cu o căutare prin tot proiectul.

În același timp, harta nu poate crește la nesfârșit. Dacă pui acolo tot ce a fost vreodată util, ajungi la încă un sistem intern de documentație care trebuie citit, întreținut și sincronizat cu codul. Eu am păstrat doar reperele care economiseau în mod repetat căutări.

Cu AGENTS.md rezultatul a fost puțin diferit

Înainte de măsurători eram aproape sigur că AGENTS.md consumă o parte vizibilă din tokeni.

Logica este simplă: este încărcat constant, deci conținutul lui ajunge în contextul fiecărei sesiuni.

Matt Pocock, în materialul despre AGENTS.md, recomandă să-l tratezi ca pe un brief, nu ca pe documentație: să fie scurt și declarativ, iar informația specializată să fie oferită doar atunci când este necesară.

În general sunt de acord, dar în datele mele AGENTS.md era departe de problema principală:

developer instructions, 1,43%

repo bootstrap, 0,34%

Împreună, mai puțin de două procente din fresh input.

Asta nu înseamnă că AGENTS.md poate avea orice dimensiune. Pentru mine rezultatul însemna ceva mai simplu: nu avea prea mult sens să încep optimizarea de aici cât timp tool outputs erau la 67,4%.

Totuși, mai târziu am curățat fișierul. Unele instrucțiuni erau necesare doar pentru anumite tipuri de task-uri, de exemplu reguli specifice pentru modularizare sau un workflow separat. Nu exista un motiv să fie încărcate în fiecare sesiune.

O parte din context am mutat-o în skills.

Matt Pocock are și un material separat, Writing for Agents, precum și colecția AI Skills for Real Engineers. O parte din abordarea asta se transferă destul de bine între Claude și Codex, chiar dacă mecanismele concrete sunt diferite.

Regula mea pentru AGENTS.md a devenit destul de simplă: nu „mai pot șterge încă zece linii?”, ci „agentul chiar are nevoie de informația asta în fiecare sesiune?”.

Uneori o instrucțiune mai detaliată merită tokenii în plus. Dacă câteva linii explică ownership-ul de la început și economisesc mai multe căutări prin repository, nu are prea mult sens să le scurtez doar ca să fac AGENTS.md mai mic.

Sesiunile lungi au fost mai previzibile

O sesiune acumulează treptat istorie.

Agentul caută ceva, testează o ipoteză, primește o eroare, încearcă altă variantă și citește mai multe fișiere. Toate lucrurile acestea au fost utile în momentul respectiv. Dar problema poate fi deja rezolvată, în timp ce istoricul rămâne.

Research-ul produce mult astfel de context suplimentar. Poți petrece destul timp verificând mai multe cauze posibile și, în final, să găsești cauza reală în altă parte. Pentru etapa următoare, de obicei contează rezultatul final: cauza este aici, acestea sunt fișierele relevante și aceasta este constrângerea care nu trebuie încălcată.

Ipotezele vechi și greșite nu mai sunt, de regulă, utile unei sesiuni noi.

Pentru task-uri mai lungi aveam un context.md pe care îl foloseam pentru a continua între sesiuni. La început am făcut o greșeală destul de evidentă: l-am transformat într-un jurnal.

## Session 1

Checked payment flow.
Looked at CheckoutViewModel and PaymentService.
First assumption was wrong because...
Build failed with...

## Session 2

Moved implementation.
Found another issue in...
Tried...

După un timp, fișierul a început să semene cu notițele întregului proces de lucru. Ca rezultat, o sesiune nouă primea din nou cea mai mare parte din contextul vechi, doar într-un alt format.

Așa că am încetat să mai adaug pur și simplu la context.md și am început să-l reconstruiesc:

## 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.

De obicei, un text de genul acesta este suficient pentru ca o sesiune nouă să continue fără întregul istoric.

Nu șterg absolut tot ce ține de trecut. Dacă agentul a urmat deja o cale evidentă, dar greșită, și există șanse mari ca un fresh agent să repete aceeași greșeală, o notă scurtă este utilă. Doar că nu este nevoie să transfer tot procesul prin care am ajuns la concluzia respectivă.

Mai târziu am găsit aproape aceeași idee în /handoff skill al lui Matt Pocock.

Matt Pocock folosește o formulare bună: portability, not compression. Scopul unui handoff nu este să păstreze cea mai completă versiune comprimată a sesiunii anterioare. Trebuie doar să-i dea unui alt agent suficientă informație pentru a continua lucrul.

Pentru Claude, skill-ul poate fi folosit aproape direct. În Codex aveam propria implementare, dar principiul este același.

Când este mai simplu să începi o sesiune nouă

După asta am început să folosesc fresh sessions mai des.

Înainte nu voiam să pierd contextul deja acumulat. În practică, după ce o etapă separată este terminată, de obicei mai este nevoie doar de o mică parte din acel context.

Să presupunem că lucrul arată aproximativ așa:

research
→ root cause found
→ implementation
→ validation

Înainte de implementation nu am nevoie de fiecare rezultat rg, de fiecare fișier deschis sau de fiecare presupunere anterioară. De multe ori este suficient ceva de genul:

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.

După aceea pot deschide un thread nou și continua.

Nu fac asta după fiecare câteva mesaje. Uneori este pur și simplu mai ușor să continui sesiunea curentă. Dar nu mai păstrez un thread vechi doar pentru că nu vreau să pierd tot istoricul acumulat în el.

Cum s-a schimbat usage-ul

În perioada observată, media de fresh input pe turn a scăzut vizibil.

Într-unul dintre intervalele anterioare era în jur de 54,6K fresh tokens per turn. Mai târziu a ajuns la aproximativ 29,7K.

Diferența este de aproximativ 46%.

Cached input s-a schimbat mult mai puțin: aproximativ 652K → 583K per turn.

Dar datele acestea nu îmi permit să separ efectul fiecărei schimbări. Nu a fost un A/B test. Task-urile s-au schimbat, sesiunile au avut niveluri diferite de complexitate, iar volumul de utilizare a variat și el.

Așa că nu pot spune cât a venit din module map, cât din fresh sessions și cât din handoff-uri mai scurte.

Privesc acel 46% ca pe o tendință generală, nu ca pe dovada că o anumită tehnică a redus usage-ul cu exact atât.

Breakdown-ul a fost mai util pentru mine. Mi-a arătat unde are sens să caut problema. Dacă 67,4% din fresh input vine din tool outputs, are mai mult sens să experimentez cu search-uri, file reads și session history decât să încerc să economisesc încă puțini tokeni din prompt.

Dar Claude?

Cifrele principale din articol provin din Codex session logs. Păstrez intenționat această distincție: statisticile unui agent nu trebuie aplicate automat altuia.

Dar ideile generale se transferă destul de bine.

Claude Code are acum /context, compaction, skills și subagents. Subagents, de exemplu, permit mutarea unei bucăți separate de research în afara sesiunii principale și întoarcerea doar a rezultatului necesar.

Pentru usage tracking se poate folosi același ccusage, care citește logurile locale Claude Code.

Un alt instrument util este Claude Code status line al lui Matt Pocock. Acesta afișează direct în terminal procentul din context window deja utilizat.

Status line-ul nu optimizează nimic singur, dar starea sesiunii rămâne vizibilă:

context: 37%

sau:

context: 82%

Asta face mai ușor de decis dacă merită să continui sesiunea curentă, să faci compact sau să pregătești un handoff.

/handoff al lui Matt Pocock rezolvă partea a doua: transferul stării curente către un fresh agent. Instrucțiunile specializate pot sta în skills, în loc ca CLAUDE.md sau AGENTS.md să se transforme treptat în documentația întregului proiect.

Mai târziu am reutilizat o parte dintre ideile acestea și cu Codex. Nu unu-la-unu la nivel de hooks sau comenzi concrete, ci ca abordare generală pentru organizarea contextului.

Aș mai scrie acum un astfel de tracker?

Probabil că nu, dacă singurul scop ar fi să văd total usage.

Pentru Codex și Claude Code, ccusage poate deja să citească datele locale, să afișeze usage-ul și să exporte JSON pentru analiză ulterioară.

Scriptul meu a fost util pentru că voiam să răspund la mai mult decât:

Câți tokeni am folosit?

Voiam să înțeleg:

Din ce este format, de fapt, consumul acesta?

Sunt două întrebări puțin diferite.

Pentru prima, un usage tracker existent este suficient astăzi. Pentru a doua, poate fi încă util să te uiți în interiorul unei sesiuni și să descoperi că promptul lung care părea suspect reprezintă doar 4,6%, în timp ce cea mai mare parte din fresh input vine din cu totul altă parte.

Pentru mine, acesta a fost cel mai util rezultat al întregului experiment.

Înainte să măsor, aproape sigur aș fi început prin a scurta prompturile și AGENTS.md.

După 5.420 de turns, rezultatul a fost:

  • user prompts: 4,6%;
  • developer instructions + bootstrap, mai puțin de 2%;
  • tool outputs: 67,4%.

Acestea sunt cifrele mele, nu statistici universale pentru orice coding agent. Un alt proiect sau un alt stil de utilizare poate produce o distribuție foarte diferită.

Dar de atunci sunt mult mai puțin preocupat de un prompt lung atunci când detaliile chiar sunt utile. Dacă încă două-trei paragrafe îi spun agentului să înceapă cu CheckoutRouter.swift în loc să facă o căutare largă prin tot proiectul, nu prea are sens să scurtez promptul doar pentru a economisi tokeni.

În schimb, acum urmăresc mult mai atent dimensiunea rezultatelor de căutare, a file reads și a celorlalte tool outputs.