Sala de servidor moderna com iluminação azul representando o limite invisível da context window em modelos de IA local

Meu Pipeline de IA Gerou 4 Horas de Dados Falsos — e o Log Dizia “Zero Errors”

Tem uma coisa que ninguém te conta quando você começa a automatizar tarefas com IA: o limite é invisível até te derrubar. E quando derruba, é em cadeia — tudo ao mesmo tempo, no pior horário possível.

Eu descobri isso da pior forma. Tinha montado um pipeline de análise de documentos com um modelo local — tudo lindo, tudo funcionando nos testes. Coloquei pra rodar em produção com 200 arquivos. Nas primeiras 174, perfeito. Da 175 em diante? Respostas cortadas ao meio. Alucinações. O modelo começava a inventar dados que não existiam. E o pior: nada no log indicava erro. Só resultado podre.

O culpado? A context window — esse limite invisível de tokens que todo modelo de linguagem tem, e que transforma seu pipeline brilhante em uma bomba-relógio.

Neste post, vou te mostrar exatamente o que aconteceu, como detectei o problema, e as 4 estratégias que uso hoje pra nunca mais tropeçar nessa armadilha. Se você roda IA local ou usa APIs com automação, isso vai te poupar horas de dor de cabeça.

O Que é Context Window — E Por Que Ela É Uma Bomba-Relógio

Context window é o “espaço de trabalho” do modelo. É a quantidade máxima de tokens (palavras, caracteres, comandos) que ele consegue processar de uma vez — incluindo o que você envia e o que ele gera como resposta.

Pensa assim: é como a memória RAM do seu computador. Você pode ter um processador incrível, mas se a RAM enche, o sistema trava. Com modelos de IA, a diferença é que o travamento é silencioso. O modelo não te avisa que tá sem espaço. Ele simplesmente começa a produzir lixo.

Infraestrutura de servidor representando os limites invisíveis da context window em modelos de IA local
Assim como cabeos em um data center, os tokens se acumulam até encontrar o limite físico. A diferença é que o modelo não pisca um LED vermelho.

Os Limites Reais dos Modelos Populares

Antes de falar da solução, vamos entender o tamanho do problema. Aqui estão os limites de context window dos modelos que eu mais uso em automações locais:

  • Llama 3.1 8B: 128K tokens — parece muito, mas some rápido quando você adiciona system prompt + contexto + histórico
  • Mistral 7B: 8K tokens — isso dá menos de 6 mil palavras. Um documento médio de contrato já estoura
  • Phi-3 Mini: 128K tokens — generoso, mas roda lento em hardware modesto
  • Qwen 2.5 7B: 131K tokens — excelente, mas consome ~16GB de RAM só pra carregar o modelo
  • Gemma 2 9B: 8K tokens — o mesmo problema do Mistral

O ponto é: mesmo os modelos com janelas grandes têm um limite. E quando você automatiza — processando dezenas ou centenas de documentos — esse limite é uma questão de quando, não de se.

O Dia Que Meu Pipeline Faliu (E Eu Nem Percebi)

O cenário era simples em teoria: pegar PDFs de contratos, extrair cláusulas específicas, e gerar um relatório comparativo. Usei um fluxo com llama.cpp rodando em um servidor com 32GB RAM. O modelo era o Qwen 2.5 7B.

Configurei o system prompt, adicionei o template, testei com 3 contratos. Resultado perfeito.

Disparei os 200 contratos. Fui fazer outra coisa.

Quando voltei, o pipeline tinha “terminado com sucesso”. Zero erros no log. Zero exceptions. Mas o relatório final era inutilizável. Cláusulas misturadas entre contratos. Valores inventados. Referências circulares que não existiam nos documentos originais.

O que aconteceu?

O Diagnóstico: Context Saturation

Depois de investigar, descobri o padrão exato do problema:

  1. Arquivos 1-174: Processamento normal. O contexto de cada documento cabia na window.
  2. Arquivos 175-188: O contexto começou a acumular. Meu script guardava o histórico da conversa entre processamentos pra “manter consistência”. Cada resposta nova somava ao contexto da próxima.
  3. Arquivo 189+: A context window transbordou. O modelo começou a truncar silenciosamente o input — cortando partes do system prompt e do documento atual. O resultado? Alucinações puras.

