tva
← Insights

Correspondência de Empregos com LLM: Construindo um Pipeline de Pontuação de Leads

A filtragem por palavras-chave é rápida, barata e errada exatamente nas formas que importam. Quando um recrutador define um perfil de candidato ideal — especificando expertise de domínio, profundidade tecnológica, estilo de comunicação e uma dúzia de preferências contextuais — nenhuma abordagem bag-of-words consegue avaliar se uma determinada listagem de emprego corresponde a esse perfil fielmente. A lacuna semântica entre “5+ anos Python” e “escreve Python para fluxos de trabalho de engenharia de dados de produção com uso intenso de I/O assíncrono” é invisível para um filtro de palavras-chave. É, no entanto, precisamente o que um modelo de linguagem pode avaliar com o prompt certo.

Construímos um pipeline de correspondência que pega um perfil de candidato fixo e pontua uma fila de listagens de emprego em relação a ele usando um LLM, executando as correspondências em paralelo, rastreando custos de tokens em tempo real e expondo controles de pausa/retomada de um aplicativo macOS nativo em SwiftUI. Esta publicação cobre como o sistema está estruturado e onde a correspondência por LLM genuinamente justifica seu custo em relação a abordagens mais simples.


A Arquitetura do Prompt

O núcleo do sistema é um único prompt de pontuação, chamado uma vez por listagem de emprego. Cada chamada recebe dois inputs: o perfil do candidato e o texto da listagem. O perfil é fixo para toda a sessão — inclui habilidades, nível de experiência, preferências de domínio, restrições inegociáveis e quaisquer notas contextuais que o recrutador deseja que o modelo considere. A listagem fornece o título do trabalho, empresa, descrição e requisitos como capturados.

O modelo retorna um objeto JSON estruturado com dois campos: uma pontuação numérica de 0 a 100 e uma breve justificativa explicando os principais fatores na correspondência ou não-correspondência. Aplicar saída estruturada importa aqui. Uma resposta não estruturada requer análise frágil e introduz variância em como a pontuação é apresentada. Usar uma interface de chamada de ferramenta ou chamada de função para aplicar um esquema JSON elimina completamente essa classe de problema e torna a agregação downstream trivial.

Uma escolha de design que vale a pena explicitar: o perfil deve ser detalhado. Perfis curtos produzem pontuações ruidosas porque o modelo tem contexto insuficiente para distinguir um 70 de um 85. Um perfil que articula claramente por que certas restrições importam — não apenas o que são — dá ao modelo sinal suficiente para pontuar com discriminação útil. Trate o perfil como uma rubrica detalhada em vez de uma string de filtro.

Execução Paralela com ThreadPoolExecutor

Com uma fila de 50 ou 100 listagens, chamadas de API sequenciais são lentas demais para serem úteis interativamente. O concurrent.futures.ThreadPoolExecutor do Python lida com isso de forma limpa. A contagem de workers é definida com base nos limites de taxa do provedor de API — tipicamente entre 5 e 10 requisições concorrentes atingem throughput significativo sem acionar erros de cota.

O padrão usa as_completed(), que produz futuros na ordem em que terminam em vez da ordem em que foram submetidos. Isso significa que as correspondências mais rápidas surgem primeiro e a UI pode ser atualizada progressivamente em vez de esperar por um lote inteiro:

from concurrent.futures import ThreadPoolExecutor, as_completed

with ThreadPoolExecutor(max_workers=8) as executor:
    futures = {
        executor.submit(score_listing, listing, profile): listing
        for listing in listings
    }
    for future in as_completed(futures):
        listing = futures[future]
        score, rationale = future.result()
        update_results(listing, score, rationale)

O tratamento de erros merece atenção explícita aqui. Chamadas de API individuais podem falhar — erros de limite de taxa, problemas de rede transitórios, rejeições de política de conteúdo. Cada worker captura exceções e retorna um valor sentinela em vez de deixar a exceção se propagar e cancelar todo o executor. Uma listagem que falha ao pontuar entra em fila para nova tentativa em vez de ser descartada silenciosamente. A falha transparente é muito mais fácil de raciocinar do que uma lista de pontuações com lacunas inexplicadas.

Rastreamento de Tokens ao Vivo

Os custos de tokens em escala são fáceis de subestimar. Um prompt de 400 tokens enviado para 100 listagens, com respostas de 150 tokens, acumula rapidamente dependendo do nível do modelo. O pipeline rastreia totais acumulados usando um contador thread-safe — um simples inteiro protegido por um threading.Lock, incrementado após cada resposta de API usando as contagens de tokens dos metadados da resposta.

O frontend SwiftUI faz poll desse contador em um timer, exibindo tokens de entrada cumulativos, tokens de saída e um custo estimado com base no preço por token do modelo atual. Isso dá ao recrutador uma noção ao vivo do custo da sessão e ajuda a identificar quando o perfil está produzindo justificativas incomumente longas — frequentemente um sinal de que o próprio prompt precisa de ajuste ou que o texto da listagem é mais ruidoso do que o esperado.

Um refinamento prático: estimar as contagens de tokens antes de enviar, não apenas depois. A maioria dos SDKs de LLM expõe um método de tokenização ou um endpoint de contagem separado. A pré-contagem permite que o pipeline avise quando o texto de uma listagem é incomumente longo e consumiria contexto desproporcional, ou quando o prompt combinado se aproxima do limite de contexto do modelo. Capturar isso antes da chamada de API evita gastos desperdiçados em uma requisição que será truncada.

