← Back to list

Echofication: Conversational AI That Catches Lies, Probes Depth, and Never Goes Off-Script

The Interviewer’s Dilemma

Aditya Pandey in Towards Explainable AI · 2026-05-23 00:19 · 57 claps · 10.2 min read
#artificial-intelligence #towards-data-science #llm #system-design-project #conversational-ai
Open on Medium ↗
Wiki topics: LLM · Large Language Models ML · Machine Learning AI · AI · General 🔬 · Science · General 🥊 · Combat Sports

Echofication: Conversational AI That Catches Lies, Probes Depth, and Never Goes Off-Script

The Interviewer’s Dilemma

A smart interviewer walks into a room with a checklist, not a script. They know what they are looking for in a candidate. Calm, composed -they listen to what the candidate has to offer. If the candidate says something vague, they probe once-only once. If the answer holds up, they move on. If it doesn’t, they still move on- but they have already noted it.

That is not a limitation. That is experience , discipline and confidence.

Most conversational AI systems choose one of the two extremes — either they follow a rigid script with no room to think, or they are given too much freedom and wander off into unpredictable territory. Neither mirrors how a sharp interviewer actually operates.

Echofication is built on a simpler philosophy: ask your question, hear the answer, probe once, store everything, judge later. No rabbit holes. No unnecessary conversation. Just structured depth — at scale.

The two traps of conversational AI Interviews

When building conversational AI systems for structured interviews that capture the organizational voice, you quickly run into a fork in the road.

Path 1: Go fully brute. Hardcode each question into the system prompt- safe, consistent, reproducible In this, the AI becomes a parrot. It reads questions, collects answers, moves on. There is no thinking happening, just pure mechanical execution — that can be done by traditional TTS as well can it not? In this method your tokens stack up fast — and now you’re paying for rigidity

Path 2: Give AI a rough brief of the types of questions and then let it figure out what to ask. Sounds elegant in theory, but in actual practice, it is unpredictable, inconsistent and the level of 2 successive interviews could be biased. One candidate gets a deep philosophical question, another gets something trivial — not fair. You can’t audit it, you can’t defend it, and HR will never sign off on it.

The real problem isn’t the questions. It’s that neither approach captures how a skilled interviewer actually operates — structured enough to cover every required topic, intelligent enough to know when something needs a second look.

That gap is exactly what Echofication was built to close.

The two traps — and the middle ground

The two traps — and the middle ground

The Interviewer I Was Trying To Replicate

The idea didn’t come from a research paper. It came from watching how a good interviewer actually behaves.

They come to the screen or walk in the room with a checklist — not a script. They don’t deviate from it — every candidate gets an equal stage to represent themselves , because fairness and consistency demand it. But they’re not robots. When a candidate says something interesting — or suspicious — they pause, and ponder, but don’t express. They follow up with just one more question. Just one. Then they move on.

They don’t spiral into a rabbit hole. They don’t skip the follow-up either. They probe once, note their response, and let the final judgement come later — when they’re reviewing everything together.

That is the mental model Echofication is built on:

Ask your question. Hear the answer. Probe once. Store everything. Judge later.

No unnecessary conversation. No unpredictable detours. Just structured depth — applied consistently, at scale, across every single candidate.

The moment I framed it that way, the architecture became obvious. Three layers. Each with a single responsibility. None of them trying to do more than they should.

Echofication isn’t a single clever prompt. It’s an architecture — three distinct layers, each doing exactly one job, none of them overstepping.

Here’s how (the explanation considers the example for a hiring system I created with Echofication for the academic hiring at the place I work .The same logic can be applied to each industry with a minor tweak):

The Echofication architecture — three layers, one responsibility each, connected by a single conversation history

The Echofication architecture — three layers, one responsibility each, connected by a single conversation history

Layer 1: Structure — The Question bank

The foundation is simple. Five categories, one question per category, selected deterministically by interview ID. Every candidate faces the same scope.. No randomness, no favouritism.

CATEGORY_ORDER = [
    "ACADEMIC_BACKGROUND",
    "TEACHING_EXPERIENCE",
    "RESEARCH_PUBLICATIONS",
    "SUBJECT_KNOWLEDGE",
    "MOTIVATION_FIT",
]

