← Volver al blog
steply / blog · como-um-agente-funciona-por-baixo-dos-panos-harness-loop.md
$ steply blog open como-um-agente-funciona-por-baixo-dos-panos-harness-loop
▸ loading article…
✓ ready

Como um agente funciona por baixo dos panos: harness, loop, context, sem detalhe fora

porSteply9 min de lectura

O post anterior definiu o que é agente: LLM controlando o próprio fluxo via tool calling dentro de um loop. Esse post abre a caixa preta. Mostra exatamente o que acontece entre um prompt entrar e a resposta sair, com foco no harness (o código não-LLM que faz tudo girar). Toda a complexidade real está aqui. Modelo bom com harness ruim faz agente ruim. O contrário também é verdade, e com mais frequência do que se admite.

Vamos cobrir: a estrutura do loop, como o prompt é montado a cada turn, como tool calls são executadas (serial vs paralela), como retornar erro estruturado, como gerenciar context que cresce, como usar prompt caching pra não falir, como persistir state, como decidir critério de parada, e como instrumentar tudo pra observabilidade. Sem cortar.

1. O loop em pseudocódigo, sem floreio

messages = [{role: 'user', content: user_input}]
turn = 0
while turn < MAX_TURNS:
 response = llm.create(
 model=MODEL,
 system=SYSTEM_PROMPT,
 messages=messages,
 tools=TOOL_DEFINITIONS,
 max_tokens=4096,
 )
 messages.append({role: 'assistant', content: response.content})

 if response.stop_reason == 'end_turn':
 return response.content

 if response.stop_reason == 'tool_use':
 tool_results = execute_tools(response.content.tool_uses)
 messages.append({role: 'user', content: tool_results})
 turn += 1
 continue

 if response.stop_reason == 'max_tokens':
 raise BudgetExceeded()

raise MaxTurnsExceeded()

Esse é o loop completo. Toda implementação de agente, do Claude Code ao Cursor ao seu MVP, é uma elaboração disso. A diferença entre brinquedo e produção está nos detalhes de cada linha. Vamos abrir cada uma.

2. Montagem do prompt: o que vai no system, o que vai no messages

System prompt é estático ao longo da execução. Vai nele: identidade do agente, regras invariantes, formato de output esperado, política de segurança, descrição do ambiente. Tudo o que não muda entre turns vai no system, porque é cacheado.

O array de messages cresce a cada turn. Estrutura típica em formato Anthropic:

[
 { role: 'user', content: 'pedido inicial' },
 { role: 'assistant', content: [
 { type: 'text', text: 'vou consultar X' },
 { type: 'tool_use', id: 'toolu_01', name: 'search', input: {...} }
 ]},
 { role: 'user', content: [
 { type: 'tool_result', tool_use_id: 'toolu_01', content: '...' }
 ]},
 { role: 'assistant', content: [
 { type: 'text', text: 'achei. agora vou processar' },
 { type: 'tool_use', id: 'toolu_02', name: 'process', input: {...} }
 ]},
 ...
]

Detalhe que muita implementação erra: tool_result vai com role: 'user', não 'tool'. Anthropic não tem role 'tool'. OpenAI tem (role: 'tool'). Misturar quebra. Cada provider tem seu wire format, abstrair por trás de um adaptador é higiene mínima.

3. Tool definitions: schema é contrato

Cada tool é declarada com name, description e input_schema (JSON Schema). O LLM lê isso a cada turn (faz parte do prompt enviado, custa tokens). Três regras práticas:

  • Description é a UI da tool pro LLM: escreva como se fosse docstring lida por dev júnior. Inclui quando usar, quando não usar, exemplo de input válido, formato esperado de output. Diferença entre descrição genérica e descrição rica é a diferença entre LLM acertar 60% ou 95% das vezes.
  • Schema valida na entrada, sempre: o LLM vai gerar input malformado em algum momento. Valide com Zod/Pydantic e retorne erro estruturado, não exception crua.
  • Não exponha tool que pode ser composta no código: se duas tools sempre são chamadas em sequência, faça uma só. Cada tool extra é prompt maior, latência maior, e mais oportunidade de LLM errar.

4. Execução de tool calls: paralela ou serial

LLMs modernos podem emitir múltiplas tool calls num mesmo turn (parallel tool use). Anthropic suporta desde Claude 3, OpenAI desde GPT-4o. O harness recebe um array e decide como executar.

Se as tools são independentes (consultar 3 APIs diferentes), execute em paralelo com Promise.all/asyncio.gather. Ganho de latência direto. Se tem dependência (raro no mesmo turn, porque o LLM já teria pedido em turns separados), execute serial. Em caso de dúvida, paralelo, e desenhe tools pra serem seguras em concorrência.

