01 / AI Agents
RagResume
A nine-step pipeline that takes an activity bank of career bullets, a resume template and a job description, and produces a resume tuned for both the ATS screen and the hiring manager reading after it. I built it alone, used it on my own applications, and archived it in September 2026 in favour of a rebuild.
Company
Personal Project
Role
Sole PM, designer and builder
Year
2026 · archived Sept 2026
Tags
01 / Role & Scope
Scope
Sole product manager, designer and builder. It started as a tool for my own job search, but I scoped it as a product: architectural decisions written down, pipeline designed before it was built, and a postmortem at the end.
02 / Problem
Two Gates, Not One
A resume has to clear an automated screen and then a person, and those two readers want different things. The ATS wants keywords, parseable structure and the absence of knockout criteria. The hiring manager wants a career that reads like it leads to this role.
Optimising for one gate at the expense of the other is the common failure. The other common failure is worse: asked to align a resume to a posting, a model will fabricate the experience that closes the gap.
03 / Solution
The Pipeline
The core decomposition was to separate what the job description asks for from what the candidate has actually done, and then match between them. The job description gets analysed into two rubrics. The ATS rubric covers keywords, required skills and knockout criteria. The intent rubric covers what the hiring manager is really reading for: domain fluency, motivation, growth. They are genuinely different objectives, and modelling them separately produced better resumes than one blended pass.
Retrieval is hybrid rather than pure vector search: 55% semantic similarity on Gemini embeddings, 30% exact keyword and phrase overlap, 15% a job-title and domain context bonus. That mix beat cosine similarity alone by a wide margin. Pure embedding similarity kept surfacing activities that were thematically adjacent but wrong for the requirement.
Two decisions did more for match quality than anything on the query side. Enriching at index time, so AI skill extraction with synonym expansion runs once at ingest and "container orchestration" is indexed alongside Kubernetes and Docker. And deduplicating rubric items by vector similarity: the analyser naturally emits 20 to 30 overlapping requirements, and merging above 0.88 cosine cut that to a usable 8 to 15 without losing coverage.
The anti-hallucination work was structural, not a prompt instruction. Activities are stored as atomic Situation-Action-Impact entries, which gives retrieval clean units to match against instead of whole resume lines it might blend. A hard requirement and a transferable skill are handled differently, so HubSpot experience against a job asking for Salesforce is surfaced as transferable rather than claimed as Salesforce. And the human picks which activity answers each rubric item, which is the one judgement a model cannot make: it does not know which of your experiences you want to lead with.
Read the diagram as text
Steps 1 to 3 are the inputs, which run in parallel: ingest an activity bank from CSV, JSON, PDF or DOCX; upload a resume template carrying contact info and section structure; upload the target job description.
Step 4, AI analysis, runs on Gemini 2.5 Flash and generates two rubrics: an ATS rubric of 8 to 15 keyword groups by skill category, and an intent rubric of 10 to 20 items based on hiring psychology.
Step 5 is hybrid similarity search, weighted 55 percent semantic using Gemini embeddings, 30 percent exact keyword matching, and 15 percent a domain-relevance context bonus.
Step 6 is the first human-in-the-loop stage: review the top matches for each rubric item, pick the activities that fit, and generate situation-task-impact bullets across eight parallel threads.
Step 7 assembles the ATS resume from the template and the generated bullets using Gemini 2.5 Pro. Step 8, the second human stage, rewrites the narrative to match hiring manager psychology.
Step 9 exports the finished resume as LaTeX or PDF.
04 / Postmortem
Why I Killed It
I archived it in September 2026 rather than building the next feature on it, and wrote a postmortem instead. The honest scorecard: roughly 15,000 lines across two parallel implementations of the same pipeline, zero tests, three architectures built and none retired.
The root cause was an ordering mistake. The project grew a FastAPI backend and a Next.js frontend without deleting the Streamlit app underneath, leaving about 3,400 lines of dead code that still imported cleanly and looked live. Every fix after the fork had to be applied twice and often wasn't. Opening a file, you could not tell whether you were editing the code that runs.
Under that, there was no eval harness. With no golden set of inputs to expected matches, there was no way to tell whether a prompt change made output better or worse, so 20 prompt templates and 24 LLM call sites accumulated with no instrument panel. The same truncation bug was "fixed" five times. And 24 of 46 commits touched UI, design or deploy, on an engine that had neither tests nor persistence.
The one-sentence version: the product thinking was sound and parts of the retrieval design were genuinely good, but half the effort went into an interface built on top of an engine with no tests, no persistence and two diverged implementations, so the presentation layer ended up hardening the very mistakes underneath it.
05 / What It Unlocked
What Survives
Four things carry into the rebuild. The core decomposition of rubric against activity bank. The dual rubric, which is a product insight rather than an implementation detail. Hybrid retrieval over pure vector search. And human-in-the-loop at match selection specifically, which of the five review surfaces was the only one that earned its place.
The governing lesson is about order, not architecture: no presentation layer until the engine is testable and stateful. Build the eval harness before the second prompt. Persist the activity bank and its embeddings server-side, because that is the durable asset and reusing one bank across many applications is the actual product. And migration means deletion, in the same commit that promotes the replacement.
Next Project
02 / PMM Intelligence