async def _select_questions(conn, department, seed):
    rng = random.Random(seed)
    selected = []
    for category in CATEGORY_ORDER:
        rows = await conn.fetch(
            """
            SELECT id, category, question_text, expected_themes, difficulty
            FROM question_bank
            WHERE is_active = TRUE
              AND category = $1
              AND (department = $2 OR department IS NULL)
            """,
            category, department,
        )
        dept_rows = [r for r in rows if r["department"] == department]
        pool = dept_rows or rows
        selected.append(rng.choice(pool))
    return selected

The questions are pulled from a live bank filtered by department and category which is created by the HR department — so a Computer Science candidate gets questions relevant to their field, but the structure remains identical across all interviews.

Odd turns 1,3,5,7, and 9 are always pre-built questions. Always.

Even turns 2,4,6,8, and 10 are always AI-generated follow-ups. Always.

if turn_number % 2 == 1:
    # Odd turn — answer to a pre-built question
    q_idx = (turn_number - 1) // 2
    question_asked = selected[q_idx]["question_text"]
    question_type = "PRE_BUILT"

    # Generate the follow-up for the next turn
    followup_q, _ = await generate_followup(
        turns=turns,
        resume_extract=resume_extract,
        key_claims=key_claims,
        candidate_position=interview["position_applied"],
    )
    next_question = followup_q

else:
    # Even turn — follow-up just answered, advance to next pre-built
    if turn_number < 10:
        next_q_idx = turn_number // 2
        next_question = selected[next_q_idx]["question_text"]
    else:
        interview_completing = True
#Ten turns total. Five pre-built, five follow-ups. When turn 10 completes,
# the judge is triggered.

Turn architecture of Echofication.

Turn architecture of Echofication.

Layer 2: Depth — The Follow-Up

This is where Echofication earns its name.

After every pre-built question is answered, the system generates exactly one follow-up. Not two. Not a chain. One.

MAX_TRANSCRIPT_CHARS = 12000  # ~3k tokens

def build_transcript(turns):
    lines = []
    for t in turns:
        q = t.get("question", "").strip()
        a = (t.get("answer") or "").strip()
        lines.append(f"[Turn {t['turn_number']} | {t['question_type']}] Q: {q}")
        if a:
            lines.append(f"A: {a}")
    return "\n".join(lines)