async def execute_tools(tool_uses):
 results = await asyncio.gather(*[
 execute_one(t) for t in tool_uses
 ], return_exceptions=True)
 return [format_result(t, r) for t, r in zip(tool_uses, results)]

return_exceptions=True é crítico. Sem isso, uma tool que falha cancela as outras e o LLM perde resultado parcial.

5. Erro de tool: nunca propaga, sempre devolve

Erro de tool não é erro de programa. É observação que vai pro LLM raciocinar. Formato Anthropic:

{
 type: 'tool_result',
 tool_use_id: 'toolu_01',
 is_error: true,
 content: 'API returned 503: service unavailable. Retry recommended.'
}

Com is_error: true, o LLM entende que a chamada falhou e decide: retentar (mesmo input, mesma tool), tentar caminho alternativo, pedir ajuda ao usuário, ou desistir. Se você levanta exception e quebra o loop, perdeu a chance do LLM se recuperar.

Erros que devem virar exception (não tool_result): tool não existe (LLM alucinou nome), schema do input é inválido após sanitização, credencial expirada (precisa intervenção externa). Tudo o que é estado transitório do mundo externo vira tool_result com erro.

6. Context management: o problema que mata agente em produção

A cada turn, o context cresce. Cada tool result acrescenta tokens. Em 10 turns, você pode ter 50k tokens. Em 30, 200k. Três coisas dão errado.

Custo: você paga input tokens a cada chamada. Loop de 20 turns com 100k tokens médios custa 20x mais que uma chamada de 100k. Sem prompt caching, é ruína.

Latência: time-to-first-token cresce com input size. Agente fica progressivamente mais lento ao longo do loop.

Context rot: modelo perde precisão à medida que context fica gigante, mesmo dentro da janela suportada. Performance degrada bem antes do limite teórico.

Quatro técnicas pra mitigar.

  • Prompt caching: provider cacheia prefixo estável. Anthropic cobra 10% do preço normal pra tokens cacheados (1h de TTL com cache extended). Coloque system prompt, tool defs e mensagens estáveis NO INÍCIO do prompt. Mensagens dinâmicas no final. Hit rate decente: 90%+. Reduz custo em 5x a 10x.
  • Summarização: a cada N turns, comprima as mensagens antigas em resumo. Perde detalhe, ganha espaço. Usado em Claude Code com compaction automática.
  • Sub-agentes: tarefas paralelizáveis vão pra sub-agente com context próprio. Resultado volta como string curta pro agente principal. Padrão usado por Claude com Task tool, deep research da OpenAI, e quase todo agent framework moderno.
  • External memory: state que não cabe no context vai pra storage (Redis, banco, filesystem). Tool de read/write expõe acesso. Padrão usado em agentes long-running.

7. Prompt caching em detalhe: a única feature que decide se você consegue pagar

Sem prompt caching, agente é inviável economicamente em volume. Como funciona em Anthropic: você marca um breakpoint na mensagem com cache_control: { type: 'ephemeral' }. Tudo antes do breakpoint é cacheado por 5 minutos (default) ou 1 hora (extended). Próxima requisição que comece com o mesmo prefixo paga 10% do preço.

{
 system: [{
 type: 'text',
 text: SYSTEM_PROMPT,
 cache_control: { type: 'ephemeral' }
 }],
 tools: TOOL_DEFINITIONS, // tools entram no cache automaticamente com system
 messages: [
 ...static_messages,
 { ..., cache_control: { type: 'ephemeral' } },
 ...dynamic_messages
 ]
}

Estratégia padrão: dois breakpoints. Um após system+tools (cacheado o loop inteiro). Outro após a última mensagem assistant estável (cacheado entre turns próximos). Hit rate sobe pra 95%+ em loops longos.

8. Stop reason: como o loop sabe que pode terminar

Anthropic retorna stop_reason com valores: end_turn (LLM terminou de falar, sem tool call), tool_use (LLM pediu tool), max_tokens (estourou budget), stop_sequence (encontrou sequence definida), refusal (recusou por política).

Lógica do harness:

  • end_turn: retorna resposta ao usuário, fecha loop.
  • tool_use: executa, devolve resultado, continua loop.
  • max_tokens: erro. Aumenta max_tokens da próxima chamada, ou desiste com mensagem ao usuário.
  • refusal: erro. Mostra ao usuário, não retenta.

Critério de parada hard do harness: MAX_TURNS (geralmente 30 a 50) e MAX_TOTAL_TOKENS (budget de execução). Sem isso, agente em loop pode custar dezenas de dólares por execução.

9. State management e durabilidade

Agente que dura segundos é stateless: state vive em memória, morre com o processo. Agente que dura minutos ou horas (deep research, refactoring de codebase) precisa ser durável: pode crashear no meio e retomar.

