O ChatCLI implementa uma série de otimizações para manter o consumo de tokens sob controle em sessões longas com os modos /agent e /coder. Esta página explica o que roda sem você mexer em nada e quais interruptores estão disponíveis quando o comportamento default não serve para o seu workflow.
Todas as otimizações desta página funcionam em todos os provedores suportados (Anthropic direto, Bedrock, OpenAI, xAI, ZAI, MiniMax, Moonshot (Kimi), Google AI, Ollama, Copilot, GitHub Models, OpenRouter, OpenAI Assistant, StackSpot). Providers com prompt caching explícito (Anthropic, Bedrock Anthropic) ou auto-caching (OpenAI, xAI) aproveitam ganhos adicionais.
O problema real
Um loop ReAct mal calibrado consegue transformar uma pergunta trivial num consumo absurdo de tokens. Sem as otimizações abaixo, uma query como “resultado do jogo do Flamengo” pode facilmente queimar 20k+ tokens porque:
- O system prompt inteiro é re-enviado em cada turno (sem cache).
- As definições de tools (15+ schemas JSON) também vão re-enviadas por turno.
- Nada quebra o loop quando o modelo repete a mesma tool_call sem convergir.
- Bodies grandes do
@webfetch entram crus no contexto.
Em agregado, cada turno pesa 4-8k tokens de overhead puro, multiplicado por 3-5 turnos de uma sessão normal = 12-40k tokens antes mesmo de contar a resposta útil.
1. Prompt Caching estruturado
O cache da Anthropic (e o auto-cache de prefixo do OpenAI/xAI) funciona por prefixo: um breakpoint só dá hit quando todos os bytes antes dele são idênticos a uma requisição anterior. A regra de ouro é simples — blocos estáveis primeiro, blocos voláteis no fim. O system prompt é montado exatamente assim:
Prefixo estável (cacheado, cache_control: ephemeral):
| Bloco | Conteúdo | Por que é estável |
|---|
| Core | Persona + regras de formato (Coder/Agent) + hint de idioma | Não muda entre turnos |
| Tools | Descrições de plugins + hint do workspace | Muda só quando um plugin é carregado/descarregado |
| Orchestrator | Catálogo do orquestrador multi-agente | Estável dentro da sessão |
| Índice de memória | Digest compacto da memória (modo index) | Não depende do turno — ver seção 7 |
Sufixo volátil (sem cache marker — muda a cada turno):
| Bloco | Conteúdo | Por que é volátil |
|---|
| Memória (full) | Recuperação hint-driven de MEMORY.md (modo full) | Varia com os hints do turno |
| Skills | Skills auto-ativadas pela query | Depende da pergunta |
| MCP channel | Mensagens push recentes dos servidores MCP | Atualiza a cada turno |
| Contexto dinâmico | Data/hora + diretório atual | Muda por definição |
Por que a ordem importa (defeito corrigido): antes, o bloco de workspace+memória carregava cache_control mas continha o timestamp ao segundo e a recuperação hint-driven — ambos voláteis — no topo do prompt. Isso garantia cache miss nesse bloco a cada turno e envenenava todos os blocos cacheados abaixo dele (contextos /context, pinned skills, catálogo MCP): pagava-se cache creation (1.25×) toda vez e nunca se colhia um read. O timestamp agora vive num bloco próprio no fim, e a memória volátil saiu do prefixo. Os blocos verdadeiramente estáveis formam um prefixo contíguo que casa o cache.
Cada bloco estável carrega cache_control: ephemeral para os providers Anthropic (respeitando o teto de 4 breakpoints, com coalescing automático). Para providers com auto-caching de prefixo (OpenAI, xAI), a ordem estável faz o cache casar naturalmente. O chat segue a mesma lógica de ordenação; como é tool-less, ele não puxa memória sob demanda (ver seção 7).
A última definição de ferramenta enviada para a Anthropic também recebe cache_control: ephemeral, o que transforma todo o array de tools num prefixo cacheável. Em uma sessão /coder com 15 ferramentas coder + 2 web tools, isso representa ~19KB que deixam de ser re-tokenizados a cada turno.
Visibilidade do cache
| Provider | Campo populado em UsageInfo |
|---|
| Anthropic / Bedrock Anthropic | CacheReadInputTokens, CacheCreationInputTokens |
| OpenAI Chat Completions (auto-caching) | CacheReadInputTokens (via prompt_tokens_details.cached_tokens) |
| OpenAI Responses API (auto-caching) | CacheReadInputTokens (via input_tokens_details.cached_tokens) |
| OpenAI reasoning models (o-series / GPT-5) | ReasoningTokens (via *_tokens_details.reasoning_tokens) — informativo, já contabilizado em CompletionTokens |
| Demais providers | Não reportado — mas prefixo estável ainda beneficia cache interno |
Cached tokens do OpenAI não exigem opt-in — prompt caching é automático em gpt-4o e mais novos (incluindo o-series e GPT-5), disparado quando o prefixo do prompt tem ≥1.024 tokens, com hits servidos em incrementos de 128 tokens. Para streaming Chat Completions, o ChatCLI envia stream_options: {include_usage: true} para o chunk terminal de usage chegar antes do [DONE]; na Responses API, o usage vem no evento SSE response.completed sem flag adicional.
Verifique o impacto real da sua sessão com /cost — o hit de cache aparece como linha separada. O envelope do chat também mostra N↑ M↓ na borda direita para qualquer provider que reporte usage, incluindo todas as APIs do OpenAI.
2. Detector de estagnação (early-exit)
Quando o modelo entra em reflection loop — emitindo exatamente a mesma batch de tool_calls turno após turno sem informação nova — o ChatCLI detecta e quebra o loop.
Como funciona
A cada turno, o fingerprint das tool_calls (nome + args normalizados, order-independent, SHA-256 truncado) é computado. Três turnos consecutivos com o mesmo fingerprint → o loop é encerrado com uma mensagem clara ao usuário.
Parâmetros
| Variável | Default | Descrição |
|---|
CHATCLI_AGENT_EARLY_EXIT | 1 (on) | Liga/desliga o detector. 0/false/off desativa. |
CHATCLI_AGENT_EARLY_EXIT_TURNS | 3 | Número de repetições consecutivas para acionar o break (clamp [2, 10]). |
Fingerprint é order-independent: [read A, read B] e [read B, read A] produzem o mesmo hash, então reordenamentos cosméticos não enganam o detector.
3. Smart Routing chat ↔ agent
Nem toda query precisa de um loop ReAct inteiro. Perguntas conversacionais ou factuais (“o que é um mutex?”, “diferença entre slice e array”) são respondidas por um único turno em chat mode.
O classificador identifica queries triviais usando sinais léxicos:
- Palavras-chave de pergunta (
o que, por que, how does, explain, …)
- Ausência de sinais de task (
create, build, run, fix, …)
- Ausência de referências ao workspace (
@file, @git, paths, extensões de código)
- Tamanho curto + ponto de interrogação
Modos
Valor de CHATCLI_AGENT_SMART_ROUTE | Comportamento |
|---|
off | 0 | false | no | Desliga completamente. /agent e /run sempre entram no loop. |
hint (default) | 1 | on | true | Detecta e imprime uma dica no terminal, mas respeita a intenção do usuário e entra no loop mesmo assim. |
auto | redirect | 2 | Auto-redireciona queries triviais para chat mode. Máxima economia; pode surpreender em casos limítrofes. |
/coder nunca é reroteado — esse modo existe para tarefas estruturadas. Mesmo perguntas aparentemente triviais ali são tratadas como pedido de trabalho.
Exemplo
$ /agent "o que é um canal em Go?"
ℹ Dica: essa pergunta parece conversacional — o loop /agent foi ignorado.
Use /chat ou digite a pergunta direto para forçar chat, ou /run para forçar o agente.
# Com CHATCLI_AGENT_SMART_ROUTE=auto, a pergunta vai direto ao chat.
# Com o default (hint), você vê a dica mas o agente roda normalmente.
O @webfetch foi calibrado para nunca vomitar páginas gigantes no contexto. Veja também WebFetch & WebSearch para a documentação completa.
| Parâmetro | Antes | Agora |
|---|
max_length default | 50.000 chars | 20.000 chars |
| Auto-save quando body > 10KB sem filtro | não | sim — salva no scratch dir e retorna preview compacto |
Escape hatch
| Variável | Descrição |
|---|
CHATCLI_WEBFETCH_AUTOSAVE_BYTES | Threshold em bytes para o auto-save disparar. Default: 10000. |
O auto-save sempre persiste o body completo (pré-filtro) em $CHATCLI_AGENT_TMPDIR, e o retorno contém:
[auto-saved: response was 142318 bytes — too large to inline.
Full body is at /tmp/chatcli-agent-.../webfetch_1712....txt.
Preview below; use read_file with start/end or rerun with
filter/from_line/to_line for specific ranges.]
[primeiros ~5000 chars do texto extraído]
...(auto-truncated — full body saved to disk)
O agente tipicamente emite um read_file apontando para esse caminho com o start/end apropriado, pagando apenas pelas linhas que importam.
5. System prompts enxutos
Os prompts que acompanham cada modo foram condensados sem perda semântica — todas as regras originais permanecem, apenas redundância e exemplos repetidos foram removidos:
| Prompt | Tamanho antes | Tamanho agora | Redução |
|---|
CoderSystemPrompt | ~1.647 tokens | ~1.000 tokens | ~40% |
CoderFormatInstructions | ~560 tokens | ~390 tokens | ~30% |
AgentFormatInstructions | ~324 tokens | ~230 tokens | ~30% |
OrchestratorSystemPrompt | ~2.111 tokens | ~1.050 tokens | ~50% |
Como esses prompts são cacheados no bloco core, modelos que suportam cache (Anthropic/Bedrock/OpenAI) enxergam a redução só no primeiro turno da sessão. Modelos sem cache ganham a economia em todo turno.
Resultados de ferramentas antigas (file reads, search, git-diff, etc.) são progressivamente comprimidos na história para não inflar o payload. Veja Tool Result Management para os detalhes completos.
Os defaults são conservadores para proteger workflows multi-turnos com cross-references (refactors grandes, review sessions). Quem quiser ser mais agressivo pode ajustar:
| Variável | Default | Descrição |
|---|
CHATCLI_MICROCOMPACT_TRUNCATE_TURNS | 2 | Após quantos turnos os tool results antigos são truncados para head+tail preview. |
CHATCLI_MICROCOMPACT_SUMMARIZE_TURNS | 4 | Após quantos turnos os tool results são substituídos por uma linha de resumo. |
CHATCLI_MICROCOMPACT_HEAD_CHARS | 2000 | Tamanho do head mantido na truncagem. |
CHATCLI_MICROCOMPACT_TAIL_CHARS | 500 | Tamanho da cauda mantida na truncagem. |
CHATCLI_MICROCOMPACT_MIN_CONTENT | 3000 | Tamanho mínimo de tool result para virar candidato à compactação. |
Para sessões de chat/lookup onde rapidez e tokens baixos importam mais que recall de longo prazo, tente:export CHATCLI_MICROCOMPACT_TRUNCATE_TURNS=1
export CHATCLI_MICROCOMPACT_SUMMARIZE_TURNS=3
export CHATCLI_MICROCOMPACT_HEAD_CHARS=1200
export CHATCLI_MICROCOMPACT_TAIL_CHARS=300
export CHATCLI_MICROCOMPACT_MIN_CONTENT=2000
7. Memória pull-first (índice + recall)
Empurrar a memória inteira no system prompt a cada turno não escala: o custo cresce com o tamanho do store e é re-enviado em todo turno. A partir de agora o ChatCLI usa por padrão um modelo pull: injeta só um digest estável e deixa o agente puxar o detalhe sob demanda via a ferramenta @memory recall.
Controle pela variável CHATCLI_MEMORY_MODE:
| Modo | Comportamento | Quando usar |
|---|
index (default) | Injeta um índice compacto e estável (resumo do perfil + nomes dos top topics/projects + contagem de fatos por categoria) e uma diretiva para o agente chamar @memory recall quando precisar de detalhe. | Default. Custo por turno limitado mesmo com a memória crescendo. |
full | Injeta a recuperação hint-driven completa todo turno (comportamento anterior). | Quando você quer que o agente sempre veja a memória relevante sem depender de ele puxar. |
off | Não injeta memória (bootstrap continua valendo). | Sessões onde a memória de longo prazo só atrapalha. |
O index é estável (não depende dos hints do turno, sem timestamp), então entra no prefixo cacheado (seção 1) e tem tamanho limitado independente do tamanho do store. O @memory recall usa o stack completo de recuperação (HyDE + busca vetorial por cosseno + extração de keywords), então o detalhe puxado tem a mesma qualidade do antigo push.
Impacto medido
Medição num store real com 500 fatos (MEMORY.md ~32KB, índice de fatos ~270KB):
| Por turno (agent/coder) | chars | ~tokens |
|---|
Push (full) | 3.946 | ~986 |
Índice (index) | 486 | ~121 |
−87,7% no bloco de memória por turno — e, ao contrário do full (limitado pelo CHATCLI_MEMORY_RETRIEVAL_BUDGET), o índice não cresce conforme a memória cresce.
Chat é tool-less por design e não pode puxar sob demanda: lá index cai automaticamente em full, e só off suprime a memória. O modo é exibido em /config memory.
No modo index o agente/coder não enxerga mais a memória inteira automaticamente — ele precisa chamar @memory recall. O índice dá o “mapa” (o que existe) para ele saber o que puxar. Se notar o agente perdendo contexto que deveria recordar, volte para CHATCLI_MEMORY_MODE=full (a economia de cache da seção 1 continua valendo).
Como medir o impacto
Rode sua sessão normalmente e olhe o /cost ao final:
Session cost summary
Provider: CLAUDEAI | Model: claude-sonnet-4-6
─────────────────────────────────────────────
Input tokens: 12_345
Output tokens: 3_210
Cache read: 87_650 ← ideal: crescendo a cada turno
Cache creation: 4_100
─────────────────────────────────────────────
Total cost: $0.0234
Os sinais que indicam que as otimizações estão ativas e funcionando:
Cache read > 0 e crescendo por turno → caching estruturado está casando o prefixo.
- Poucos/nenhum FORMAT ERROR no log → reminders estão segurando o formato nos modelos menores.
- Turnos com
tool_calls = 0 seguidos de conclusão rápida → early-exit detectou convergência.
- Marker
[auto-saved: response was N bytes] em respostas de @webfetch → o limite inline está protegendo o contexto.
Resumo das variáveis
Todas as variáveis desta página em um só lugar:
| Variável | Default | Desliga com |
|---|
CHATCLI_MEMORY_MODE | index | full (push) / off |
CHATCLI_AGENT_EARLY_EXIT | 1 (on) | 0 / false / off / no |
CHATCLI_AGENT_EARLY_EXIT_TURNS | 3 | — (clamp [2, 10]) |
CHATCLI_AGENT_SMART_ROUTE | hint | off / 0 / false / no |
CHATCLI_WEBFETCH_AUTOSAVE_BYTES | 10000 | setar valor muito alto |
CHATCLI_MICROCOMPACT_TRUNCATE_TURNS | 2 | valor alto (ex: 100) |
CHATCLI_MICROCOMPACT_SUMMARIZE_TURNS | 4 | valor alto |
CHATCLI_MICROCOMPACT_HEAD_CHARS | 2000 | valor alto |
CHATCLI_MICROCOMPACT_TAIL_CHARS | 500 | valor alto |
CHATCLI_MICROCOMPACT_MIN_CONTENT | 3000 | valor muito alto |
Próximos passos