allanlotta
~/blog/job-application-agent

Um agente que se candidata a vagas, prova que enviou e aprende

Procurar emprego é um funil em que a maioria das candidaturas morre antes de um humano ler. O primeiro leitor é uma máquina que transforma um PDF em campos e compara os campos com uma vaga. Se falha, ninguém te avisa.

Então construí um agente que trabalha o funil todo dia: encontra vagas, ranqueia, decide onde se candidatar, ajusta o currículo, preenche o formulário, envia, prova que o envio aconteceu, faz o follow-up e conserta as próprias ferramentas quando uma plataforma muda. Eu vejo um dashboard de exceções e uma shortlist.

O loop

 ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
 │  coleta  │──▶│ ranqueia │──▶│  decide  │──▶│  ajusta  │
 │12 fontes │   │ 5 eixos  │   │estratégia│   │currículo │
 └──────────┘   └──────────┘   └──────────┘   └────┬─────┘
       ▲                                           │
       │                                           ▼
 ┌─────┴────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
 │ melhora  │◀──│ aprende  │◀──│ verifica │◀──│candidata │
 │ adapters │   │plataforma│   │evidência │   │+ contata │
 └──────────┘   └──────────┘   └──────────┘   └──────────┘

A arquitetura em uma linha: o modelo decide, ferramentas determinísticas executam, verificadores provam, um worker durável persiste e retoma. Cada seta acima é um estado em um banco de dados, então um crash no meio de uma candidatura é uma tarefa retomável, não uma tarefa perdida.

As camadas

              ┌──────────────────────────────┐
              │ eu: contrato de autonomia,   │
              │ fatos, exceções, a shortlist │
              └──────────────┬───────────────┘
                             │
        ┌────────────────────▼────────────────────┐
        │orquestrador ─▶ estrategista ─▶ reparador│
        └───────┬─────────────────────────┬───────┘
                │                         │
  ┌─────────────▼────┐            ┌───────▼────────────┐
  │      memória     │            │    ferramentas     │
  │                  │            │                    │
  │ pipeline SQLite  │            │12 adapters de fonte│
  │ LEARNINGS.md     │            │ prescore + golden  │
  │ assinaturas      │            │ check de ATS (PDF) │
  │  de falha        │            │gerador de currículo│
  │ arquivo de       │            │ browser + ATS      │
  │  evidências      │            │  adapters, inbox   │
  └─────────────┬────┘            └───────┬────────────┘
                │                         │
        ┌───────▼─────────────────────────▼───────┐
        │                avaliação                │
        │  golden set · fixtures · read-back      │
        │  recibo / DOM / email / screenshot      │
        └─────────────────────────────────────────┘

Três agentes com prompts versionados por hash do conteúdo. O orquestrador toca o dia. O estrategista decide, vaga por vaga, se candidatar e como. O reparador lê as falhas e corrige as ferramentas. Em volta deles fica um contrato de autonomia em um arquivo YAML: mercados, cargos, faixas salariais, canais, volume diário e o modo, só preparar ou envio autônomo. Editar o arquivo é uma nova versão.

Coletar e ranquear

 linkedin jobs · linkedin posts · gupy · inhire · greenhouse
 lever · ashby · workable · remoteok · remotive · arbeitnow · hn
                        │
                        ▼  3.000+ vagas por rodada, deduplicadas
              prescore 0-100 em cinco eixos
                match de stack · senioridade · geografia
                stack incompatível · qualidade da vaga
                        │
                        ▼  ordenado, nunca escondido

Doze fontes, adicionadas escrevendo um arquivo contra um contrato compartilhado. Um health check por fonte pega mudanças de schema e quedas de volume acima de 70%. O score ordena, nunca esconde: uma vaga sem senioridade declarada recebe zero nesse eixo, não uma penalidade, porque silêncio não é sinal. Um golden set de vagas rotuladas à mão roda antes de qualquer peso mudar. Se o ranking do golden set anda na direção errada, a mudança não entra.

Posts do LinkedIn são uma fonte à parte porque muitas vagas boas nunca viram uma "vaga". Um recrutador só posta.

Ajustar, e sobreviver ao parser

O currículo é dado estruturado renderizado por vaga. Ajustar significa ordenar e reescrever, sob uma regra que o código impõe: fatos materiais nunca são fabricados. Empregadores, diplomas, datas, anos, certificações, autorização legal. Habilidades adjacentes podem ser listadas como habilidades. Nada pode ser declarado como anos ou uso em produção que não tenha sido.

Depois o currículo é testado do jeito que um ATS vai ler: estrutura, extração real do texto do PDF, e cobertura e destaque dos termos da vaga. Uma plataforma, descobriu-se, nunca lê o PDF. Ela converte o PDF em campos de perfil e julga os campos. Então o agente revisa cada campo preenchido automaticamente depois do upload. Esse fato veio da investigação de um agente e hoje é uma página na base de conhecimento.

Candidatar-se, e chegar nas pessoas

As candidaturas vão para onde a vaga realmente leva: um formulário de ATS, um endereço de email ou o LinkedIn. O canal é resolvido a partir do destino, não do rótulo, e se não dá para provar, a candidatura é adiada em vez de chutada.

