Ridurre l'uso dei token con l'ingegneria senza compromettere la qualità
Il mese scorso ho esaminato un agente di assistenza clienti che consumava 18,000–42,000 token di input per ticket prima di scrivere anche solo una parola. Il problema effettivo dell'utente occupava solitamente meno di 300 token. Il resto era contesto inserito «per sicurezza»: l'intera cronologia della conversazione, ogni pagina delle policy interne recuperata tramite ricerca per parole chiave, JSON risultati degli strumenti copiati integralmente e un prompt di sistema di 900 token che si era stratificato nel corso di sei mesi.
La soluzione non è stata «usa un modello più piccolo» o «abbassa max_tokens e spera che vada bene». Abbiamo ridotto l'utilizzo mediano dei token di circa il 68% mantenendo stabile la qualità delle risposte grazie a un lavoro di ingegneria poco entusiasmante: snellire il contesto, recuperare evidenze più mirate, riassumere lo stato, imporre schemi di output e misurare i token in modo accurato.
Questo articolo riguarda proprio quel tipo di lavoro. Non esercizi di riduzione estrema dei prompt. Non trucchi che peggiorano il modello. Solo modi pratici per consumare meno token senza rendere il sistema meno intelligente.
L'utilizzo dei token è un problema di architettura
La maggior parte dei team considera il costo dei token un problema legato ai prezzi dei modelli. In pratica, di solito è un problema di instradamento delle informazioni.
Una tipica chiamata a un LLM in produzione comprende quattro categorie di token:
- Token delle istruzioni: prompt di sistema, prompt dello sviluppatore, regole di formattazione, vincoli di sicurezza.
- Token del contesto: cronologia della chat, documenti recuperati, output degli strumenti, record del database.
- Token del ragionamento e dell'attività: la richiesta effettiva dell'utente e i dettagli intermedi dell'attività.
- Token di output: la risposta generata, l'oggetto JSON, la query SQL, il riepilogo o la chiamata a uno strumento.
Quando l'utilizzo dei token sfugge al controllo, di solito i responsabili sono il contesto e l'output. Il modello non è costoso perché l'utente ha posto una domanda difficile. È costoso perché l'applicazione ha infilato il mondo intero nel prompt e poi ha lasciato che il modello si dilungasse.
Un primo esercizio utile consiste nel registrare l'utilizzo dei token per sezione, non solo la dimensione totale della richiesta:
{
"request_id": "ticket_81291",
"model": "sonnet-4.6",
"tokens": {
"system": 742,
"history": 3890,
"retrieval": 12640,
"tool_results": 2110,
"user": 184,
"output": 931
},
"latency_ms": 4820
}
Una volta vista questa suddivisione, il percorso di ottimizzazione diventa evidente. Non serve discutere se Claude Opus 4.8, Sonnet 4.6, GPT-5.5, Gemini 3, Fable 5 o un modello aperto come Qwen o DeepSeek sia il «migliore» in astratto. Prima devi smettere di inviare 12,000 token irrilevanti provenienti dal recupero delle informazioni.
Inizia con un budget di token per ciascun percorso
Preferisco assegnare i budget per percorso del prodotto, anziché globalmente. La revisione di un contratto legale, un agente di programmazione e un bot per le FAQ non dovrebbero basarsi sulle stesse ipotesi relative ai token.
Esempio di budget per un assistente di supporto:
| Modalità di accesso | Budget di input | Budget di output | Classe del modello | Note |
|---|---|---|---|---|
| Classificazione del ticket | 600 | 50 | Modello economico e veloce | Etichette, urgenza, lingua |
| Recupero delle evidenze | 1,500 | 0 | Embedding/ricerca | Nessuna generazione necessaria |
| Stesura della risposta | 4,000 | 600 | Modello potente di fascia media o alta | Usa le evidenze selezionate |
| Riepilogo della conversazione | 1,000 | 250 | Modello economico e veloce | Aggiorna lo stato della memoria |
| Analisi per l'escalation | 6,000 | 900 | Modello potente | Solo per i casi complessi |
Il punto non è che questi numeri siano universalmente corretti. Sono vincoli iniziali volutamente modesti. Il comportamento utile è che ogni fase deve giustificare i token che utilizza.
Un'insidia comune: se imposti solo max_tokens per l'output, l'input può comunque crescere a dismisura senza che te ne accorga. I limiti di output impediscono risposte lunghe; non impediscono di sovraccaricare il prompt con informazioni recuperate.
Snellisci il contesto prima del recupero, non dopo
Molti sistemi recuperano documenti usando l'ultimo messaggio dell'utente, ma includono comunque l'intera cronologia della conversazione nel prompt finale. Questo significa che vecchie digressioni, precedenti chiamate agli strumenti fallite e ipotesi obsolete restano nel contesto.
Mantieni invece uno stato di lavoro compatto:
{
"user_goal": "Get refund for duplicate annual subscription charge",
"known_facts": [
"User paid twice on 2025-01-08",
"Invoice IDs: inv_1042 and inv_1077",
"Account email: user@example.com"
],
"constraints": [
"Refund policy allows duplicate-charge refunds within 90 days",
"User prefers email follow-up"
],
"open_questions": [
"Whether one charge has already been reversed"
],
"last_updated_turn": 12
}
La chiamata finale includerà quindi:
- Lo stato compatto.
- L'ultimo messaggio dell'utente.
- Solo gli estratti pertinenti delle policy recuperate.
- Eventualmente gli ultimi uno o due scambi della conversazione, se la continuità del tono è importante.
Non includere ciecamente tutti i messaggi precedenti. La cronologia della chat non è memoria: è una trascrizione. La memoria è lo stato essenziale necessario per proseguire l'attività.
In pratica, uso tre livelli:
- Scambi recenti: gli ultimi 1–4 scambi, a seconda della delicatezza dell'attività.
- Riepilogo progressivo: fatti e decisioni stabili.
- Recupero da fonti esterne: documenti, record del database, output degli strumenti.
Questo approccio di solito funziona meglio dell'inserimento indiscriminato della trascrizione grezza, perché il modello vede meno rumore e meno istruzioni in conflitto.
Il recupero delle informazioni deve essere selettivo, non decorativo
La generazione aumentata dal recupero delle informazioni spesso nasce per migliorare la qualità e finisce per diventare uno spreco di token. Il problema è facile da individuare: top-k è impostato su 10 o 20, i blocchi contengono 1,000 token ciascuno e il prompt finale contiene una pila di documenti vagamente correlati.
Un recupero migliore è di solito anche un recupero meno costoso.
Una pipeline pratica per il recupero delle informazioni:
def build_context(query, user_state):
search_query = rewrite_for_search(query, user_state)
candidates = vector_search(
query=search_query,
top_k=30,
filters={"product": user_state["product"]}
)
reranked = rerank_cross_encoder(
query=search_query,
docs=candidates,
top_k=6
)
selected = []
token_budget = 1800
for doc in reranked:
snippet = extract_relevant_span(doc.text, query=search_query, max_tokens=300)
if token_count(selected) + token_count(snippet) <= token_budget:
selected.append({
"doc_id": doc.id,
"title": doc.title,
"text": snippet
})
return selected
I dettagli importanti:
- Recupera più candidati di quanti prevedi di includerne.
- Riordina i risultati per rilevanza prima della generazione.
- Estrai i passaggi pertinenti invece di passare blocchi interi.
- Imponi un budget di token per il recupero delle informazioni.
- Conserva gli ID dei documenti affinché il modello possa citare le evidenze o farvi riferimento internamente.
La dimensione dei blocchi conta. Se i blocchi sono troppo piccoli, il modello perde il contesto. Se sono troppo grandi, ogni corrispondenza trascina con sé testo irrilevante. Per la documentazione dei prodotti e le pagine delle policy, spesso parto da circa 300–600 token per blocco, mantenendo i titoli, e poi adatto le dimensioni in base ai casi di errore.
Che cosa succede davvero quando inserisci troppo contesto recuperato? Il modello spesso diventa meno preciso. Cerca di conciliare policy affini ma irrilevanti, incorpora avvertenze obsolete o risponde alla versione sbagliata della domanda. Ridurre i token può migliorare la qualità perché elimina gli elementi di distrazione.
Riassumere significa comprimere senza perdere le informazioni essenziali
La sintesi può risparmiare enormi quantità di contesto, ma i riepiloghi imprecisi sono pericolosi. Se un riepilogo omette un vincolo, il tuo agente potrebbe fare con sicurezza la cosa sbagliata.
Preferisco riepiloghi strutturati a quelli in prosa:
{
"task": "Debug failing OAuth callback in staging",
"environment": {
"service": "auth-api",
"branch": "release-2025-02-14",
"runtime": "node 20"
},
"confirmed": [
"Callback returns 302 to /login",
"Provider sends code and state",
"State validation passes"
],
"suspected": [
"Session cookie is not persisted after callback"
],
"rejected": [
"Provider credentials are invalid",
"Redirect URI mismatch"
],
"next_steps": [
"Inspect Set-Cookie attributes in staging response",
"Compare SameSite and Secure flags with production"
]
}
Questo consuma meno token ed è più affidabile rispetto a:
L'utente sta eseguendo il debug di OAuth e ha provato varie soluzioni…
Un buon passaggio di sintesi dovrebbe preservare:
- L'obiettivo dell'utente.
- I fatti certi.
- Le decisioni già prese.
- I vincoli e le preferenze.
- I tentativi falliti.
- Le questioni aperte.
- ID, nomi di file, timestamp e altri riferimenti esatti.
Non eliminare i valori esatti nella sintesi, a meno che non siano davvero irrilevanti. «L'utente ha un problema con una fattura» consuma pochi token, ma è inutile. «Addebito duplicato: inv_1042 e inv_1077 in data 2025-01-08» è il tipo di compressione a cui puntare.
Per gli agenti che operano a lungo, conservo anche le versioni delle sintesi. Se il modello produce una sintesi errata, serve la possibilità di verificare quando lo stato ha iniziato a divergere.
Eliminare la prolissità dell'output con gli schemi
Una quantità sorprendente di token viene sprecata nell'output. Il prompt dice «sii conciso», ma il modello scrive un preambolo, precisazioni, un riepilogo e un'offerta di aiuto.
Per gli output destinati all'elaborazione automatica, usa gli schemi. Non chiedere genericamente «una risposta JSON»; definisci la struttura e rifiuta tutto il resto.
Esempio di chiamata di classificazione:
{
"type": "object",
"additionalProperties": false,
"required": ["category", "priority", "needs_human", "reason"],
"properties": {
"category": {
"type": "string",
"enum": ["billing", "bug", "account", "how_to", "other"]
},
"priority": {
"type": "string",
"enum": ["low", "normal", "high", "urgent"]
},
"needs_human": {
"type": "boolean"
},
"reason": {
"type": "string",
"maxLength": 160
}
}
}
Per le risposte rivolte agli utenti, specifica un formato con un limite massimo rigido:
Write the answer in this structure:
- First sentence: direct answer, max 25 words.
- Then 2-4 bullets with concrete steps.
- Do not include a greeting.
- Do not include "let me know if you need anything else."
- Stop after the final bullet.
Questo funziona meglio di «sii breve», perché fornisce al modello una struttura.
Un'insidia comune: un valore basso di max_tokens può troncare un output utile a metà di un ragionamento. Uno schema o un formato vincolato riduce la lunghezza dell'output preservandone la completezza. Il troncamento è l'ultima risorsa, non una strategia di formattazione.
Usare le sequenze di arresto con cautela
Le sequenze di arresto sono utili quando il completamento ha un confine naturale. Per esempio, se il modello sta generando una sezione di un documento basato su un modello predefinito, puoi interromperlo al marcatore della sezione successiva.
{
"model": "gpt-5.5",
"messages": [
{
"role": "user",
"content": "Generate only the Risk Summary section for this review..."
}
],
"max_tokens": 500,
"stop": ["\n## Mitigations", "\n## Appendix"]
}
Le sequenze di arresto sono meno utili per le risposte aperte dell'assistente, perché possono troncare contenuti validi. Inoltre, si comportano diversamente tra API e famiglie di modelli, soprattutto quando sono coinvolte chiamate a strumenti o output strutturati. Considerale protezioni dei confini, non una tecnica di compressione primaria.
Usi appropriati:
- Generare una sezione di un modello di documento noto.
- Impedire al modello di proseguire con gli esempi.
- Fermarsi a un delimitatore nell'elaborazione in batch.
- Vincolare i prompt tradizionali basati sul completamento.
Usi rischiosi:
- Fermarsi in corrispondenza di segni di punteggiatura comuni.
- Fermarsi su parole che potrebbero comparire nelle risposte normali.
- Usare sequenze di arresto al posto di uno schema di output adeguato.
- Presumere un comportamento identico tra Claude, GPT, Gemini e i gateway per modelli aperti.
Distribuire le attività tra modelli più economici
Non ogni passaggio richiede il modello più potente. Nei sistemi in produzione, spesso instrado le sottoattività verso modelli più piccoli o più economici:
| Sottoattività | Requisito tipico | Soluzione adatta |
|---|---|---|
| Rilevamento della lingua | Classificazione semplice | Modello piccolo/economico |
| Categoria del ticket | Output strutturato di etichette | Modello piccolo o medio |
| Riscrittura delle query | Trasformazione breve | Modello piccolo o medio |
| Riordinamento dei documenti per rilevanza | Modello specializzato nel riordinamento per rilevanza o modello piccolo | Non generativo, ove possibile |
| Risposta finale | Ragionamento e tono | Modello più potente |
| Eccezione alle policy | Sfumature e rischio | Modello più potente disponibile |
È qui che l'accesso a più modelli diventa utile sul piano operativo. Potresti volere Sonnet 4.6 per la maggior parte delle attività di scrittura degli agenti, GPT-5.5 per un percorso di ragionamento complesso, Gemini 3 per determinati flussi di lavoro multimodali o con contesto lungo, Fable 5 quando la finestra di contesto 1M è davvero necessaria, e modelli aperti come Llama, Qwen, DeepSeek, MiniMax o Kimi per attività interne controllate. Dal punto di vista ingegneristico, ciò che conta è l'instradamento, non la fedeltà al marchio.
Il limite: il sovraccarico dell'orchestrazione è reale. Ogni chiamata aggiuntiva a un modello introduce latenza, modalità di errore, complessità nella registrazione dei log e più aspetti da valutare. Se un classificatore economico fa risparmiare 300 token ma aggiunge 600 ms e un ulteriore percorso di ritentativo, potrebbe non valerne la pena.
Un approccio che mi piace:
if route == "simple_faq":
model = "haiku-4.5"
elif route == "standard_support":
model = "sonnet-4.6"
elif route == "legal_or_financial_exception":
model = "opus-4.8"
else:
model = "sonnet-4.6"
I nomi esatti cambieranno. La regola dovrebbe restare la stessa: usare il modello più economico che soddisfa in modo affidabile il livello di qualità richiesto dall'attività.
Misurare con onestà i token in ingresso e in uscita
L'ottimizzazione dei token fallisce quando i team guardano solo al costo medio per richiesta. Servono distribuzioni, suddivise per percorso e fase.
Monitora almeno:
- Token in ingresso per sezione del prompt.
- Token in uscita.
- Numero di documenti recuperati e token del recupero.
- Nome e versione del modello.
- Percentili della latenza.
- Tasso di successo della cache, se si usa la memorizzazione nella cache del prompt o del contesto.
- Numero di ritentativi.
- Indicatori di qualità percepibili dall'utente.
- Tassi di intervento o correzione da parte di operatori umani.
Una dashboard utile mostra p50, p90 e p99. I bug relativi ai token si nascondono spesso nella coda della distribuzione.
Esempio di riga di log:
{
"route": "support.draft_answer",
"model": "sonnet-4.6",
"input_tokens": 3820,
"output_tokens": 514,
"retrieved_docs": 4,
"retrieval_tokens": 1320,
"latency_ms": 3100,
"cache_hit": false,
"quality": {
"user_thumb": "up",
"human_edited": false
}
}
Fai attenzione ad affermazioni come «abbiamo ridotto i token del 40%». Sono aumentati i ritentativi? Sono aumentati i passaggi agli operatori? Gli utenti hanno posto più domande di approfondimento perché le risposte sono diventate troppo stringate? Il modello più economico ha prodotto più JSON non validi, rendendo necessarie chiamate di correzione?
La metrica onesta non è il numero di token per chiamata. È il numero di token per attività completata con successo.
Un contesto lungo non autorizza la pigrizia
Le finestre di contesto ampie sono davvero utili. La classe di contesto 1M di Fable 5, i punti di forza di Gemini sul contesto lungo e le modalità di contesto lungo dei modelli di frontiera consentono flussi di lavoro che erano difficili da gestire qualche anno fa: analisi del codice dell'intero repository, revisione di documenti estesi, trascrizioni di più ore e corposi dossier di ricerca.
Ma il contesto lungo è una capacità, non un'architettura predefinita.
Le chiamate con contesto lungo possono essere appropriate quando:
- La risposta dipende da molte parti distanti dell'input.
- Il recupero non coglierebbe le relazioni tra documenti.
- L'attività è esplorativa e non sai ancora cosa conta.
- Il costo della perdita di un dettaglio è superiore al costo di leggere di più.
Di solito sono uno spreco quando:
- L'utente pone una domanda fattuale circoscritta.
- Bastano pochi estratti delle fonti.
- Il contesto contiene informazioni duplicate o obsolete.
- Il modello viene usato come filtro per un database.
Un approccio comune consiste nell'usare il contesto lungo per l'analisi offline e produrre artefatti compatti da utilizzare online. Per esempio, analizza una volta l'intero corpus di policy, poi genera una mappa strutturata delle policy, embedding, sintesi e casi di test. L'assistente in esecuzione non dovrebbe rileggere l'intero corpus per ogni ticket.
Un flusso di lavoro pratico per l'ottimizzazione
Quando eredito un sistema LLM che consuma molti token, di solito procedo in quest'ordine:
-
Prima aggiungere la strumentazione
Aggiungi il conteggio dei token per sezione. Non ottimizzare alla cieca. -
Limitare il contesto recuperato
Imposta un budget rigido di token per il recupero. Riordina i risultati per rilevanza ed estrai i passaggi pertinenti. -
Sostituire l'accumulo di trascrizioni con lo stato
Conserva i turni recenti insieme a una memoria strutturata. -
Vincolare gli output
Usa schemi per gli output destinati all'elaborazione automatica e formati espliciti per quelli rivolti agli utenti. -
Instradare le sottoattività
Affida classificazione, riscrittura e sintesi a modelli più economici quando sono affidabili. -
Valuta la qualità a livello di attività
Confronta le risoluzioni riuscite, non solo il costo per chiamata. -
Tieni d'occhio la coda della distribuzione
Indaga sui picchi di token ai percentili p95 e p99. Spesso rivelano problemi nel recupero delle informazioni o una cronologia che cresce senza controllo.
Ecco un semplice meccanismo di protezione che ho usato nei servizi:
TOKEN_LIMITS = {
"support.classify": {"input": 800, "output": 80},
"support.retrieve": {"input": 500, "output": 0},
"support.draft": {"input": 4500, "output": 700},
"support.summarize": {"input": 1800, "output": 300},
}
def assert_token_budget(route, prompt_sections, max_output_tokens):
input_tokens = {
name: count_tokens(text)
for name, text in prompt_sections.items()
}
total_input = sum(input_tokens.values())
limits = TOKEN_LIMITS[route]
if total_input > limits["input"]:
raise TokenBudgetError(route, total_input, input_tokens)
if max_output_tokens > limits["output"]:
raise TokenBudgetError(route, max_output_tokens, {"output": max_output_tokens})
Interrompere subito è meglio che scoprire una settimana dopo che una query di recupero mal formulata ha consumato il budget su PDF irrilevanti.
Indicazioni pratiche
- La riduzione dei token dovrebbe iniziare dalla visibilità: registra i token di input e output per sezione del prompt, percorso e modello.
- Il recupero delle informazioni è più efficace dell'accumulo indiscriminato quando è selettivo: riordina i risultati per rilevanza, estrai i passaggi e imponi un limite al contesto.
- Le sintesi dovrebbero rappresentare uno stato strutturato, non essere prosa vaga. Conserva fatti esatti, ID, vincoli e tentativi falliti.
- Gli schemi di output riducono la prolissità in modo più sicuro rispetto al semplice abbassamento di
max_tokens. - Le sequenze di arresto sono utili per delimitare l'output, ma non sostituiscono gli output strutturati.
- Instrada le sottoattività semplici verso modelli più economici solo quando la latenza aggiuntiva e le modalità di errore valgono il compromesso.
- Valuta il risparmio in base ai token per attività completata con successo, non ai token per singola chiamata.
- I modelli con contesto lungo sono potenti, ma non eliminano la necessità di un'architettura dell'informazione.
Un'unica chiave API per Claude Opus 5.5, Sonnet 5, Haiku 4.5 e Fable 5.1, oltre a GPT-6 modelli. Pagamento a consumo, senza abbonamento.
Ottieni la tua chiave API →