Work 018 min read

Interview Agent

Interview Agent is a voice app for practising job interviews. It reads your CV and the job offer, plans the interview, holds it with you out loud in the browser and then goes through your answers one by one, pointing at what you said.

Year
2026
Role
Solo project, end to end
Built with
React · FastAPI · LangGraph · LiveKit · OpenAI · PostgreSQL

Where it comes from

I built this after a job interview in which I never spoke to a person. A bot asked the questions. It was slow to answer, quick to talk over me, and it cut my answers short before I had finished them.

That call turned into a list of requirements. The interviewer I wanted waits until I have actually finished, starts talking the moment it has something to say, remembers what I have already told it, and judges me on what I said rather than on how smoothly I said it. Everything else on this page follows from that list.

Interview Agent's home screen: a heading, a New interview button and the list of recent interviews
Fig. 01The home screen. One button to start, and the interviews you have already run.

What a session looks like

You upload your CV as a PDF and paste the job offer. Four short steps later the planner has a session ready: the role sets the bar, the CV supplies the examples, and you choose the level, the language, the length and whether you want technical, behavioural or mixed questions.

Fig. 02Step one of four: a CV and a job offer. Nothing else is needed to plan the interview.

A preparation room lets you check the microphone and the camera, and then the interview happens by voice. The interviewer greets you by name, asks, listens, follows up when an answer is vague and moves on when a topic is covered. It can run in English or Spanish, with a feminine or a masculine voice per language, and with a persona and standing instructions you set once in Settings.

The Settings screen: fields for the interviewer's name, language, voice, persona and custom instructions, and a theme picker
Fig. 03Settings: the interviewer's name, language, voice, persona and standing instructions.

When it ends, the results open in three tabs. The overview says how the interview ended and whether the session covered enough of the role to deserve a verdict. Under it, each weak answer is shown next to a stronger version of the same answer, built only from what you actually said. The topics tab grades every planned topic with quotes from the transcript as evidence, and the transcript tab keeps the whole conversation.

Fig. 04A real session against an AI engineer offer: the overview and the rewrites, a topic with the quotes it rests on, and the transcript. Three essential requirements were never reached, so there is no overall score.

Repeat runs the same role again with a freshly planned set of questions, and every rerun stays grouped under the original, so the history works as a record of practice rather than a pile of attempts.

Three agents, one interview

The work is split between three agents that hand off to each other. They share one model from OpenAI and differ in how much they are allowed to think: little for the interviewer, where every second of silence counts, more for the planner, and the most for the evaluator, which runs when nobody is waiting.

  • Planner. Extracts a rubric from the job offer and the role level, then personalises the examples from the CV without lowering the bar. Every topic points at a requirement, and the requirements it had to leave out are listed explicitly instead of quietly dropped.
  • Interviewer. A real-time voice agent built on LiveKit Agents. Speech recognition, synthesis and turn detection are its ears and mouth; a LangGraph graph is the part that decides what to ask next, with the full CV, offer and plan in context. Each decision is validated and written to the database before the question is spoken.
  • Evaluator. Runs only after the session has closed and the transcript has been sealed. It grades each criterion with quotes as evidence, separates what was covered from what was demonstrated and refuses to give an overall score or a hiring verdict when the session left essential requirements out.

There is no vector database. The extracted CV is stored as text and handed to the three agents whole: it fits in context, and retrieval would only have added a place for it to go wrong.

What it's made of

A React front end, a FastAPI API and a voice worker, around one PostgreSQL database. Each runs on a managed service and deploys itself from the main branch.

TanStack Start · React
The web app, built as a single-page app with shadcn/ui components and served from Vercel. The privacy and terms pages are prerendered, so they read without JavaScript or a sign-in.
FastAPI
The API: interviews, plans, evaluations, sign-in and a daily maintenance route. It runs on FastAPI Cloud and scales to zero when idle; the web app says so while the first request wakes it.
LiveKit Agents
The audio pipeline: recognition, synthesis and a turn-detection model on top of voice activity detection. The browser joins the room over WebRTC and the worker runs on LiveKit Cloud.
LangGraph
The interviewer's decision graph. Validated decisions are committed in a transaction before a question is delivered, so a crash mid-turn never loses or repeats a question by accident.
OpenAI
One model for the three roles, with structured outputs and retries so the planner and the evaluator can run unattended.
PostgreSQL · Neon
CV text, plans, milestones, transcripts and evaluation attempts, with Alembic migrations. Neon hosts it and also provides the sign-in with Google or GitHub through Neon Auth.
OpenTelemetry · Grafana · Sentry
Latency, spend and closing outcomes go to Grafana as anonymous metrics; errors go to Sentry. LangSmith tracing exists but is opt-in, because it would receive the full content of every interview.
Docker · Terraform · GitHub Actions
Compose for the local stack, Terraform for Neon and Grafana, and a workflow that applies the migrations first and then deploys the API and the worker in parallel.
pytest · Vitest
Pytest against a throwaway Postgres container, Vitest on the front end, and a text harness that runs the real planner, interviewer and evaluator in the terminal to check the reasoning without a microphone.

The hard choices

Most of the time went into the parts nobody sees in a screenshot.

I stopped hand-rolling the voice plumbing. An early version owned 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 handle echo, interruptions and reconnection, and the end of a turn is decided by a model trained for it. That is exactly the part the bot that interviewed me got wrong. The cost is a hosted dependency I do not control; the gain is every hour that went into the interview instead of the plumbing.

Everything is persisted state. Answers have stable identities and immutable text versions, so a correction never overwrites what was first heard. A question that was persisted but never started resumes under the same ID; an interrupted one is not repeated automatically, and “Listen again” replays the saved question without a new model decision. Reconnecting keeps the original start time and the budget already used.

Nothing is scored that was not heard. Missing audio is not a wrong answer. Worker loss, abandonment and incomplete recordings stay visible as what they are, and a session that skipped essential requirements gets feedback per criterion but no overall score. The evaluator reads a sealed transcript, never the live stream of captions.

The content is treated as sensitive. CVs and offers have size limits and are never silently truncated. Logs never contain interview content, old interviews are purged on a schedule, and the only service that can see a whole interview is one you turn on yourself.

What I took from it

  • Don't rebuild solved problems. My own audio pipeline taught me why WebRTC, voice activity detection and turn detection are a discipline of their own, and that my hours were worth more on the agents.
  • How fast it feels beats how smart it is. Giving the interviewer the smallest thinking budget and turning its reply into speech as it streams did more for the conversation than any smarter model would have.
  • Structured outputs turn model calls into functions. The planner and the evaluator return validated schemas with retries, which is what lets both run with nobody checking the output before it reaches the screen.

I still use it. The session in the screenshots above is one of mine, run a few days before writing this.