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.

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:
- Arquivos 1-174: Processamento normal. O contexto de cada documento cabia na window.
- 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.
- 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, ollama.cppvai 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)

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.
