29 august 2026 · 14 min. de citit
Experimente cu măsurarea și optimizarea consumului de tokeni
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.