RLRoman Levchenko
WorkHow it's builtAI workflow
Get in touch
← All work

W01 / Case study

Fewer LLM calls, better matches

Rebuilding a candidate-to-vacancy matching engine against a measured baseline, with a human approving every match.

Client
An AI recruiting platform (EU), under NDA
Role
Lead engineer of the matching engine, from the first version
Period
2025 — now
Stack
  • Python
  • Node.js
  • Next.js
  • PostgreSQL
  • LLMs
fewer top matches later rejected by recruiters
~3×
lower LLM cost per matching run
~25%
of the LLM calls the old engine made
< ½

01 / Context

Context

When a candidate signs up, an LLM turns their application into a structured profile. The engine then ranks open vacancies for that candidate and explains each match, and recruiters approve or reject the suggestions in an internal dashboard. I built the first version of the engine and still lead it, and I am the top contributor to the core API and the approval dashboard behind it.

02 / Problem

Problem

Version one scored every candidate vacancy with its own LLM call. It worked, but recruiters rejected most of the top suggestions, and one LLM call per vacancy made every run slow and expensive.

03 / Architecture

Architecture

RetrieveGateRankDecideCandidate profileLLM, structured outputEligible vacanciesfilters + geoCheap LLM pre-filterReranker+ LLM evaluatorsBusiness rulesHuman approvaldashboard
  1. Candidate profile — LLM, structured output
  2. Eligible vacancies — filters + geo
  3. Cheap LLM pre-filter
  4. Reranker — + LLM evaluators
  5. Business rules
  6. Human approval — dashboard

Cheap steps narrow the pool before the expensive ones run, and a person approves every match.

04 / Key decisions

Key decisions

  • Eligibility before any LLM call

    Version two starts from vacancies that pass hard filters and a location check, so the LLM only sees plausible matches.

    Trade-off Vacancies outside the filters are never considered.

  • A learned ranker, not the LLM's score

    LLM scores bunched together and barely separated good matches from weak ones, so a small model trained on recruiter decisions orders the shortlist.

    Trade-off The model needs retraining as recruiter behaviour shifts.

  • Hard rules live in code

    Business rules are enforced in code after ranking. The model never gets to break them.

    Trade-off Every new rule is a code change, reviewed and tested.

  • Batched LLM calls

    One call evaluates many vacancies, which cut calls by more than half and cost by about a quarter.

    Trade-off Answers can land on the wrong vacancy (see hard problems).

  • Idempotent by design

    Jobs are keyed on the event that triggered them, and snapshot tests guard every pipeline configuration.

    Trade-off Snapshot updates are part of every pipeline change.

05 / Hard problems

Hard problems

  1. Answers landing on the wrong vacancy

    Batching made the model attribute some answers to the wrong vacancy. Making it echo what it was scoring and repairing partial batches made that rare.

  2. Misses that weren't

    Many of the matches the engine seemed to miss were recruiters deliberately matching to closed vacancies. Those vacancies are now part of the pool.

  3. Rules that fought each other

    Several ranking rules interacted badly at the cut-offs, until each became an explicit, tested rule.

06 / How it was built

How it was built

Every change started as a written spec and was measured against a fixed benchmark with a holdout set before it shipped. Role-split AI agents (architect, executor, reviewer) did much of the implementation inside those guardrails.

Next →A real-money prediction market, built end to end

© 2026 Roman Levchenko · Warsaw, EU

Built with Next.js, NestJS and Postgres on Railway. See how it's built

  • LinkedIn
  • GitHub
  • Upwork