Lição 6: Prática: ligando a gestão de contexto ao harness
Objetivos de aprendizado:
- Ligar o rastreamento de uso de tokens a um loop de harness dirigido por stop_reason: acumular com
response.usage, decidir se o contexto está se aproximando do limite da janela
- Transformar o
compact() da Lição 4 em um mecanismo disparado por limiar: definir a razão de disparo, pensar até o fim como messages e o contador de uso devem ser redefinidos depois do disparo
- Ligar notas estruturadas a esse fluxo de compactação, de modo que o
NOTES.md seja lido de volta sempre que uma janela nova reiniciar, e rodar uma tarefa que excede a capacidade de uma única janela
Pré-requisitos: Você concluiu a Lição 4 sobre compactação e notas, a Lição 5 sobre isolamento de subagentes, e tem à mão o loop de harness do curso 7 desta série, “Fundamentos do Harness de Agente: Laços e Controle” | Anterior: Lição 5 <<
Primeiro, veja rodando
As primeiras cinco lições foram todas de princípios: por que o contexto é um recurso finito, como a compactação funciona, como as notas funcionam, como os subagentes isolam. Esta lição solda os dois primeiros — compactação e notas — ao loop de harness que você escreveu no curso 7 desta série. Primeiro veja como fica rodando, depois desembrulhamos o código.
Abaixo está um registro de execução real (usando um cliente stub para simular respostas de modelo em vários turnos, de modo que uma tarefa longa caiba em poucas linhas de log; a implementação do cliente stub aparece no fim desta lição). A tarefa é aquele cenário familiar da Lição 4: corrigir um bug de corrida por concorrência em um serviço de pedidos. Para disparar a compactação em poucos turnos, a demonstração define deliberadamente uma janela de contexto minúscula:
Leia linha a linha: as três primeiras chamadas empurram tokensUsed de 400 para 1050 e para 1850; depois que os resultados de ferramenta da terceira rodada são anexados ao histórico, o valor acumulado cruza o limiar definido, então o harness não espera a janela de fato estourar — ele dispara proativamente uma chamada de compactação (chamada 4; o stop_reason deixa de importar porque essa resposta nunca entra no loop principal, sua saída é usada diretamente para reiniciar messages); tokensUsed volta a zero; as duas rodadas seguintes contam do zero na janela nova até o modelo encerrar. O processo inteiro tem apenas um artefato visível ao usuário — aquela resposta final; compactação e notas acontecem nos bastidores. O resto desta lição é construir, linha por linha, o código por trás daquele log.
I. Ligue o rastreamento de uso de tokens ao loop
O primeiro passo é direto: saber quantos tokens você usou até agora, porque esse é o pré-requisito para decidir se compacta. Você já usou este campo quando escreveu a válvula de orçamento (válvula 2) no curso 7 desta série — response.usage carrega input_tokens e output_tokens desta chamada, e a cada resposta recebida você os soma a um acumulador:
A válvula TOKEN_BUDGET do curso 7 usava esse valor acumulado para uma coisa só: parar ao bater no teto. Esta lição faz outra coisa: compactar proativamente a uma razão bem mais cedo e continuar trabalhando, em vez de parar. As duas usam o mesmo acumulador; o que acontece depois do disparo é completamente diferente — uma freia, a outra respira fundo.
Qual é o tamanho da janela em si? Isso é uma constante que você define com seu próprio julgamento de engenharia:
Não existe resposta padrão para quanto deve valer COMPACT_RATIO — é um julgamento de engenharia, não uma cláusula de especificação. Defina-o alto demais e, quando você perceber que “é hora de compactar”, a janela pode estar tão apertada que nem dá para enviar a próxima requisição; defina-o baixo demais e a compactação vai interromper a tarefa mais cedo e mais vezes do que deveria, desperdiçando chamadas de modelo. Como regra prática, deixar cerca de 30% de folga (ou seja, um limiar de 0,7) costuma funcionar; o número específico deve ser ajustado com base no tamanho real da janela do seu modelo e no volume das saídas de ferramenta de um turno.
II. Compactação disparada por limiar: ligue o compact() da Lição 4 ao loop
Com o mecanismo definido, o passo seguinte é ligá-lo ao corpo do loop. Lembre da conclusão da Lição 4: a compactação não entulha um resumo de volta na conversa antiga e continua espremendo — ela reinicia uma janela nova com o resumo, abandonando o messages antigo por inteiro1. Ligado ao loop, isso significa substituir messages no momento certo:
Três posições determinam se este código está correto:
- Onde a verificação fica: imediatamente depois que os resultados de ferramenta desta rodada são anexados a
messages, antes do próximo client.messages.create. Cedo demais (verificar antes de anexar) perde as saídas de ferramenta recém-produzidas; tarde demais (anexar depois de verificar) envia conteúdo já acima do limiar em uma requisição extra.
- Depois da compactação,
messages é substituído por atacado, não acrescido: o valor de retorno de compact() é atribuído diretamente a messages, e o array antigo com suas dezenas de idas e vindas de ferramenta é descartado — essa é a fronteira entre “reiniciar” e “continuar empilhando na conversa antiga”.
tokensUsed tem de voltar a zero: a janela nova parte de um resumo, então o uso deve ser contado a partir desse resumo, e não continuar carregando o valor acumulado da janela antiga. Pular esse passo é uma armadilha comum; os exercícios desta lição vão diagnosticá-la especificamente.
O compact() em si reaproveita a implementação da Lição 4; a COMPACT_INSTRUCTION e os princípios de triagem (preservar decisões arquiteturais, bugs não resolvidos e detalhes de implementação importantes; descartar saídas de ferramenta redundantes) permanecem os mesmos1. A próxima seção acrescenta a ele uma capacidade nova: no reinício, ler não só o resumo, mas também o NOTES.md.
III. Notas estruturadas como rede de segurança: o NOTES.md é lido de volta durante a compactação
A compactação é passiva e a posteriori — ela resume “o que sobrou na janela no momento do disparo”. A Lição 4 já explicou que as notas são um seguro ativo, escrito conforme se avança: o agente escreve decisões e problemas em um NOTES.md fora da janela no instante em que eles acontecem1. O jeito de ligar os dois é direto: quando uma janela nova reinicia, além de ler o resumo, leia também o NOTES.md de volta — assim, mesmo que a triagem do resumo desta rodada tenha errado, as notas ainda têm uma cópia de segurança independente.
Primeiro, dê ao agente uma ferramenta para escrever notas:
O input de update_notes é o conteúdo completo da nota a salvar, e a implementação o escreve por atacado — esta é a semântica mais simples: o agente mantém um corpo de notas completo e toda atualização significa “este é o estado atual”, sem nenhuma fusão incremental a resolver. Ligue um requisito ao system prompt: “Sempre que tomar uma decisão importante, descobrir um problema novo ou concluir uma fase, chame update_notes para atualizar as notas antes de continuar”, exatamente como na Lição 4.
Vem então o passo novo desta lição: depois que compact() gera o resumo, ele também lê o NOTES.md para dentro da mensagem de reinício:
Quando a janela nova acorda, ela tem dois materiais: o resumo do próprio modelo e as notas que o agente escreveu à mão. O primeiro pode perder detalhe por causa da triagem do resumo; as segundas são sem perdas — isso é o “quanto mais diligentes as notas, mais leve a consequência de a compactação deixar algo cair” da Lição 4 em forma de código.
IV. Junte tudo: uma tarefa que excede a capacidade de uma única janela
Três componentes — rastreamento de uso, compactação disparada por limiar, leitura e escrita do NOTES.md — ligados ao mesmo runAgent produzem o código completo por trás do log de abertura:
Esta é a origem completa do log de abertura: três chamadas de ferramenta empurram tokensUsed de 400 para 1050 e para 1850, cruzando a linha de limiar 2000 * 0.7 = 1400; compact() é chamado, messages é substituído por atacado, o contador volta a zero; então mais duas rodadas na janela nova, e o modelo encerra. Só uma compactação aconteceu na execução inteira, mas se a tarefa continuasse e batesse no limiar de novo, a mesma lógica dispararia uma segunda, uma terceira vez — shouldCompact não se importa com qual janela é esta, ele só olha o uso da janela atual. É isso que “rodar uma tarefa que excede a capacidade de uma única janela” significa: o comprimento total da tarefa não é limitado pela capacidade de nenhuma janela isolada, apenas limitado por “uma inferência contínua e ininterrupta”.
Para rodar uma verificação completa, conecte um cliente stub que simula respostas de modelo em vários turnos (troque-o por new Anthropic() em chamadas reais; o código de runAgent não muda um caractere):
O valor de verificar com um cliente stub assim é que ele fixa “o modelo vai chamar ferramentas neste turno, quantos tokens ele usou” como quantidades conhecidas, de modo que se a compactação dispara e em qual turno, se tokensUsed volta a zero, se o NOTES.md é escrito e depois lido de volta — tudo pode ser checado com asserções em vez de apertar os olhos para a saída de chamadas reais e adivinhar.
Senso de proporção: nem toda tarefa precisa desse maquinário
Depois de ligar tudo isso, é fácil desenvolver uma impressão falsa: de agora em diante, escrever agentes deveria incluir por padrão o pacote de rastreamento de uso, compactação por limiar e notas estruturadas. Volte ao senso de proporção estabelecido na Lição 2: considere adicionar complexidade apenas quando isso puder melhorar comprovadamente os resultados2. Para tarefas que terminam em uma dúzia de turnos, compactação e notas são ambas peças supérfluas — comece pelo loop pelado mais as válvulas de controle básicas do curso 7 desta série, e só acrescente esta camada quando você de fato bater no limite da janela ou vir sintomas de amnésia do tipo “a janela nova não sabe o que a janela antiga fez”.
Neste ponto, tudo o que este curso ensinou da Lição 1 à Lição 6 — orçamento de atenção, altitude dos system prompts, recuperação sob demanda, compactação e notas, isolamento de subagentes — converge para a mesma lição: o que o modelo deve ver a cada turno é sempre um julgamento de engenharia que você revisita continuamente, não uma configuração única que você define e esquece.
Recapitulação
- Ligar o rastreamento de uso ao harness precisa só de um acumulador: a cada resposta recebida, some
response.usage.input_tokens + response.usage.output_tokens; ele compartilha os mesmos dados com a válvula TOKEN_BUDGET do curso 7, mas a ação depois do disparo difere — a válvula de orçamento para ao encher; o limiar desta lição compacta e continua trabalhando.
- A compactação disparada por limiar liga o
compact() da Lição 4 ao corpo do loop: a verificação fica em “resultados de ferramenta desta rodada totalmente anexados, antes de a próxima requisição sair”; depois do disparo, messages é substituído por atacado pelo resultado da compactação — é um reinício, não um acréscimo1; tokensUsed tem de ser redefinido em sincronia, senão você cai em uma tempestade de compactações repetidas.
- Notas e compactação se ligam nesta lição: a ferramenta
update_notes escreve conforme se avança — isso é persistir notas fora da janela de contexto1 — e compact() lê o NOTES.md de volta para a mensagem de reinício além de gerar o resumo; o resumo pode perder conteúdo por causa da triagem, as notas são lidas de volta como uma cópia sem perdas. “Ler uma vez só em momentos críticos como o reinício da janela, não enfiar no system prompt de todo turno” é o trade-off de engenharia desta lição, baseado no princípio do orçamento de atenção (cada novo token esgota esse orçamento1).
- Uma verificação real de ponta a ponta mostra: três chamadas de ferramenta empurram o uso de 400 para 1850, cruzam o limiar e disparam uma compactação, o contador volta a zero, e então mais duas rodadas encerram — o comprimento total da tarefa deixa de ser limitado pela capacidade de uma única janela, ficando limitado apenas por “uma inferência contínua e ininterrupta”.
- Não trate este maquinário como configuração padrão: só o acrescente quando a complexidade extra puder melhorar comprovadamente os resultados2; para tarefas que terminam em poucos turnos, o loop pelado mais as válvulas de controle básicas do curso 7 desta série bastam.