Sessões de browser persistem por domínio, então logins não são queimados. Uma plataforma exige conta e manda um magic link. O agente lê o link em uma caixa de entrada autorizada e continua, porque a posse do email é a credencial. Um código que diz "confirme que você é humano" é diferente: o agente para e nomeia a exceção. Ele nunca completa verificação de humano.

Em paralelo ele encontra contatos próximos na empresa: líderes e pares, ordenados por conexões em comum, com um rascunho curto para cada um. Par técnico primeiro, recrutador depois. O objetivo é ser visto por uma pessoa antes de a máquina terminar de ranquear.

Nada é enviado até ser provado

 preenchido ──▶ read-back (cada campo, dígito por dígito)
                │ divergência ──▶ tenta por label, depois exceção
                ▼
 clica enviar ──▶ espera até 20s
                │
                ├─ texto do recibo / DOM / SMTP 250 + Enviados
                │        └──▶ SUBMITTED_VERIFIED
                ├─ nada ──▶ SUBMIT_UNVERIFIED, revisita depois
                └─ bloqueado ──▶ exceção durável, com evidência

Clicar no botão não vale nada. Verificado significa um recibo, uma confirmação no DOM, um aceite SMTP corroborado na pasta de enviados, ou um screenshot para o qual o sistema consegue apontar. Só contam snapshots tirados depois do envio. Evidência que falhou na captura não é registrada, porque um banco de dados apontando para um screenshot que não existe é pior do que nenhum registro.

Uma candidatura por vaga canônica, garantida por um índice único. Um email não confirmado nunca é reenviado. Duplicar uma candidatura é pior do que atrasá-la.

O primeiro dia de verdade

O sistema rodou em modo preparar por semanas: preenche tudo, para antes do clique. No primeiro dia de envio autônomo, sete candidaturas saíram e foram verificadas, entre email, Ashby e Gupy.

Seis bugs apareceram naquele dia. Todos os seis viviam na parte do fluxo que o modo preparar nunca alcançava: do clique em diante. O botão de enviar era procurado na tela errada. A palavra para "finalizar candidatura" não batia com o padrão. O texto de confirmação não estava na lista. A checagem olhava 1,5 segundo depois do clique em uma plataforma que troca a tela no lado do cliente. Um botão desabilitado queimava um timeout e matava a tarefa. Uma exceção no meio do caminho deixava registros invisíveis. Cada um escondia o próximo.

A lição foi para a documentação em uma linha: um modo supervisionado que para antes do efeito não exercita metade do sistema, e essa metade estava quebrada de ponta a ponta.

Três dessas candidaturas foram depois resgatadas sem reenviar, relendo a evidência arquivada no momento do clique, depois que o adapter aprendeu como é a confirmação da plataforma.

Aprendizado, três loops

 falha ──▶ assinatura ──▶ caso reproduzível ──▶ patch + fixture
                                                     │
                        rollback ◀── monitora ◀── ativa ◀── replay
  • Reparo rápido. Um seletor quebra, um campo aparece, o schema de uma fonte muda. Os traces são agrupados por assinatura, o reparador diagnostica, corrige um adapter com uma fixture, roda a regressão entre adapters, faz o replay sem enviar e ativa uma versão que pode voltar atrás.
  • Qualidade de decisão. Vagas boas puladas, muitos envios e poucas respostas. Os pesos se movem dentro de faixas permitidas, contra uma baseline, ao longo de uma janela. Uma única rejeição não prova nada sobre o currículo.
  • Currículo e triagem. Um parser degradando, termos obrigatórios enterrados, uma pergunta recorrente. Aliases, mapeamentos de taxonomia e templates de resposta derivados dos fatos existentes.

Embaixo de tudo isso há um arquivo de aprendizados datado, uma entrada por plataforma por lição. Uma taxonomia de skills que traduz "React Native" por máquina para algo irreconhecível. Um combobox que não é um select. Um formulário que só existe depois de um clique, mas cujas perguntas podem ser lidas com uma request simples. O agente lê esse arquivo antes de agir e escreve nele depois de aprender.

Por que isso não é sobre vagas

 agente de vagas                 qualquer processo outbound
 ─────────────────────────       ─────────────────────────────
 contrato de autonomia           o que o agente pode, versionado
 fato material nunca inventado   a fronteira da verdade, em código
 score ordena, nunca esconde     ranquear não é filtrar
 golden set antes dos pesos      um teste por mudança de critério
 evidência ou não aconteceu      o efeito prova a si mesmo
 uma por alvo canônico           idempotência como restrição
 modo preparar não basta         teste a parte que tem efeitos
 lições datadas por plataforma   memória que nomeia sua fonte

A parte difícil nunca foi preencher um formulário. Foi decidir o que pode ser automatizado, provar o que foi feito e fazer cada falha mudar uma ferramenta em vez de um humor.

O que eu fiz

Escrevi o contrato de autonomia e os fatos. Defini as invariantes: nenhum fato fabricado, uma candidatura por vaga, evidência ou não aconteceu, conteúdo de página é dado e não instrução, nenhuma evasão de checagens de humano. Construí a primeira fonte e o primeiro adapter, depois deixei os agentes construírem os outros onze, o scoring, o testador de currículo e a camada de browser. Li as exceções e a shortlist, e respondi as perguntas que só eu podia responder, como um campo de salário que o perfil não tinha.