Pausa e Retomada

Quando um recrutador está revisando resultados no meio de uma sessão, ele pode querer pausar novas requisições de pontuação sem perder o trabalho já em andamento. O mecanismo de pausa/retomada usa um threading.Event como sinal compartilhado. Antes de cada chamada de API, a thread worker verifica o estado do evento. Se pausado, bloqueia em event.wait(). Quando o evento é definido novamente, a execução continua.

Isso é mais limpo do que matar e reiniciar threads porque as chamadas de API em andamento têm permissão para ser concluídas naturalmente. Pausar significa “parar de iniciar novas chamadas” em vez de “abortar tudo agora.” A distinção importa quando você quer que o lote atual termine antes de o usuário revisar os resultados — lotes parciais são mais difíceis de raciocinar do que completos.

A persistência de estado entre pausa/retomada vale a pena ser tratada explicitamente. O pipeline mantém um arquivo de checkpoint — uma simples lista JSON de IDs de listagem já pontuados. Na retomada, o pipeline pula qualquer listagem já no checkpoint. Isso também significa que se o aplicativo travar no meio de uma sessão, nenhuma listagem já pontuada precisa ser repontuada na reinicialização. A idempotência no nível de sessão custa quase nada para implementar e economiza aborrecimentos significativos.

O Frontend macOS em SwiftUI

Escolhemos SwiftUI para o frontend porque a máquina alvo era macOS e queríamos uma experiência nativa — responsiva, de baixa sobrecarga, com gerenciamento de janelas macOS adequado. Um shell Electron teria funcionado, mas adicionado complexidade desnecessária para uma ferramenta de produtividade de usuário único. O backend Python roda como um processo local e expõe uma API HTTP mínima usando Flask; o aplicativo SwiftUI se comunica com ele via localhost.

O padrão ObservableObject e @Published do SwiftUI lida com o loop de atualização reativa de forma limpa. Um view model faz poll do backend em um timer, atualizando os arrays publicados que alimentam a lista de resultados e o display do contador de tokens. A janela principal mostra três painéis: uma lista de resultados ordenada por pontuação decrescente, um contador ao vivo de tokens e custo estimado, e uma barra de controle com botões de Pausar/Retomar e Parar.

Cada linha de resultado mostra o título do trabalho, nome da empresa, um badge de pontuação com código de cores e a justificativa de uma linha do LLM. Tocar em uma linha abre uma folha de detalhes com a justificativa completa, os principais fatores sinalizados pelo modelo e um link direto para a listagem original. A folha de detalhes é onde a maior parte do tempo de revisão do recrutador é gasta — a pontuação é um sinal para direcionar a atenção, não uma decisão final.

Quando a Correspondência por LLM Supera a Filtragem por Palavras-Chave

A filtragem por palavras-chave é a ferramenta certa quando os requisitos são binários e explícitos: deve ter uma certificação específica, deve estar localizado em uma cidade específica, deve ter um número mínimo de anos em uma função. Para esses, um filtro simples roda em milissegundos e não custa nada.

Mas na realidade, a maioria das restrições interessantes é contextual. “Procurando alguém que escreve código limpo e bem testado” é uma preferência que aparece em como uma descrição de trabalho é escrita, não como uma palavra-chave discreta. “Não interessado em funções onde a stack é apenas legada sem caminho de modernização” requer ler e interpretar a descrição. Um filtro de palavras-chave não consegue detectar esse sinal. Um LLM com um perfil explícito com essas restrições claramente delineadas pode avaliá-las diretamente.

A lacuna se amplia ainda mais em mercados com inflação significativa de palavras-chave. Quando toda listagem de emprego lista as mesmas habilidades independentemente de serem realmente centrais para a função, a correspondência por palavras-chave produz uma distribuição quase plana onde a maioria das listagens parece equivalente. A pontuação semântica rompe esse ruído porque avalia a coerência geral da listagem em relação ao perfil, não apenas a presença de termos.

Quando É Excessivo

A pontuação por LLM adiciona custo e latência reais. Para triagem de alto volume e baixa nuance — milhares de listagens com requisitos binários diretos — uma cadeia de filtros estruturados é mais rápida e mais barata por ordens de magnitude. A arquitetura correta frequentemente combina ambas as abordagens: um pré-filtro rápido remove não-correspondências óbvias, reduzindo a fila para um subconjunto gerenciável, e a pontuação por LLM é aplicada apenas ao que resta. Pré-filtrar 80% das listagens antes da pontuação por LLM reduz drasticamente o custo sem reduzir meaningfully a qualidade da correspondência.

A outra consideração é a consistência. Os LLMs introduzem não-determinismo. A mesma listagem pontuada duas vezes pode receber pontuações levemente diferentes. Para a maioria das aplicações de recrutamento, essa variância é aceitável — uma variação de alguns pontos em qualquer direção não muda quais listagens valem a pena revisar. Mas vale a pena estar ciente ao comparar pontuações entre sessões que usaram versões de modelo diferentes ou iterações de prompt significativamente revisadas.

Insights Relacionados

Artigos relacionados