CONTEXT
When the interviewer was a bot
I built this after a job interview where I never spoke to a person. Being interviewed by a bot was deeply frustrating — it was slow to answer, quick to talk over me, and cut my answers short before I could finish. It is the kind of AI-led first interview people joke about online, until it happens to you.
I had just been interviewed by an AI. So I built a better one to practice with.
Every frustration from that call became a design choice — I wanted mine to be everything that bot was not. It waits until you have truly finished before it replies, and it starts speaking the moment it has something to say, so a turn feels like a real conversation instead of a fight to be heard.
WHAT I BUILT
Three agents, one interview
You upload your CV as a PDF and paste the job offer. From there, three agents hand the work to each other:
- Plan — the planner reads the CV and job description, then prepares an interviewer persona and milestones in the selected language.
- Interview — a voice agent uses LiveKit to conduct the session in the browser, with the complete CV, job description and plan available as context.
- Evaluate — after the session closes and its transcript is sealed, the evaluator reviews criterion-level evidence. Partial or insufficient interviews receive feedback without a global score or hiring verdict.
ARCHITECTURE
A voice worker and an API around one database
The application combines a React/TanStack Start frontend, a FastAPI API and a LiveKit Agents voice worker. PostgreSQL holds the extracted CV text, interview plans, milestones, transcripts and assessments. The planner, interviewer and evaluator receive the CV and job description directly as text context. The browser joins the interview room over WebRTC.
LiveKit Agents handles speech recognition, synthesis and turn detection. LangGraph coordinates interview decisions using OpenAI models, and validated decisions are persisted before a question is delivered. Confirmed answers retain their text versions and provenance, so corrections and uncertain recordings remain distinguishable. Evaluation uses a sealed transcript rather than an unfinished stream of live captions.
How a single turn works
TRADE-OFFS
The hard choices, and what they cost
I stopped hand-rolling the voice plumbing. An early version tried to own the whole audio path — capture, silence rules, my own turn heuristics. It taught me a lot, and it was never going to feel human. Now WebRTC and LiveKit Agents handle echo, interruptions, and reconnection, and end-of-turn is decided by a purpose-built turn-detection model on top of Silero VAD — the exact part the bot that interviewed me got wrong. Cost: a hosted dependency I do not control. Gain: every hour not spent on audio plumbing went into the interview itself.
Keep a session’s configuration stable. Each interview stores its language, voice, models, level and limits. Reconnecting preserves the original start time and consumed budget. A repeat interview gets a fresh plan while retaining the source CV and job description, and remains linked to the original run. That makes the session history useful for comparing practice attempts.
Treat interview decisions as persisted state. LangGraph decisions are validated and committed before question delivery. An uncertain or interrupted question is not automatically repeated; an explicit replay can use the saved question without creating a new model decision. Transcript corrections and assessment attempts retain their earlier versions, so recovery does not silently overwrite the session’s history.
OUTCOME
Feedback you can trace to your answers
The report distinguishes topic coverage from demonstrated ability. It links each assessed criterion to transcript evidence and suggests what to practice. An incomplete recording or an abandoned session remains visibly incomplete: missing audio is not treated as a wrong answer, and partial or insufficient interviews do not receive a global score or verdict.
Plans, transcripts and evaluation attempts are persisted in PostgreSQL. The History view groups repeat interviews and retains earlier feedback when a new assessment is requested. Recoverable requests preserve their identities and previous results, rather than replacing an earlier assessment after an uncertain response.
This is the part I cared about most. The report gives me specific answers to review and topics to practice again. I can pause, revisit feedback and repeat the same role with a fresh interview plan. The goal is to practice the difficult conversations before facing them in a real hiring process.
I built it to make practice useful before a real interview: a session I can review, repeat and learn from.
WHAT I LEARNED
Three things I learned
- Don't rebuild solved problems. My hand-rolled audio pipeline taught me why WebRTC, VAD, and turn detection are their own discipline — and that my time was better spent on the agents than on the plumbing.
- How fast it feels beats how smart it is. Turning reasoning off on the interviewer and streaming its reply into speech did more for the conversation than any smarter model would have.
- Structured outputs turn LLM calls into functions. The planner and evaluator return validated schemas with retries, which is what lets both run unattended — no human checks the output before it ships to the screen.