← AI Concepts

Interview Brush-Up · AI · LLM workflows

Prompt chaining

In one line: Fixed steps, with plain-code checks between them.

Your prompt isn’t bad. It’s doing four jobs at once. Prompt chaining splits it into fixed steps, so each step does one job, plain code checks the output between steps, and a failure retries only the step that broke.

Reel cover: Surya holding a chain of three prompts
BRUSH UP IN 5 MINUTES

Say this in the interview

  1. I split one task into fixed steps. Each step’s output is the next step’s input, and my code decides the steps, not the model. That makes it a workflow, not an agent.
  2. Between steps I put gates in plain code: validate the structured output, and on failure retry only that step with the error. Retries are bounded, then it fails clearly.
  3. Not every step needs an LLM. Deterministic work stays in code, and easy steps can run on a smaller, cheaper model.
  4. The trade-off is more calls and more latency, in exchange for accuracy, debuggability and per-step testing. If one prompt is already reliable, I don’t chain.

Follow-ups you’ll get

Errors compound across steps. How do you handle that?

Gates after the risky steps, so a bad output is caught where it happens and only that step retries. At 95% per step, three unchecked steps land around 86%.

The chain is slow. What do you do about latency?

Run independent steps in parallel, use smaller and faster models for the easy steps, stream the last step to the user, and cache step outputs that repeat.

How is this different from an agent?

In a chain, your code fixes the steps in advance. An agent decides its next step itself. If you can write the steps down, chain; agents cost more to run and test.

How do you test it?

Log every step’s input and output, build a small eval set per step, and test each step on its own. With one giant prompt you can only test the end result.

01 One huge prompt

A “chat with your database” feature. One prompt gets everything and has to do everything.

# 1. Schema: 200+ tables
CREATE TABLE orders (id, customer_id, total, created_at);
CREATE TABLE customers (id, name, city, joined_at);
CREATE TABLE refunds (id, order_id, amount, reason);
-- … 197 more tables
# 2. User question
"Which customers got the most refunds last month?"
# 3. Do all of this:
- find the right tables · write the SQL · explain the result
ANSWER Customer #4821 got the most refunds ✗ wrong
wrong tables?bad SQL?bad summary?

Which part failed? You can’t tell. And you can’t fix or test one part alone.

02 Split it into steps

Each step does one job. Each step’s output is the next step’s input.

  1. 1
    Pick the tablesLLM
    → ["orders", "customers", "refunds"]A small, cheap model is enough.
  2. 2
    Write the SQLLLM
    → SELECT c.name, SUM(r.amount) FROM refunds r …Sees only those 3 schemas + the question.
  3. 3
    Run the queryCODE · NO LLM
    → 12 rowsNot every step needs an LLM.
  4. 4
    Explain the resultLLM
    → "3 customers got 60% of the refunds…"Gets the original question too, not just the rows.

03 The real power is between the steps

A check in plain code between two steps. Anthropic calls it a gate. Here it sits between step 2 and step 3.

2 Write the SQL SUM(r.refund_amt)
GATE · PLAIN CODE
  • ✓Exactly one statement, and it’s a SELECT
  • ✗Do these columns exist? refund_amt not found
  • –Does EXPLAIN pass? (plans it, doesn’t run it)
↩ error goes back to step 2 · retry only that step · max 2 retries, then fail clearly
3 Run the query only after the gate passes

The gate isn’t your only defence. A SELECT-only check can still be beaten by an expensive query. Run generated SQL as a read-only DB user, on a replica, with a statement timeout and a row limit.

04 What it looks like in code

A sketch in Python. llm_json, llm_text, run_readonly and explain are your own thin helpers over any LLM SDK and DB driver. The gate uses sqlglot to parse the SQL.

import sqlglot
from sqlglot import exp

MAX_RETRIES = 2