O sistema reportou “sucesso” porque nunca pediu pro modelo verificar se havia estourado o limite. E os modelos open-source, na maioria dos casos, simplesmente cortam o que não cabe — sem avisar.

🔧 O Perrengue do Olivetto

Erro: Pipeline de análise de 200 contratos gerando dados inventados sem nenhum erro no log.

Causa: Acúmulo de contexto entre chamadas consecutivas — cada resposta era adicionada ao prompt da próxima, criando um efeito bola de neve que estourou a context window silenciosamente.

Como descobri: Comparei manualmente 10 resultados aleatórios com os originais. Os 3 primeiros batiam. Os 7 últimos eram ficção. O padrão de degradação foi a pista: quanto mais tarde no batch, pior a qualidade.

Tempo perdido: 4 horas rodando um pipeline quebrado + 3 horas de debugging. Tudo porque eu confiei no “zero errors” do log.

Estratégia 1: O Reset Obrigatório (Simples e Brutal)

A solução mais simples é também a mais eficaz: resetar o contexto a cada processamento. Não acumule histórico. Cada documento é uma conversa nova.

Aqui está o padrão que uso agora com llama-cpp-python:

from llama_cpp import Llama

llm = Llama(model_path="./qwen2.5-7b.Q4_K_M.gguf", n_ctx=4096)

def processar_documento(doc_text: str) -> str:
    """Processa um documento SEM acumular contexto."""
    
    # System prompt LIMPO a cada chamada
    prompt = f"""<|system|>
Você é um analista de contratos. Extraia APENAS as cláusulas solicitadas.
Se a informação não estiver no documento, responda "NÃO ENCONTRADO".
Nunca invente dados.<|end|>

<|user|>
Documento:
{doc_text[:3500]}  ← LIMITADO PROPOSITAMENTE

Extraia: valores, prazos e multas.<|end|>

<|assistant|>"""
    
    resposta = llm(
        prompt,
        max_tokens=512,
        temperature=0.1,  # Baixa = mais determinístico
        stop=["<|end|>"]
    )
    
    return resposta["choices"][0]["text"]

# Cada documento é independente
for doc in documentos:
    resultado = processar_documento(doc.texto)
    salvar_resultado(resultado)

O segredo aqui está em três decisões:

  • n_ctx=4096: Limite explícito. Se o input passar disso, o llama.cpp vai truncar — mas você sabe que isso pode acontecer.
  • doc_text[:3500]: Truncamento intencional do input. Melhor perder uma parte do documento do que estourar o contexto sem saber.
  • temperature=0.1: Mínimo de criatividade. Queremos extração, não improvisação.

Estratégia 2: O Sentinel de Tokens (Seu Cão de Guarda)

Resetar contexto resolve o problema, mas introduz outro: você não sabe quantos tokens está gastando por chamada. E isso importa quando você tem centenas de processamentos.

A solução é um “sentinel” que conta tokens antes de enviar:

import tiktoken  # Ou transformers.AutoTokenizer

# Para modelos baseados em Llama/Mistral
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("./qwen2.5-tokenizer")

def contar_tokens(texto: str) -> int:
    """Conta tokens reais do modelo, não caracteres."""
    return len(tokenizer.encode(texto))

def processar_com_limite(doc_text: str, max_tokens: int = 4096) -> str:
    system_prompt = """<|system|>Analise o contrato e extraia cláusulas.<|end|>"""
    user_prompt = f"<|user|>{doc_text}<|end|>"
    
    tokens_sistema = contar_tokens(system_prompt)
    reservado = 512  # Espaço pra resposta
    
    # Quanto sobra pro documento?
    disponivel = max_tokens - tokens_sistema - reservado
    
    if contar_tokens(user_prompt) > disponivel:
        # Trunca no limite exato de tokens
        tokens_doc = tokenizer.encode(doc_text)[:disponivel - 100]
        doc_truncado = tokenizer.decode(tokens_doc)
        user_prompt = f"<|user|>{doc_truncado}[... texto truncado ...]<|end|>"
    
    # Agora temos CERTEZA que cabe
    return enviar_para_modelo(system_prompt + "\n" + user_prompt)