def prune_transcript(transcript, max_chars=MAX_TRANSCRIPT_CHARS):
    """Keep head + tail. Drop the middle. Preserves recency and opener."""
    if len(transcript) <= max_chars:
        return transcript
    head = transcript[:max_chars // 3]
    tail = transcript[-(2 * max_chars // 3):]
    return f"{head}\n\n[...transcript trimmed...]\n\n{tail}"

But it’s not a generic follow-up. The model is grounded in three things: the candidate’s resume extract (extracted during an earlier step in JSON), their key claims, and a pruned rolling summary of the conversation so far. It’s been explicitly instructed to look for contradictions, probe state strengths, and reference specific details.

FOLLOWUP_SYSTEM = (
    "You are an academic interviewer probing a faculty candidate. "
    "Generate exactly ONE sharp follow-up question (under 40 words). "
    "Reference a SPECIFIC detail from the resume or key claims. "
    "If the answer contradicts the resume, probe the inconsistency. "
    "If the answer shows depth, push deeper on that strength. "
    "Output ONLY the question text. No preamble, no formatting."
)

async def generate_followup(*, turns, resume_extract, key_claims, candidate_position):
    transcript = prune_transcript(build_transcript(turns))

    user = (
        f"POSITION: {candidate_position or 'Faculty'}\n\n"
        f"RESUME_EXTRACT:\n{json.dumps(resume_extract or {})[:4000]}\n\n"
        f"KEY_CLAIMS:\n{json.dumps(key_claims or [])[:1500]}\n\n"
        f"TRANSCRIPT SO FAR:\n{transcript}\n\n"
        f"Generate ONE follow-up question now."
    )
    content, _ = await chat(
        messages=[
            {"role": "system", "content": FOLLOWUP_SYSTEM},
            {"role": "user", "content": user},
        ],
        max_tokens=120,
        temperature=0.5,
    )
    return content.strip().strip('"'), _

The rolling summary is the quiet hero here — instead of passing the entire growing transcript into every follow-up call, the system keeps a head-and-tail trim of the conversation. First impressions preserved, most recent context prioritized, middle trimmed if needed. Token costs stay flat regardless of how long the interview runs.

Layer 3: Evaluation — The Judge

Once turn 10 is complete, a single separate LLM call receives everything — the full transcript, resume, key claims, and candidate summary — and scores the candidate across five dimensions which may vary from industry to industry:

  • Academic qualification
  • Teaching experience
  • Research and publications
  • Subject knowledge
  • Communication
SCORING_SYSTEM = (
    "You are a senior academic hiring evaluator. "
    "Score the candidate on a strict rubric using the full transcript and resume. "
    "Return ONLY a single valid JSON object. No preamble, no markdown."
)

async def score_interview(*, turns, resume_extract, key_claims, candidate_summary):
    transcript = prune_transcript(build_transcript(turns), max_chars=14000)

    user = (
        f"CANDIDATE_SUMMARY:\n{json.dumps(candidate_summary or {})[:1500]}\n\n"
        f"RESUME_EXTRACT:\n{json.dumps(resume_extract or {})[:5000]}\n\n"
        f"KEY_CLAIMS:\n{json.dumps(key_claims or [])[:2000]}\n\n"
        f"FULL TRANSCRIPT:\n{transcript}\n\n"
        f"{RUBRIC}\n\n"
        f"Return JSON in this exact shape:\n{SCHEMA}"
    )
    content, _ = await chat(
        messages=[
            {"role": "system", "content": SCORING_SYSTEM},
            {"role": "user", "content": user},
        ],
        max_tokens=900,
        temperature=0.2,
        json_mode=True,
    )
    return _normalise(parse_json_strict(content)), _

The overall score is weighted and recomputed independently after the model responds — so even if the model drifts in its self-reported score, the system corrects it. The judge can’t cheat its own rubric.

def _normalise(raw):
    q = _clamp(raw.get("score_qualification"))
    e = _clamp(raw.get("score_experience"))
    r = _clamp(raw.get("score_research"))
    s = _clamp(raw.get("score_subject_knowledge"))
    c = _clamp(raw.get("score_communication"))

    overall_calc = round(
        q * 0.20 + e * 0.20 + r * 0.25 + s * 0.25 + c * 0.10, 2
    )
    overall = _clamp(raw.get("score_overall", overall_calc))

    # If model drifts more than 1.0 from the formula, override it
    if abs(overall - overall_calc) > 1.0:
        overall = overall_calc

    rec = raw.get("ai_recommendation")
    if rec not in ALLOWED_RECS:
        rec = _derive_recommendation(overall)

The output: a score, a recommendation (STRONG YES/ YES/ MAYBE/ NO), a summary citing specific transcript evidence, and evaluator notes.

The Thread That Connects All Three

Every turn is appended to a single shared JSONB blob in the database — the conversation history. It carries the selected questions and full turn log. Each layer reads from it, writes to it, and moves on.

history["turns"] = turns
async with conn.transaction():
    await conn.execute(
        "UPDATE faculty_interviews SET conversation_history = $1::jsonb WHERE id = $2",
        json.dumps(history), interview_id,
    )

No WebSocket, no persistent connection, no real-time streaming required. A straightforward HTTP request-response pattern is sufficient — and that simplicity is a feature, not a compromise.

Why it works

“Decisions That Made The Difference”

Good architecture isn’t just about what you build. It’s about the calls you make along the way — what you constrain, what you trust the model with, and what you deliberately keep out of its hands.

Here are the four decisions that make Echofication work in production.

  1. One Follow-Up. Not Two. Not a Chain.

The easiest mistake to make here is letting the follow-up loop.If the candidate gives a vague answer to the follow-up,why not generate another? And another?

Because that’s a rabbit hole with an API bill at the bottom.

Capping at one follow-up per question keeps the interview feeling natural, keeps costs predictable, and — critically — keeps the system auditable. Every interview has exactly 10 turns. Always. You can reason about it, debug it, and explain it to a non-technical stakeholder without drawing a flowchart.

Vague answers don’t get a second chance at the depth layer. They get flagged at the evaluation layer. That’s the judge’s job, not the follow-up’s.

  1. Rolling Summary Instead of Full History.

Every follow-up call could receive the entire conversation transcript. But as the interview grows, so does your input token count — linearly, predictably, expensively.

Instead, the system passes a pruned transcript — head and tail preserved, middle trimmed if needed. First impressions stay. Most recent context stays. Everything in between is summarized away.

The trade-off is minimal. The follow-up call doesn’t need to remember what was said three turns ago in precise detail — it needs to know the opening context and the most recent exchange. The full transcript is saved anyway for the judge.

Token costs stay flat. Quality stays high.

Token usage- Full history vs Pruned Summary (Echofication capability)

Token usage- Full history vs Pruned Summary (Echofication capability)

  1. The Judge Is a Separate Call — Always.

The follow-up generator and the judge are never the same LLM call. This is non-negotiable.

Asking a single call to both generate a follow-up question and evaluate the candidate simultaneously is taking it to hold two conflicting objectives. Models under that kind of prompt pressure tend to hedge — the follow-up gets softer, the evaluation gets muddier.

Separation of concerns isn’t just a software engineering principle. It applies directly to the prompt design.

The judge receives everything — full transcript, resume, key claims, candidate summary — and does one thing: verdict. Low temperature, JSON mode, rubric-enforced, formula-verified. No ambiguity.

  1. No WebSocket Required.

Most developers’ first instinct when building conversational AI is to reach for the WebSockets. So was mine truthfully. Because they real-timed, bidirectional, persistent connections. It feels right for a “live” interview.

But Echofication is turn-based by design. The candidate answers, the system processes, the next question is returned. There’s no streaming, no typing indicator, no need for a live pipe between client and server.

A plain HTTP request-response pattern is sufficient for this use case. And that simplicity compounds in production — no socket servers to manage, no dropped connection edge cases to handle, easier horizontal scaling, smaller failure surface.

The best infrastructure decision is often the one that you don’t make.

What Production Actually Looks Like.

Most AI demos are impressive until they meet real users. Echofication was built for a different standard — not a demo, but a hiring pipeline that actual organizations depend on.

After deployment at my current organization, the most telling result was the absence of drama from the system. I was prepared counter unexpected errors and failures, but surprisingly the system withheld itself. It ran interviews end-to-end without deviation. Every candidate got the same five questions. Every follow-up was contextually relevant. Every judge call returned a structured, rubric-consistent verdict.

In production AI, that’s not a small thing. Consistency at scale — across departments, across candidates, across sessions — is exactly what an organization needs from a hiring tool. Not brilliance on a good day. Reliability on every day.

The architecture held up because the constraints were right from the start. And having built other variants of the hiring pipeline had given me the idea of where do these systems fail, so mitigating them was the first action I took while building this. One follow-up. Flat token costs. Separated concerns. A judge that can’t drift from its own rubric.

Sometimes the best thing a system can do is exactly what it promised — nothing more, nothing less.

Catches Lies. Probes Depth. Never Goes Off-Script.

That’s not just a title. That’s a spec.

Echofication catches lies because the follow-up forces the candidates to go one level deeper than a rehearsed answer can confortably go. The judge then reads the full transcript — and inconsistencies don’t survive a rubric-enforced evaluation with transcript-cited evidence.

It probes depth because every answer to a pre-built question earns exactly one intelligent follow-up — grounded in the candidate’s own resume, their state claims, and the conversation so far. Not a generic probe. A specific one.

It never goes off-script because the structure layer is immutable. Five categories. One question each. Seeded deterministically. Every candidate, every time.

Three guarantees. Three layers. One architecture.

I didn’t find this technique in a research paper. I built it because I needed it — and because neither of the existing approaches was good enough. If you’re building conversational AI for structured, high-stakes interactions, I hope Echofication gives you a starting point.

The name might be mine. The problem isn’t.


메타데이터
post_id
a2cdb7941e09
slug
echofication-conversational-ai-that-catches-lies-probes-depth-and-never-goes-off-script-a2cdb7941e09
url
https://medium.com/towards-explainable-ai/echofication-conversational-ai-that-catches-lies-probes-depth-and-never-goes-off-script-a2cdb7941e09
canonical_url
https://medium.com/towards-explainable-ai/echofication-conversational-ai-that-catches-lies-probes-depth-and-never-goes-off-script-a2cdb7941e09
author_url
https://medium.com/@pandeyaditya0088
status
ok
fetched_at
2026-06-15 20:49:13