reflekt.me

Engineering··6 min read

The honest keyword list: why we don't let the model invent your résumé

Give a language model a job posting and ask it to write you a resume for it, and it will do exactly what you asked: it will make you look like a match. That's the failure mode, not the feature. The posting says "Kubernetes," so the resume says "Kubernetes." Nobody checked whether you've actually touched it. And now, despite otherwise being the most qualified candidate for the position, a passing question about Kubernetes suddenly outs your resume as a fabrication.

This is the default behavior of "AI resume builder," and it's the reason the category has a credibility problem. We built reflekt.me's grounding pass specifically to make that failure mode structurally impossible rather than something we ask the model nicely to avoid.

Keyword matching without the model writing the resume

Most tools that claim ATS optimization mean "we scanned the posting for keywords and stuffed them in." We do the keyword scanning — that part's table stakes — but we split it into its own step, run by a separate model call, with a single narrow job: for each keyword an ATS screener would plausibly search for, decide honestly whether the candidate's actual record already supports it.

Concretely, before the drafting model ever starts writing, a classification pass reads the job posting and the candidate's full record and reports 8 to 20 ATS-relevant terms — specific skills, tools, technologies, certifications, and named methodologies, the kind of exact phrase a screening system would search for, not vague filler like "team player." For each one, it answers a single boolean: does the record already provide direct evidence of this?

The instruction that boolean is held to is deliberately strict, and it's the same standard we hold our fact-checking pass to elsewhere in the pipeline:

Mark something present only when the record genuinely supports it — never because a resume could plausibly claim it, and never because it would be a good idea to add. Absence from the record means present: false, full stop.

That last sentence is the whole point. It would be trivial to build a version of this that's generous — that lets "5 years of experience with distributed systems" count as support for "Kubernetes" because it's adjacent enough. We don't want adjacent. We want the model to say no.

What the drafting model gets instead of the job posting

This is the structural part, not just a prompting trick: the drafting model doesn't see "here's the job posting, make yourself sound like a match." It sees the result of the grounding pass — the list of keywords already marked true or false against your actual record — as part of what it's given to work with. It's handed permission to lean on what's already substantiated, not an invitation to invent what would look good. A keyword marked present: false isn't a gap to paper over; it's information the writer is explicitly not allowed to pretend away.

That ordering matters more than it might look like on paper. Run the grounding pass after drafting, as a validation step, and the best case is catching a fabrication after it's already been written — a QA pass. Run it before drafting, as an input, and there's no fabrication to catch, because the model was never given the option to write something ungrounded in the first place. Prevention beats detection, and it only works if the honest classification happens strictly before the persuasive writing does.

Diagram: fact-checking after drafting catches fabrications late; grounding before drafting leaves nothing to fabricate.MOST TOOLSDraft resumeFact-check(after the fact)2 claimsflagged too lateREFLEKT.MEGroundingtrue / falseDraft resumenothing leftto fabricate
Run the grounding pass after drafting and the best case is catching a fabrication that already got written. Run it before, and the drafting model is never given the option.

What this looks like on a real run

On a real generation we audited, the grounding pass identified 15 keywords from a live job posting and marked 13 of them present against the candidate's actual record — and it marked the other 2 honestly missing, rather than finding a way to make them fit. The resume that came out the other end didn't claim those two things. That's not a bug we're describing around — it's what the system is supposed to do. A resume that quietly claimed all 15 would score higher on a naive "keyword coverage" metric and be a worse resume, because two of those claims would be fabricated.

We think that's the right tradeoff, and we think it's the one the category mostly gets wrong. "ATS-optimized" should describe how a resume is built — real text, not scanned images, keywords that are actually there to be found — not a promise about what happens after it's submitted, which no resume tool can actually see or control. And "tailored to the job" should mean the truth about you gets organized around what the role needs, not that the truth gets stretched until it fits.

The uncomfortable part, on purpose

Sometimes this pass returns bad news: fewer keywords supported than you'd like, for a role that's a stretch. We think that's more useful than the alternative. A resume that honestly can't check every box is one you can stand behind in an interview. One that silently claims everything is a resume that's setting you up to explain a lie you didn't even write.

← All posts