A diferença entre contar tokens reais e estimar por caracteres é brutal. “Orçamentário” tem 14 caracteres mas conta como 3 tokens em alguns tokenizers. “Implementation” tem 14 caracteres e pode contar como 4-5 tokens. Sem contagem real, você está chutando.

Estratégia 3: Chunking Inteligente (Quando o Documento é Grande Demais)

Resetar contexto e contar tokens resolve a maioria dos casos. Mas e quando o documento legítimo é maior que a context window? Um contrato de 80 páginas num modelo de 8K tokens?

Aqui entra o chunking inteligente — dividir o documento em pedaços processáveis e depois recompor os resultados:

def chunk_documento(texto: str, max_tokens: int = 3500, overlap: int = 200) -> list[str]:
    """Divide documento em chunks com overlap pra não perder contexto nas bordas."""
    
    tokenizer = AutoTokenizer.from_pretrained("./tokenizer")
    tokens = tokenizer.encode(texto)
    
    chunks = []
    inicio = 0
    
    while inicio < len(tokens):
        fim = inicio + max_tokens
        
        # Tenta quebrar em ponto natural (fim de parágrafo)
        chunk_tokens = tokens[inicio:fim]
        chunk_texto = tokenizer.decode(chunk_tokens)
        
        # Se não é o último chunk, tenta encontrar um "\n\n" perto do fim
        if fim < len(tokens):
            ultima_quebra = chunk_texto.rfind("\n\n", -500)
            if ultima_quebra > 0:
                chunk_texto = chunk_texto[:ultima_quebra]
                chunk_tokens = tokenizer.encode(chunk_texto)
        
        chunks.append(chunk_texto)
        inicio += len(chunk_tokens) - overlap  # overlap evita perder contexto
    
    return chunks

def analisar_contrato_grande(texto_completo: str) -> dict:
    """Analisa um contrato grande processando chunks e agregando resultados."""
    
    chunks = chunk_documento(texto_completo)
    todos_resultados = []
    
    for i, chunk in enumerate(chunks):
        resultado = processar_documento(chunk)
        todos_resultados.append({
            "chunk": i + 1,
            "total_chunks": len(chunks),
            "resultado": resultado
        })
    
    # Agregação final com outro modelo (ou o mesmo, com contexto limpo)
    resumo_chunks = "\n---\n".join([r["resultado"] for r in todos_resultados])
    
    prompt_final = f"""<|system|>
Consolide os resultados abaixo removendo duplicatas.
Mantenha APENAS cláusulas únicas com seus valores.<|end|>

<|user|>Resultados de {len(chunks)} chunks:
{resumo_chunks}<|end|>"""
    
    return enviar_para_modelo(prompt_final)
Servidores com múltiplas conexões representando o processamento em chunks de documentos grandes com IA local
Processar em chunks é como distribuir carga entre servidores: cada um faz sua parte, e um orchestrator consolida o resultado final.

O overlap é o detalhe que faz a diferença. Se uma cláusula começa no final do chunk 2 e termina no início do chunk 3, sem overlap você perde metade da informação. Com 200 tokens de overlap, você garante que o contexto cruza a fronteira.

Estratégia 4: O Circuito de Emergência (Fail-Safe)

As três estratégias anteriores previnem problemas. Mas prevenção não é infalível. Por isso, todo pipeline que eu monto tem um circuito de emergência:

import json
from datetime import datetime

