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.

Say this in the interview
- 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.
- 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.
- Not every step needs an LLM. Deterministic work stays in code, and easy steps can run on a smaller, cheaper model.
- 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.
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.
- 1Pick the tablesLLM
→ ["orders", "customers", "refunds"]A small, cheap model is enough. - 2Write the SQLLLM
→ SELECT c.name, SUM(r.amount) FROM refunds r …Sees only those 3 schemas + the question. - 3Run the queryCODE · NO LLM
→ 12 rowsNot every step needs an LLM. - 4Explain 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.
SUM(r.refund_amt)- ✓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)
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.
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
✗ 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).
✓ 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
Later steps only see what you pass them. Step 4 needs the original question, not just the rows.
With each step’s input and output logged, you can build a test set per step and catch regressions where they start.
A small, fast model for table picking, a stronger one for SQL. It’s a cost and latency lever you choose step by step.
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.
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
- 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.
- 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.
- 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.