Anatomia de um harness em produção: as 6 camadas que separam POC de sistema confiável
O harness do tutorial roda. Você cola o snippet, roda local, vê o agente respondendo, fica feliz.
Aí coloca em produção.
Primeira semana: alguém manda um payload de 80kb dentro do prompt e o loop vai pra 20 turnos sem terminar. Custo do dia em uma única conversa: 90 dólares. Segunda semana: prompt injection escondido num e-mail que veio via tool de leitura de inbox dispara uma chamada de API que não devia. Terceira semana: o agente começa a alucinar campos de JSON que o consumidor a jusante não sabe tratar e o monitor não viu nada porque ninguém instrumentou a saída.
Tudo isso é evitável. Nada disso aparece no tutorial.
Esse post mapeia as seis camadas que ficam entre o request e o response quando seu agente está vivo 24 por 7. É a anatomia de um harness em produção, com código de referência, padrões abertos e a dor real de cada pedaço.
TL;DR
- O que é: mapa das seis camadas que precisam existir num harness em produção (gate, router, contexto, loop, pós-processamento, telemetria).
- Stack de referência: Python com PydanticAI, LiteLLM, MCP, OpenTelemetry e Claude Agent SDK. Em Node os mesmos padrões valem.
- Custo/Acesso: todo open-source. O custo cai do seu provedor de modelo + observability.
- Repositório de referência: awesome-harness-engineering com tools por categoria e exemplos.
Por que esse mapa importa agora
A OWASP segue colocando prompt injection no topo do LLM Top 10 pelo segundo ano consecutivo. Não é coincidência. A maioria dos agentes em produção foi montada sem camadas de defesa porque o tutorial original não tinha.
E o tutorial original não tinha porque o tutorial original não precisava. Numa demo, o input vem de você. Em produção, o input vem de um e-mail de alguém que leu um post sobre como sequestrar agente alheio.
A diferença entre POC e produção não é "modelo melhor". É infraestrutura ao redor do modelo.
O próprio Claude Agent SDK reconhece isso de forma explícita: por padrão, max_turns é ilimitado e max_budget_usd é opcional. A intenção é clara. O SDK te dá o loop. A contenção é sua.
Aqui está o mapa completo.
O diagrama
┌──────────────────────────────────────────────────────────────┐
│ Request do cliente │
└──────────────────────────────┬───────────────────────────────┘
│
┌───────────────▼───────────────┐
│ 1. GATE │
│ rate limit, schema input, │
│ sanitização anti-injection │
└───────────────┬───────────────┘
│
┌───────────────▼───────────────┐
│ 2. ROUTER │
│ classifica intent, │
│ escolhe sub-agente / skill │
└───────────────┬───────────────┘
│
┌───────────────▼───────────────┐
│ 3. CONTEXTO MONTADO │
│ memória + RAG + state + │
│ system prompt versionado │
└───────────────┬───────────────┘
│
┌───────────────▼───────────────┐
│ 4. LOOP │
│ timeout, retry por tipo de │
│ erro, budget hard cap │
└───────────────┬───────────────┘
│
┌───────────────▼───────────────┐
│ 5. PÓS-PROCESSAMENTO │
│ validação de schema, │
│ redaction PII, eval inline │
└───────────────┬───────────────┘
│
┌───────────────▼───────────────┐
│ 6. TELEMETRIA │
│ trace por hop, custo por │
│ request, alertas │
└───────────────┬───────────────┘
│
▼
Response ao cliente
Cada camada responde uma pergunta. Vamos uma por uma.
Camada 1: gate de entrada
Pergunta: esse request deveria sequer chegar no modelo?
Três pedaços rodam aqui, antes de qualquer token sair pra API.
Rate limit. Por usuário, por endpoint, por tier de plano. Em produção real os números são granulares: algo como 30/min em chat normal, 10/min em deep research, 5/min em geração de imagem. Sem isso, o primeiro engraçadinho derruba seu orçamento mensal numa tarde.
Validação de schema do input. Tamanho máximo, tipos esperados, allowlist de campos. Não é firewall, é higiene. O agente não devia receber um payload de 80kb pra começar.
Sanitização anti-injection. Aqui o jogo é mais sutil. A defesa real não é regex em "ignore previous instructions". Isso é teatro. A defesa real é separação estrutural: tudo que vem do usuário ou de fonte externa entra delimitado como dado, não como instrução. O system prompt sabe disso. O modelo é treinado pra respeitar a fronteira. A OWASP recomenda exatamente esse padrão.
from pydantic import BaseModel, Field
class ChatInput(BaseModel):
user_id: str
message: str = Field(max_length=8000)
thread_id: str | None = None
def gate(raw: dict, limiter: RateLimiter) -> ChatInput:
if not limiter.allow(raw.get("user_id"), endpoint="chat"):
raise TooManyRequests()
payload = ChatInput.model_validate(raw) # 422 se quebrar
payload.message = strip_html(payload.message)
return payload
Detalhe que ninguém menciona: o gate não devolve "request bloqueado" como string vazia. Ele devolve uma resposta determinística (HTTP 4xx) que o frontend sabe tratar. Resposta vazia gera retry no cliente, que gera mais carga, que gera mais bloqueio. Espiral.
Camada 2: roteador de intent
Pergunta: qual sub-agente, qual skill, qual ferramenta deveria atender isso?
Numa demo, todo request vai pro mesmo loop com o mesmo system prompt. Em produção isso é caro e ruim.
O roteador classifica o request, geralmente com um modelo pequeno e barato (Haiku, Gemini Flash, Mistral Small), e despacha pra um pipeline especializado. Pergunta sobre código vai pra um agente com tools de busca de repo. Pergunta sobre status de pedido vai pra um agente com tool de banco. Pergunta vaga vai pra um agente de clarification.
Por quê? Três razões:
- Contexto enxuto. Cada sub-agente recebe só o system prompt e as tools que importam. Menos tokens, menos confusão, melhor decisão.
- Avaliação separada. Você roda eval por intent. Quando uma classe regride, você sabe qual.
- Failure isolado. Se a tool de pagamento explode, só o agente de billing fica degradado.
LangGraph e o Agents SDK da OpenAI tratam isso como handoffs explícitos entre nós. Vale a pena olhar mesmo se você não for usar nenhum dos dois — o vocabulário ajuda a desenhar o seu.
Camada 3: contexto montado
Pergunta: o que o modelo precisa saber pra responder bem essa mensagem específica?
A parte mais subestimada do harness em produção. Janela de 200k tokens não significa que você deve usar 200k tokens. Análises de uso real do Claude Code apontam um contexto efetivo de trabalho na faixa de 60k a 80k tokens antes do desempenho começar a degradar — mesmo com janela nominal muito maior.
Então o jogo é montagem deliberada. Três fontes alimentam o contexto:
- Memória do usuário. Preferências persistidas entre sessões. Bibliotecas como mem0 e Letta implementam camadas core/archival/recall — vale entender o padrão antes de inventar o seu.
- RAG. Recuperação semântica do que é relevante agora. Aqui mora outra superfície de injection: tudo que vem do índice precisa ser tratado como dado não confiável, exatamente como o input do usuário. Trecho indexado pode ter sido envenenado.
- State da conversa. Histórico recente, mas comprimido. A própria API da Anthropic oferece compaction server-side com redução agressiva de tokens. Se você não tem isso, a conversa fica cara em poucos turnos.
A função que monta o contexto é, na prática, o coração do seu harness. Versione ela como código de produção. Teste com snapshots. Não monte prompt em string concatenada espalhada por seis arquivos.
Camada 4: loop com timeout, retry e budget
Pergunta: quando esse loop deve parar mesmo que o agente não queira parar?
O loop em si é cinco linhas. As bordas é que matam.
Três modos de falha numa chamada de tool, cada um pedindo retry diferente:
- A chamada errou (rede, 5xx, timeout). Retry com backoff exponencial faz sentido.
- A chamada respondeu mas não veio nada útil. Retry cego repete o mesmo nada. Aqui é hora de reformular ou desistir.
- A chamada respondeu e o agente interpretou errado. Retry não resolve. Você precisa de validação a jusante.
Em cima disso, três contadores duros:
async def run(ctx, tools, *, max_turns=12, timeout_s=60, budget_usd=0.50):
started = time.time()
spent = 0.0
for turn in range(max_turns):
if time.time() - started > timeout_s:
raise TimeoutBudgetExceeded()
if spent > budget_usd:
raise CostBudgetExceeded()
msg = await model.respond(ctx)
spent += msg.cost_usd
if msg.stop_reason == "end_turn":
return msg
ctx = await execute_tools(msg.tool_calls, tools, ctx)
raise TurnBudgetExceeded()
Detalhe importante: trip de qualquer um desses três limites não deve disparar retry automático. Retry automático em loop multiplica custo. Ou você pede confirmação humana, ou você manda pra fila de revisão. Nunca silencia.
Camada 5: pós-processamento
Pergunta: essa resposta do modelo é segura e válida pra mandar adiante?
A saída do modelo nunca vai direta pro cliente. Entre os dois fica uma fileira de validadores determinísticos:
- Schema. Se você prometeu JSON com um shape específico, valide com Pydantic ou Zod antes de devolver. Se quebrou, ou você re-pede ao modelo (com a mensagem de erro como dica), ou degrada com fallback. Nunca devolve JSON quebrado pro consumidor.
- Redaction. PII, segredos, tokens de API que o modelo possa ter regurgitado. Lista de regex + classificador.
- Eval inline. Aqui é onde a coisa fica interessante. Plataformas como Galileo já rodam guardrails de eval em sub-200ms, com custo da ordem de centavos por milhão de tokens — barato o suficiente pra rodar em todo request, não só em amostragem batch.
A regra mental: toda saída passa por filtro determinístico. O modelo erra. O filtro pega.
Camada 6: telemetria
Pergunta: quando isso quebrar às 3 da manhã, eu vou conseguir entender o que aconteceu?
Telemetria não é dashboard bonito. É a diferença entre debugar em 5 minutos ou em 5 horas.
O mínimo viável:
- Trace distribuído por hop. Cada camada vira um span. As GenAI semantic conventions do OpenTelemetry já existem e são o caminho. Atributos: modelo, tokens in/out, latência, custo, ferramenta chamada, resultado.
- Custo por request, agregado por usuário e por intent. Sem isso você não sabe quem está te quebrando.
- Eval em runtime amostrado. Não basta eval offline. A cada N requests, um juiz LLM ou regra determinística avalia a saída e o resultado vira métrica. Quando a taxa de "resposta ruim" sobe, alerta.
- Trace de cada gatilho de guardrail. Quantas vezes a sanitização bloqueou? Quais inputs? Esses dados alimentam a próxima geração do gate.
Ferramentas como LangSmith, Braintrust ou o stack OTel + Tempo + Grafana resolvem. Escolha um e pare de escolher.
O que cola tudo
O harness em produção não é uma biblioteca. É um conjunto coerente de seis decisões. Você vai compor com peças prontas — LiteLLM pra roteamento e cost tracking, PydanticAI pra tipagem do loop, MCP pra tools, OTel pra trace, mem0/Letta pra memória — mas a cola entre elas é seu produto.
Esse desenho é exatamente o que vamos abrir ao vivo no Harness Engineering com Claude Code: código rodando, decisões de arquitetura na mesa e o porquê de cada camada existir antes do agente entrar em produção.
FAQ
Eu preciso das seis camadas no dia 1?
Não. Você precisa do gate (camada 1) e do budget no loop (camada 4) no dia 1, sempre. Telemetria entra na semana 1. As outras você adiciona quando a dor aparece — e ela aparece. O importante é saber que existem antes de subir o agente, pra não ser pego desprevenido.
Posso usar um framework que já faz tudo isso?
Em parte. LangGraph cobre router + loop bem. PydanticAI cobre tipagem + validação. Nenhum cobre os seis. E o que você não quer é um framework que esconda a camada — quando quebrar em prod, você precisa entender o que está embaixo.
E se eu rodar tudo num modelo só, sem router?
Funciona até certa escala. A partir do ponto em que você tem mais de uma classe de request, o router começa a pagar por si só em qualidade e custo. Comece sem, mas projete pra ele caber.
Por que não confiar só no modelo pra validar a saída?
Porque o modelo é exatamente o que você está validando. Validador determinístico em cima de saída de modelo é o mesmo princípio de testes em cima de código: a peça que verifica não pode ser a peça que erra.
Conclusão
POC e sistema confiável usam o mesmo modelo. O que muda são as seis camadas que o tutorial nunca mostra. Gate, router, contexto, loop, pós-processamento, telemetria. Cada uma responde uma pergunta concreta. Juntas, elas convertem um agente que funciona na demo num agente que aguenta o que o mundo joga.
Próximo passo prático: pega seu agente atual e marca, no código, em qual arquivo cada uma das seis camadas vive. Se alguma delas não tem arquivo, você acabou de achar a primeira coisa pra construir essa semana.
{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.
Você também pode gostar
Anatomia de um Agent Harness: state, tool execution, feedback loops e guardrails
Harness é o software que envolve o LLM e separa um demo bonito de um agente que aguenta produção. Quebro a anatomia em cinco peças obrigatórias: estado persistente, roteador de ferramentas, validação de I/O, loop de raciocínio e limites de segurança. É o mapa mental que abre a série de posts sobre engenharia de agentes.
O que é Harness Engineering e por que seu Claude Code trava em tarefas longas
Quando o agente esquece o que estava fazendo, repete trabalho ou alucina arquivos, raramente é falha do modelo. É falha do harness. Definição do termo, anatomia mínima (loop, tools, contexto, memória) e o ponto onde a maioria dos devs para de evoluir o setup.
Chatbot não é agente: o teste dos 3 turnos que separa brinquedo de produto
Três perguntas simples sobre um produto real — preço hoje, reviews recentes, disponibilidade no CEP — quebram qualquer chatbot cru. O que separa brinquedo de produto não é o modelo. É o harness: a camada que transforma um LLM em agente confiável, com tool use, estado e validação contra o mundo real.
Do legado ao SDD: refatorando um módulo bagunçado a partir de uma specification reversa
SDD nasceu pensando em greenfield. A maioria dos tutoriais começa em mkdir projeto-novo e ignora quem está em projeto maduro. Reverse-spec resolve isso: o agente lê o código existente, gera a specification, humano revisa, e a partir daí o ciclo SDD clássico roda. Vou mostrar 4 passos práticos pra aplicar a técnica num módulo legado real, sem reescrever do zero e sem precisar esperar comando oficial em ferramenta nenhuma.