Estratégias:

  • Event log: cada turn (user message, assistant response, tool result) é appendado num log persistente (banco, Kafka, arquivo). Restart reconstrói messages do log.
  • Checkpoint: a cada N turns, serializa o array messages inteiro. Restart carrega último checkpoint. Mais simples, menos granular.
  • Workflow engine: Temporal, Inngest, Restate. O agente vira workflow durável: cada tool call é uma atividade, retry e resume são primitivas do framework. Padrão usado em agentes de produção sérios.

10. Streaming: por que sempre vale a pena

LLM gera token a token. Sem streaming, você espera a resposta completa antes de ver qualquer coisa. Latência percebida explode. Com streaming, você mostra texto à medida que sai, e detecta tool_use antes do final pra começar a preparar execução.

Anthropic streaming usa SSE com eventos tipados: message_start, content_block_start, content_block_delta (deltas de texto), content_block_stop, message_delta, message_stop. Tool use chega com input_json_delta parcial, agregado em string que vira JSON ao final.

Harness sério streama por default. Latência de 4 segundos vira latência percebida de 800ms.

11. Observabilidade: trace, span, métricas

Cada execução de agente é uma árvore. Root: a request inicial. Filhos: cada turn. Netos: cada tool call dentro do turn. Instrumentação útil:

  • Por turn: tokens input/output, latência, modelo usado, cache hit ratio, stop_reason.
  • Por tool call: nome, input, output truncado, latência, sucesso/falha, retry count.
  • Por execução: total turns, total tokens, custo USD, tempo total, desfecho (sucesso/budget/erro).

Ferramentas: LangSmith, Langfuse, Helicone, Phoenix Arize, ou OTEL direto pra Datadog/Honeycomb. Build vs buy depende de volume. Sem trace, debug em produção é arqueologia em log.

12. Sub-agentes: quando e como

Sub-agente é um agente invocado como tool pelo agente pai. Padrão: pai decide subdividir, chama spawn_agent(prompt, tools), sub-agente roda loop próprio com context isolado, retorna string. Pai consome string como tool_result.

Vantagens: paralelismo (vários sub-agentes ao mesmo tempo), context isolation (sub não polui o pai), especialização (sub com tools restritas, system prompt focado). Custo: cada sub-agente é um loop completo com seu próprio overhead.

Anti-pattern: cadeia profunda de sub-agentes (pai chama filho chama neto chama bisneto). Latência empilha, debug fica impossível. Mantenha 2 níveis no máximo, exceto em caso muito justificado.

13. Estruturas que importam: ReAct, Reflexion, Plan-and-Execute

Três variações do loop básico que aparecem em produção.

ReAct (Reasoning + Acting): o loop padrão descrito acima. LLM alterna entre raciocinar (texto) e agir (tool). É o default.

Reflexion: após cada ação, LLM avalia o resultado e gera 'reflexão' que vai pra memória. Próximas iterações consultam reflexões pra evitar erro repetido. Útil em tarefas com tentativa-e-erro (debug, exploit, otimização).

Plan-and-Execute: separa em duas fases. Fase 1 (uma chamada): LLM gera plano em formato estruturado. Fase 2 (loop): executor segue plano passo a passo, com LLM revisando se o passo deu certo. Replaneja se necessário. Menos chamadas que ReAct puro pra tarefas com plano claro.

14. O que vai dar errado em produção

Lista honesta dos bugs que aparecem cedo:

  • LLM aluciná tool name que não existe. Harness deve retornar erro estruturado e logar.
  • LLM passa input que falha schema. Validar, devolver com mensagem clara do que está errado.
  • Tool tem side effect e LLM retenta após timeout, dobrando ação. Idempotência key obrigatória.
  • Loop infinito quando LLM insiste em retentar tool que falha de jeito determinístico. Detectar repetição e quebrar.
  • Context overflow em meio de execução. Compactar proativamente baseado em token count, não só quando explode.
  • Cache miss porque alguma mensagem mudou ordem ou conteúdo entre turns. Auditar com métrica de hit rate.
  • Tool com timeout default infinito trava o agente todo. Sempre setar timeout explícito.
  • Erro de rede no streaming abandona resposta parcial. Implementar resume ou retentar inteiro.

15. O que o harness pequeno NÃO precisa fazer

Pra fechar com calibragem: pra começar, você não precisa de framework. anthropic-sdk + 200 linhas de código resolvem 80% dos casos. Frameworks como LangGraph, LlamaIndex, CrewAI agregam quando você tem escala (>100 agentes, multi-tenant) ou quando precisa de durabilidade séria.

Esse post inteiro descreve o que você implementa do zero. Entender essa camada é pré-requisito pra escolher framework com critério, em vez de adotar e descobrir 6 meses depois que está preso a uma abstração que não cabe no seu caso. O harness é seu ponto de leverage. Trata como infra crítica.