def answer(question: str) -> str:
    # Step 1 (LLM, small model): pick tables. JSON out, so code can check it.
    tables = llm_json(PICK_TABLES, question=question, tables=list(SCHEMA), model="small")
    if not tables or not set(tables) <= SCHEMA.keys():      # gate
        raise ChainError("step 1: unknown tables", tables)

    # Step 2 (LLM): write SQL from only those schemas. Gate it; retry only this step.
    error = None
    for _ in range(MAX_RETRIES + 1):
        sql = llm_text(WRITE_SQL, question=question,
                       schemas={t: SCHEMA[t] for t in tables}, last_error=error)
        error = check_sql(sql, tables)                         # gate
        if error is None:
            break
    else:
        raise ChainError("step 2: no valid SQL", error)        # fail clearly

    # Step 3 (code, no LLM): read-only user, statement timeout, row limit.
    rows = run_readonly(sql, timeout_s=5, max_rows=500)

    # Step 4 (LLM): explain. Pass the original question, not just the rows.
    return llm_text(EXPLAIN_ROWS, question=question, rows=rows)


def check_sql(sql: str, tables: list[str]) -> str | None:
    """The gate: plain code. Returns an error for the LLM to fix, or None."""
    try:
        stmts = sqlglot.parse(sql, read="postgres")
    except sqlglot.errors.ParseError as e:
        return f"parse error: {e}"
    if len(stmts) != 1 or not isinstance(stmts[0], exp.Select):
        return "send exactly one SELECT statement"
    known = {col for t in tables for col in SCHEMA[t]}
    aliases = {e.alias for e in stmts[0].expressions if e.alias}  # ORDER BY total
    missing = {c.name for c in stmts[0].find_all(exp.Column)} - known - aliases
    if missing:
        return f"columns not found: {sorted(missing)}"
    try:
        explain(sql)                       # EXPLAIN plans the query without running it
    except DatabaseError as e:
        return f"EXPLAIN failed: {e}"
    return None
  • Structured output between steps. Step 1 returns JSON, so the gate can check it. Free text between steps is where chains break.
  • The retry carries the error. Step 2 gets last_error, so the model fixes the actual problem instead of guessing again.
  • Retries are bounded. After 2, the chain fails with a clear error instead of looping or returning a confident wrong answer.

05 Why it matters: errors multiply

If each step is right 95% of the time, three steps in a row are right only about 86% of the time. Move the sliders.

NO GATES85.7%
WITH GATES96.4%

Illustration, not a benchmark. With gates, a step is right first time, or its miss is caught and the retry is right: p + (1 − p) · catch · p, then multiplied across the steps.

06 The trade-off

YOU PAY

✗ More LLM calls. One call per LLM step instead of one.

✗ More latency. Steps run one after another, so 3 LLM calls are roughly 3 round trips. Run independent steps in parallel (episode #3).

YOU GET

✓ Easy to debug. Log each step’s input and output, and test each step on its own.

✓ Cheaper models. Each call is smaller, so easy steps like picking tables can run on a small model. One giant prompt has no such lever.

07 Senior notes

Context loss

Later steps only see what you pass them. Step 4 needs the original question, not just the rows.

Evals per step

With each step’s input and output logged, you can build a test set per step and catch regressions where they start.

Model per step

A small, fast model for table picking, a stronger one for SQL. It’s a cost and latency lever you choose step by step.

When not to chain

If one prompt is already reliable, chaining only adds calls and latency. If you can’t write the steps down in advance, you need an agent, not a chain.

✅ THUMB RULE

Split your prompt wherever you’d want to check the output.

08 Interview check

An LLM turns resumes into a JSON profile, then scores each profile against the job description. Where do the gates go?

Show one good answer
  1. Gate after extraction, before scoring. Validate the JSON against a schema (required fields, types). Sanity-check it: dates parse, end ≥ start, total experience fits the career span. Check every extracted skill actually appears in the resume text, because the model can invent skills. On failure, retry extraction with the error.
  2. Gate after scoring. The score is in range, and every reason points to a field that exists in the profile. Low confidence or a failed check goes to a human, not to the hiring manager’s shortlist.
  3. Why two steps at all: you can test extraction on its own (did it read the resume right?) separately from scoring (did it judge fairly?). One prompt hides which one went wrong.

Sources: Anthropic, Building effective agents (Dec 2024) · Anthropic docs, Chain complex prompts.