class CircuitoEmergencia:
    """Monitora a saúde do pipeline e dispara alertas quando algo sai do padrão."""
    
    def __init__(self, threshold_qualidade=0.7):
        self.threshold = threshold_qualidade
        self.historico = []
        self.alertas = []
    
    def avaliar_resposta(self, resposta: str, contexto: str) -> dict:
        """Verifica se a resposta parece válida."""
        score = 1.0
        
        # Teste 1: Resposta muito curta (modelo pode ter truncado)
        if len(resposta.strip()) < 20:
            score -= 0.4
            self._alertar("RESPOSTA_CURTA", resposta[:100])
        
        # Teste 2: Repetição excessiva (sinal de alucinação/loop)
        palavras = resposta.split()
        if len(palavras) > 10:
            unicidade = len(set(palavras)) / len(palavras)
            if unicidade < 0.3:
                score -= 0.3
                self._alertar("REPETICAO", f"Unicidade: {unicidade:.2f}")
        
        # Teste 3: Referências a informações fora do contexto
        # (heurística simples: procura nomes próprios não presentes no contexto)
        # Isso é simplificado — em produção, use NER ou embedding comparison
        
        resultado = {
            "score": max(0, score),
            "timestamp": datetime.now().isoformat(),
            "status": "OK" if score >= self.threshold else "SUSPEITO"
        }
        
        self.historico.append(resultado)
        
        if score < self.threshold:
            self._alertar("SCORE_BAIXO", f"Score: {score:.2f}")
        
        return resultado
    
    def _alertar(self, tipo: str, detalhe: str):
        alerta = {
            "tipo": tipo,
            "detalhe": detalhe,
            "timestamp": datetime.now().isoformat()
        }
        self.alertas.append(alerta)
        # Em produção: enviar pra Telegram, email, ou log estruturado
        print(f"⚠️ ALERTA [{tipo}]: {detalhe}")
    
    def resumo(self) -> str:
        total = len(self.historico)
        suspeitos = sum(1 for h in self.historico if h["status"] == "SUSPEITO")
        return f"Processados: {total} | Suspeitos: {susppeitos} | Taxa: {susppeitos/total:.1%}"

Esse circuito não impede erros, mas te avisa quando eles acontecem. A diferença entre “meu pipeline gerou dados errados por 4 horas” e “recebi um alerta no Telegram no minuto 12” é simplesmente ter monitoramento.

O Checklist Que Eu Sigo Agora (E Recomendo)

Depois do desastre dos 200 contratos, criei um checklist que uso antes de qualquer deploy de pipeline com IA local:

  • n_ctx explícito — nunca deixar o modelo decidir o limite sozinho
  • Contagem de tokens com o tokenizer real do modelo, não estimativa por caracteres
  • Reset de contexto entre processamentos independentes
  • Chunking com overlap para documentos grandes
  • Circuito de emergência com pelo menos 3 verificações automáticas de qualidade
  • Sample manual — validar 5-10 resultados aleatórios contra o original antes de confiar no batch completo
  • Temperature baixa (0.1-0.3) para tarefas de extração; reservar temperature alta pra geração criativa

O item mais importante é o último da lista: sample manual. Eu perdi 4 horas porque confiei no “zero errors” do log. Se eu tivesse verificado 5 resultados aleatórios nos primeiros 10 minutos, teria parado o pipeline no arquivo 10, não no 200.

Quando Usar Cada Estratégia

Não adianta ter ferramentas se você não sabe quando usar cada uma. Aqui está meu mapa mental:

  • Documentos pequenos (< 2K tokens): Reset obrigatório + contagem de tokens. Simples e suficiente.
  • Documentos médios (2K-8K tokens): Chunking com overlap + circuito de emergência.
  • Documentos grandes (8K+ tokens): Chunking agressivo (overlap maior) + agregação final + circuito de emergência.
  • Batch processing (100+ documentos): Tudo acima + sample manual a cada 50 documentos.

A Lição Que Ficou

Automação com IA local é poderosa. Mas é como dirigir um carro esportivo: quanto mais rápido você vai, mais importante é ter bons freios.

A context window é o limite físico do seu modelo. Não tem workaround mágico. O que existe é design consciente — saber onde estão os limites, monitorar quando você chega perto, e ter planos de contingência pra quando inevitavelmente estourar.

O pipeline que eu reescrevi com essas 4 estratégias processou os mesmos 200 contratos em 2 horas — sem uma única alucinação. E se algum dia começar a degradar de novo, o circuito de emergência me avisa antes que eu precise comparar manualmente.

Essa é a diferença entre automação que funciona e automação que parece que funciona.

E você? Já teve algum pipeline de IA que te pregou uma peça silenciosa? Conta aí nos comentários — erro compartilhado é erro pela metade.

Posts Similares