Perché un system prompt da 35 KB si rompe su un modello locale
- Fare l'aritmetica del budget di contesto prima di spostare un prompt, e capire perché un prompt da 35 KB invisibile su Claude è un quarto di una piccola finestra locale
- Sapere esattamente cosa fa Ollama quando il prompt è troppo lungo: scarta la parte centrale, tiene pochi token di prefisso e lo dice solo al log del server
- Riconoscere i cinque segnali comportamentali di esaurimento del contesto in un agente locale prima che sprechi un'ora
- Ristrutturare un prompt monolitico in unità a obiettivo singolo che entrano nella finestra, passando lo stato tra loro su disco
- Regolare le manopole del runtime che contano: OLLAMA_CONTEXT_LENGTH, quantizzazione della KV cache, flash attention e il moltiplicatore delle richieste parallele
Un post arrivato in prima pagina su Hacker News il 13–14 settembre 2026 descriveva un'esperienza che molti stanno per fare: un system prompt da 35 KB che girava senza problemi su Claude Opus è stato puntato su un modello 27B self-hosted tramite Ollama e, nel giro di circa tre minuti, l'agente rileggeva file già letti, ripeteva chiamate a tool identiche e riscriveva lavoro già finito. Il modello non aveva niente che "non andasse". Il prompt più la cronologia della sessione avevano semplicemente superato una finestra da 65K token, e il runtime aveva iniziato a buttare via pezzi della conversazione.
Questa pagina è la guida che l'autore avrebbe voluto trovare. Parla di dimensione del prompt contro budget di contesto, che è un problema diverso dalla formulazione del prompt tra modelli, trattata in Portare i prompt tra modelli, e da quale modello locale scegliere, trattato in Eseguire modelli in locale con Ollama.
L'aritmetica che nessuno fa per prima
Su Claude un system prompt da 35 KB sono circa 9.000 token, dentro una finestra da 1.000.000 di token: meno dell'1 %. Non ci pensi mai. Su un runtime locale lo stesso prompt sono gli stessi 9.000 token, ma la finestra è quella che hai configurato, e il default è piccolo:
Contesto di default di Ollama (documentazione main corrente) | Macchina |
|---|---|
| 4K token | meno di 24 GiB di VRAM |
| 32K token | 24–48 GiB di VRAM |
| 256K token | 48 GiB di VRAM o più |
A 4K, un system prompt da 9.000 token non entra proprio. A 32K è il 28 % della finestra prima che l'utente dica una parola. Ai 65K configurati dall'autore del post HN è il 14 %, che sembra accettabile finché non aggiungi ciò che una sessione agentica trasporta davvero: definizioni dei tool (da qualche centinaio a qualche migliaio di token), ogni file che l'agente legge, ogni risultato di tool e l'output del modello stesso. Un agente di coding che legge tre file medi ed esegue due comandi è di routine a 20–30K token di conversazione. Con un costo fisso di 9K, la memoria di lavoro utilizzabile sparisce in una manciata di turni.
Le due abitudini del cloud che rendono tutto questo invisibile su Claude mancano in locale: la finestra è due ordini di grandezza più piccola, e non c'è nessuna compattazione integrata. Claude Code riassume la conversazione quando si avvicina al limite (Gestione del contesto); un endpoint Ollama grezzo no. Alcuni harness per agenti locali fanno la propria compattazione, ma il runtime sotto di loro no.
Misura il prompt prima di migrare (qualsiasi endpoint locale compatibile OpenAI)
# Count what the model will actually see, from Ollama's own tokenizer.
# prompt_eval_count in the response is the tokenised prompt length.
curl -s http://localhost:11434/api/chat -d '{
"model": "qwen3:32b",
"messages": [{"role":"system","content":"'"$(cat system-prompt.md | sed 's/"/\\"/g')"'"},
{"role":"user","content":"ping"}],
"stream": false,
"options": {"num_predict": 1}
}' | jq '{prompt_tokens: .prompt_eval_count, cached: .prompt_eval_cached_count}'Confronta quel numero con il num_ctx con cui giri, poi sottrai lo schema dei tool e una trascrizione realistica. Se il resto è sotto circa metà della finestra, il prompt è troppo grande per quella configurazione, e nessuna riformulazione lo sistemerà.
Cosa fa Ollama quando il prompt non entra
Questo è il meccanismo che trasforma "leggermente troppo lungo" in "misteriosamente stupido", e non è documentato da nessuna parte in prosa; sta nel sorgente.
Quando il prompt di una richiesta è più lungo del contesto e il troncamento è abilitato (lo è, di default, per /api/chat e /api/generate), il runner di Ollama basato su llama.cpp fa quanto segue, in llm/llama_server.go:
- Tiene i primi
num_keeptoken del prompt. Ilnum_keepdi default è 4, impostato "per evitare problemi nei context shift". - Calcola una lunghezza obiettivo che lascia libera circa metà del contesto rimanente per la generazione.
- Scarta i token dal centro: il prefisso conservato resta, la coda resta, il blocco tra i due viene rimosso.
- Scrive
truncating input promptcon le dimensioni prima/dopo nel log del server. La risposta API non porta nessun flag, e una libreria client vede un completamento normale.
La tabella di test del progetto stesso rende concreta la scala: con un contesto da 4.096 token e il num_keep di default, un prompt troppo lungo viene tagliato a 2.050 token. Metà della finestra è riservata deliberatamente alla risposta. Quindi un system prompt da 9.000 token su una finestra da 4K arriva al modello come quattro token del suo inizio, poi un buco, poi quello che ci sta alla fine della conversazione. Le istruzioni sono sparite; l'ultimo messaggio utente sopravvive. È esattamente il comportamento "ripete gli obiettivi, ripete le chiamate ai tool" descritto nel post HN: al modello viene chiesto di agire su una conversazione le cui regole sono state cancellate in silenzio.
Due vie d'uscita dalla versione silenziosa:
- Invia
"truncate": falsenella richiesta. Ollama allora restituisce HTTP 400 con un messaggio che dice che il prompt è più lungo del contesto attualmente disponibile, invece di tirare a indovinare. Per un harness agentico è il comportamento corretto: fallire rumorosamente, poi compattare o ripartire. - Alza
num_keepalla lunghezza del tuo system prompt così il prefisso sopravvive a uno shift. Questo preserva le regole ma cancella comunque conversazione dal centro; è un cerotto, non una soluzione.
L'endpoint chat ha un secondo livello, più gentile, sopra questo: quando la lista dei messaggi è troppo lunga per il limite, il costruttore di prompt di Ollama scarta prima i messaggi più vecchi tenendo il messaggio di sistema e l'ultimo turno (vedi server/prompt.go). Anche così puoi perdere risultati intermedi dei tool, ed è per questo che un agente può "dimenticare" di aver già letto un file. Guarda il log del server; con OLLAMA_DEBUG=1 il taglio è visibile.
:::note Non solo Ollama Il server di llama.cpp ha lo stesso comportamento di context shift sotto un flag diverso, vLLM rifiuta i prompt troppo lunghi di default, e LM Studio espone la lunghezza del contesto come impostazione per modello che è facile lasciare al suo piccolo default. Il pattern da interiorizzare è: sapere se il tuo runtime tronca o va in errore, e preferire l'errore. :::
I cinque segnali di fallimento
L'autore del post HN ha tenuto una lista di come appariva dall'esterno l'esaurimento del contesto, e coincide con quello che si riporta su ogni agente con finestra piccola. Tratta ognuno di questi come "la finestra è piena", non come "il modello è scarso":
- Chiamate a tool identiche consecutive. Il modello non ha più in vista il risultato precedente, quindi chiede di nuovo.
- Rilettura di un file già letto. Stessa causa; il risultato della lettura è stato tagliato.
- Riformulazione dell'obiettivo con parole proprie a metà task, a volte con deriva. Il system prompt è stato tagliato, e il modello sta ricostruendo il compito dalla coda della conversazione.
- Errori di parsing delle chiamate a tool che compaiono dopo una serie di chiamate pulite. Una volta spariti gli esempi e lo schema nel prompt, i modelli piccoli tornano alla loro formattazione di default.
- Numero di turni alto rispetto ai file effettivamente modificati. Il rapporto è la metrica più economica da tracciare in un harness; quando schizza, fermati e riparti con contesto fresco.
Se passi da un client compatibile OpenAI, logga prompt_eval_count a ogni turno. Quando smette di crescere mentre la conversazione continua a crescere, il troncamento è iniziato.
Ristrutturare: prompt a obiettivo singolo e stato su disco
Un prompt da 35 KB di solito è l'intero regolamento di un prodotto: persona, standard di codice, etichetta dei tool, formati di output, casi limite e una lunga lista di "mai fare X". Su un modello da 1M di token funziona per forza bruta. Su un modello da 32K no, e la soluzione a cui è arrivato l'autore del post HN è la stessa sostenuta in modo indipendente dai commentatori più forti: spezzare il prompt in unità a obiettivo singolo ed eseguire ciascuna nella propria sessione breve.
- Scorri i 35 KB e tagga ogni paragrafo con il lavoro che svolge: pianificare, modificare, testare, revisionare, scrivere il commit. La maggior parte dei monoliti contiene da cinque a otto lavori distinti più un nucleo condiviso (persona, fatti sul repo, regole di sicurezza) che sta sotto i 2 KB.
- Ogni lavoro riceve il nucleo condiviso più solo le proprie regole ed esempi. Punta a 2–4K token ciascuno. Su Ollama, distribuiscili come Modelfile separati (`PARAMETER num_ctx` impostato esplicitamente, blocco `SYSTEM` per lavoro) o come file di prompt per agente nel tuo harness.
- Ogni unità scrive un breve riepilogo strutturato (cosa ha fatto, cosa resta, quali file) in un file quando finisce; l'unità successiva legge solo quel file più la fetta di repo che le serve. La sessione termina. È l'equivalente per modelli locali della compattazione di Claude Code, fatta a mano.
- I modelli piccoli seguono 'fai solo X' molto meglio di 'non fare Y', e le liste negative sono il punto in cui i prompt monolitici si gonfiano. Converti `never touch tests` in `edit only files under src/`.
- Ogni risultato di tool è contesto che paghi finché non viene tagliato. Preferisci tool che restituiscono una fetta (una funzione, un hunk) a tool che restituiscono un file intero; la coppia `inspect_function` / `replace_function` di un commentatore HN è l'esempio canonico.
- Finisci l'unità, scrivi il passaggio di consegne, chiudi la sessione. Non aspettare i segnali di fallimento; a quel punto il prompt è già stato troncato e l'output non è affidabile.
Un file di passaggio di consegne che la prossima unità a obiettivo singolo legge per prima
# HANDOFF — written by unit 2 (implement), read by unit 3 (test) Objective completed: renamed getUser -> fetchUser across src/ Files changed: src/api/user.ts, src/pages/profile.tsx, src/hooks/useUser.ts Not done: tests still reference getUser (tests/user.test.ts) Constraints carried forward: no new dependencies; keep TypeScript strict Next unit should: update tests, run `npm test`, write result to HANDOFF.md
Questa è anche la risposta onesta al commentatore che ha detto che un prompt da 35 KB è "confuso e sfocato su qualsiasi LLM". I modelli di frontiera nascondono il gonfiore; i modelli locali lo espongono. Spezzare per obiettivo di solito migliora i risultati anche su Claude, ed è il design che Claude più modelli locali presuppone quando instrada i passi economici in locale.
Regolare il runtime
Una volta che il prompt ha la dimensione giusta, quattro manopole decidono quanta finestra puoi davvero permetterti. Tutte vengono dalla FAQ e dalla documentazione di Ollama.
| Manopola | Cosa fa | Trappola |
|---|---|---|
OLLAMA_CONTEXT_LENGTH=65536 ollama serve (oppure num_ctx per richiesta, oppure PARAMETER num_ctx in un Modelfile) | Imposta la finestra | Un contesto più grande costa memoria. La documentazione lo dice senza giri di parole; la KV cache cresce linearmente con esso |
OLLAMA_KV_CACHE_TYPE=q8_0 | Quantizza la KV cache; circa metà della memoria dell'f16 di default. q4_0 è circa un quarto | Richiede flash attention; piccolo costo in qualità, maggiore con q4_0 |
OLLAMA_FLASH_ATTENTION=1 | Abilita flash attention; prerequisito per la quantizzazione della KV cache e riduce la memoria per contesti lunghi | Non tutti i backend/modelli la supportano; impostala e controlla il log del server |
OLLAMA_NUM_PARALLEL (default 1) | Richieste concorrenti per modello | La memoria richiesta scala con OLLAMA_NUM_PARALLEL × OLLAMA_CONTEXT_LENGTH. Due slot paralleli a 64K costano la memoria KV di uno a 128K |
Un ordine di operazioni utile con un budget di memoria fisso: scegli il modello più grande che riesci a tenere con una finestra piccola, poi abilita flash attention e la KV cache q8_0, poi alza il contesto finché il log del server non mostra che trabocca sulla CPU. Il contesto diviso tra GPU e CPU è il punto in cui i token al secondo crollano; diversi commentatori HN hanno riportato modelli densi 27B–32B in FP8 con finestre molto lunghe su configurazioni a doppia GPU, ma su una singola macchina da 128 GB a memoria unificata il tetto pratico erano i 65K usati dall'autore.
:::tip Il prefix caching aiuta, ma solo per il prefisso
Ollama riporta prompt_eval_cached_count in ogni risposta. Un system prompt stabile proprio all'inizio della conversazione viene riutilizzato dalla KV cache tra una richiesta e l'altra, quindi il suo costo si paga una volta per sessione, non per turno. Tutto ciò che viene dopo il primo cambiamento nella conversazione viene ricalcolato. Metti il materiale fisso all'inizio e quello volatile alla fine, la stessa regola del prompt caching di Claude (Economia del prompt caching).
:::
Checklist di migrazione
- Tokenizza il system prompt con il tokenizer locale e annota il numero.
- Imposta
num_ctxesplicitamente. Non affidarti mai al default; dipende dalla VRAM della macchina. - Decidi tra troncare o andare in errore. Per gli agenti, invia
"truncate": falsee gestisci il 400. - Spezza il prompt per obiettivo; nucleo condiviso sotto i 2K token; ogni unità 2–4K.
- Passa le consegne tra unità tramite un file; chiudi la sessione dopo ogni unità.
- Logga
prompt_eval_countper turno e lancia un allarme quando si appiattisce. - Abilita flash attention e la KV cache
q8_0prima di alzare la finestra. - Controlla il log del server alla ricerca di
truncating input promptdurante la prima settimana.
Verifica quello che hai imparato
0/5Fonti e approfondimenti
- Notes on gotchas while migrating 35 KB preprompts from Opus to self-hosted Ollama — il post di settembre 2026 che ha motivato questa pagina (Ryzen AI MAX+ 395 da 128 GB, finestra da 65K, modello 27B, harness OpenCode), e la sua discussione su Hacker News
- Documentazione Ollama: context length — i default a scaglioni di VRAM e
OLLAMA_CONTEXT_LENGTH - FAQ di Ollama —
OLLAMA_KV_CACHE_TYPE,OLLAMA_FLASH_ATTENTION,OLLAMA_NUM_PARALLELe la regola di memoria - Sorgente Ollama:
llm/llama_server.go— il codice di context shifttruncating input prompte la sua tabella di test;api/types.goper il defaultnum_keep: 4;server/routes.gopertruncatea true di default - Riferimento API di Ollama —
prompt_eval_count,prompt_eval_cached_count,options.num_ctx,keep_alive
Prossimi passi
- Claude più modelli locali — i pattern ibridi che tengono il prompt grande su Claude e mandano in locale i passi piccoli a obiettivo singolo
- Uno stack AI locale e privato — le scelte di runtime, harness e modello una volta che il prompt entra nella finestra
- Gestione del contesto in Claude Code — cosa fa per te la compattazione su Claude, così sai cosa ricostruire a mano in locale