The Core Idea
Trying to solve a complex task in a single Large Language Model (LLM) call is like asking one person to build an entire car. Prompt chaining breaks the problem into a sequence of smaller, focused steps — each one a separate LLM call, where the output of one becomes the input to the next.
Why It Works
A focused LLM call with a clear, narrow task outperforms a sprawling one-shot prompt almost every time. Each link in the chain only needs to do one thing well, using the output of the previous link as its starting point. The result: better accuracy, less hallucination, and reasoning that builds across steps.
The Cost: Latency and Spend. Every link in a sequential chain adds a synchronous round-trip API call. A 4-step chain with validation gates can push a user-facing interaction from ~2 seconds to 12+ seconds, and each step costs tokens. Prompt chaining is a deliberate trade-off: you are exchanging latency and API cost for accuracy and controllability. This is often the right trade-off for back-office workflows, but think carefully before putting a long chain in a real-time user path.
In Production: It's Not Just LLM → LLM. The examples in this lesson chain LLM calls together. But real-world chains are hybrid — they interleave LLM calls with tool calls, API requests, database queries, and programmatic logic:
LLM (classify) → API (fetch customer record) → Validation (check policy) → LLM (draft response) → Database (log interaction) → LLM (translate)
The chaining pattern is identical — output feeds into input, validation gates sit between steps. The difference is that not every step is an LLM call. Many are deterministic tools, APIs, or business rules. Think of prompt chaining as action chaining where LLM calls are one type of action among many.
The Danger: Error Propagation
The same dependency that makes chaining powerful also makes it fragile. If an early step produces flawed output, that error compounds through every step that follows.
This is the fundamental tension: chaining gives you better step-by-step accuracy, but introduces dependency risk. The solution: validation gates between every step.
Validation Gates
Quality control checkpoints between each step in the chain. Before an output is passed to the next step, a gate checks whether it's accurate, relevant, and correctly formatted. Catch errors early, before they cascade.
Four Types of Validation
Programmatic Checks
Code that verifies concrete conditions: is the output valid JSON? Does the summary meet the length requirement? Is the number in range?
Best for: structural correctness, format, data types
LLM-Based Validation
A second LLM call that evaluates the first one's output. Can assess nuanced qualities: factual accuracy, relevance, tone, completeness.
Best for: semantic quality that's hard to codify as rules
Rule-Based Validation
Predefined explicit rules: "email must have a salutation and closing," "analysis must reference at least 3 sources." Pattern matching against a declared ruleset.
Best for: business requirements, compliance, format standards
Confidence Scoring
Uses the LLM's own confidence scores, checked against a threshold. Outputs below the threshold trigger corrective action.
Best for: gauging uncertainty, triggering human review
Caveat: LLM self-reported confidence is often poorly calibrated — it does not equal probability of correctness. Treat it as a heuristic signal, not a reliability measure.
Validation in Code
Each validation type translates to a concrete gate function that sits between chain steps. (chat() is the thin LLM wrapper used throughout this series — it sends a prompt and returns the text reply.)
import json
def validate_json_output(output):
"""Gate: is it valid JSON with required keys?"""
try:
data = json.loads(output)
except json.JSONDecodeError:
return False, "Output is not valid JSON"
required = ["category", "urgency", "summary"]
missing = [k for k in required if k not in data]
if missing:
return False, f"Missing keys: {missing}"
return True, data
LLM-Based Validation
def validate_with_llm(original_input, agent_output):
"""Gate: does the output actually address the input?"""
verdict = chat(
user_prompt=f"""INPUT:\n{original_input}\n\nOUTPUT:\n{agent_output}\n\n
Does the output accurately and completely address the input?
Reply with exactly PASS or FAIL followed by a one-line reason.""",
system_prompt="""You are a strict quality reviewer. Evaluate whether
the output faithfully addresses the input.""",
temperature=0,
max_tokens=60,
)
passed = verdict.strip().upper().startswith("PASS")
return passed, verdict
Production note: Parsing raw text with .startswith("PASS") is fragile — an LLM might return "Based on my analysis, PASS." which starts with "B", not "P", and your gate silently fails. In production, use structured outputs (e.g. response_format={"type": "json_object"} in the OpenAI Application Programming Interface (API), or libraries like instructor / Pydantic) to force the model to return an exact schema like {"passed": true, "reason": "..."} — no string parsing needed.
def validate_email_rules(email_text):
"""Gate: does the email meet business format requirements?"""
rules = {
"has greeting": any(email_text.lower().startswith(g)
for g in ["dear", "hi", "hello"]),
"has closing": any(c in email_text.lower()
for c in ["regards", "sincerely", "thank you"]),
"under 500 words": len(email_text.split()) < 500,
}
failures = [r for r, ok in rules.items() if not ok]
return len(failures) == 0, failures
Confidence Scoring
def validate_confidence(agent_output, threshold=0.7):
"""Gate: ask the LLM to self-rate, reject below threshold."""
score_text = chat(
user_prompt=f"""Rate your confidence in this output on a scale of
0.0 to 1.0. Reply with ONLY the number.\n\n{agent_output}""",
system_prompt="You are a calibration judge. Output a single float.",
temperature=0,
max_tokens=10,
)
try:
score = float(score_text.strip())
except ValueError:
return False, "Could not parse confidence score"
return score >= threshold, score
Using Gates in a Chain
Simple retry# Between step 1 and step 2: validate then pass or retry
step1_output = classifier_agent(ticket)
passed, detail = validate_json_output(step1_output)
if not passed:
print(f"Gate failed: {detail} -- retrying step 1")
step1_output = classifier_agent(ticket) # retry
step2_output = responder_agent(step1_output)
Re-prompt with feedback (more robust)
def run_with_gate(agent_fn, gate_fn, input_data, max_retries=2):
"""Run an agent, validate output, re-prompt with feedback on failure."""
output = agent_fn(input_data)
for attempt in range(max_retries):
passed, detail = gate_fn(output)
if passed:
return output
# Re-prompt with structured feedback
output = agent_fn(
f"--- ORIGINAL INPUT ---\n{input_data}\n\n"
f"--- GATE FEEDBACK ---\n"
f"Previous attempt rejected: {detail}. Fix the issues."
)
return output # fallback: return best effort after retries
# Usage
classification = run_with_gate(classifier_agent, validate_json_output, ticket)
response = responder_agent(classification)
Cost note: Every retry re-runs the full agent call — you pay for the tokens again. Set max_retries conservatively (1–2 is typical) and log every retry so you can monitor spend. An infinite retry loop against a stubborn validation gate will drain your API budget fast.
When Validation Fails: Error Handling
A gate catches a bad output. Now what? Five strategies, from simplest to most sophisticated:
Production Chains Need Tracing. Strategy #5 deserves special emphasis. In production, every chain step should capture:
- Inputs and outputs — what went in, what came out
- Prompt version — which system prompt was active
- Model and parameters — model ID, temperature, max_tokens
- Latency and cost — how long each step took, token usage
- Validation outcomes — which gates passed, which failed, how many retries
Without traces, debugging a 4-step chain means replaying each step manually and guessing where things went wrong. With traces, you can pinpoint the exact step, the exact input that broke it, and the exact prompt version that was active.
Context Management
Each step needs context from previous outputs, but too much context degrades performance — the LLM gets distracted or hits token limits. Two strategies to balance this:
Selective Context Passing
Only pass the relevant parts of previous outputs. Plan what each step truly needs. Don't forward the entire conversation history — extract the key data points.
Chaining itself helps: distributing work across prompts naturally splits context across smaller windows.
Contextual Reiteration
Restate critical earlier details in each new prompt to prevent the LLM from "forgetting." Especially useful when a later step depends on constraints established early in the chain.
Goal: precisely the necessary and sufficient context for each step. No more, no less.
Example: Customer Support Escalation Chain
A three-step chain that classifies a support ticket, drafts a response, and translates it. Watch what each step actually needs from the previous one:
The Balancing Act. Too little context: the LLM makes assumptions or contradicts earlier steps — a translator that doesn't know the urgency level produces a casual tone for a critical issue. Too much context: the LLM gets distracted, wastes tokens on irrelevant details, or hits the context window limit. The fix: plan your context like you plan your data model. For each step, ask: "What does this step need to act?" and "What constraint from an earlier step must it preserve?" Pass the first. Reiterate the second. Drop everything else.
Worked Example: Research → Drafter Chain
The simplest real prompt chain: a researcher gathers structured facts, a drafter turns them into a polished article. Two function-based agents, each powered by an LLM, connected by a single variable.
Notice how agents are plain functions. The chaining happens in run_simple_chain — the researcher's return value is passed directly into the drafter's prompt.
def researcher_agent(topic):
return chat(
user_prompt=f"Research the following topic thoroughly:\n\n{topic}",
system_prompt="""You are a research specialist who provides structured
information. Always format with: # OVERVIEW # KEY POINTS # DETAILS""",
max_tokens=600,
temperature=0.3, # low = factual, focused
)
def drafter_agent(topic, research_results):
return chat(
user_prompt=f"Write an engaging article about: {topic}\n\n"
f"Use this research as your source material:\n\n{research_results}",
system_prompt="""You are a content drafter who creates engaging material
from research. Create a well-structured article...""",
max_tokens=800,
temperature=0.7, # higher = creative prose
)
def run_simple_chain(topic):
research = researcher_agent(topic)
article = drafter_agent(topic, research) # ← the chain
return {"research": research, "article": article}
What to Notice. The chain is one line: drafter_agent(topic, research) — the researcher's output is the drafter's context. Each agent gets a specialised system prompt with a different role; the same LLM powers both. Each function makes an independent stateless LLM call — the research variable is the complete context transfer.
Temperature Matters. Low temperature (near 0) = more deterministic, focused output. Good for the researcher who needs to stick to facts. Higher temperature = more creative, varied output. Good for the drafter who needs engaging prose. Matching temperature to the agent's role is a simple but effective way to shape behaviour. Common pitfall: do not invert this — factual tasks need low temperature; creative tasks need higher.
Worked Example: Startup Pitch Evaluation
A more ambitious chain with four specialised agents and a key new pattern: fan-in — one step receiving inputs from two or more earlier steps. The final recommender synthesises insights from two separate analysis streams.
agentic_workflows/pitch_evaluator.py# Agent 1: Understand the pitch
def pitch_analyst_agent(startup_pitch):
return chat(
user_prompt=f"Analyse this startup pitch: {startup_pitch}",
system_prompt="""You are a startup analyst who evaluates pitch decks.
Summarise the proposition, target market, revenue model...""",
max_tokens=600, temperature=0.2,
)
# Agent 2: Assess the market
def market_assessor_agent(pitch_analysis):
return chat(
user_prompt=f"""Based on this pitch analysis, assess the market:\n\n
{pitch_analysis}""",
system_prompt="""You are a market research specialist. Evaluate TAM,
competitive landscape, timing, and growth potential...""",
max_tokens=600, temperature=0.2,
)
# Agent 3: Identify risks
def risk_analyst_agent(market_assessment):
return chat(
user_prompt=f"Identify key risks based on this assessment: {market_assessment}",
system_prompt="""You are a risk analyst specialising in early-stage
investments. Rate each risk as high, medium, or low...""",
max_tokens=600, temperature=0.3,
)
# Agent 4: Synthesise — receives inputs from agents 2 AND 3
def investment_recommender_agent(market_assessment, risk_analysis):
return chat(
user_prompt=f"""Given the following market assessment:
--- MARKET ASSESSMENT ---
{market_assessment}
--- END MARKET ASSESSMENT ---
And the following risk analysis:
--- RISK ANALYSIS ---
{risk_analysis}
--- END RISK ANALYSIS ---
Provide a clear invest or pass recommendation with rationale.""",
system_prompt="You are a venture capital investment committee adviser...",
max_tokens=700, temperature=0.3,
)
def run_pitch_evaluation(startup_pitch):
analysis = pitch_analyst_agent(startup_pitch)
market = market_assessor_agent(analysis)
risks = risk_analyst_agent(market)
# fan-in: takes outputs from BOTH step 2 and step 3
return investment_recommender_agent(market, risks)
What's New Here. Fan-in pattern: the recommender receives market (from step 2) and risks (from step 3) — two analysis streams converge at a decision point. Labelled delimiters: wrapping each input in --- MARKET ASSESSMENT --- markers is selective context passing in practice — the LLM can distinguish which input is which. Low temperatures throughout: 0.2–0.3, because investment analysis demands precision, not creativity.
Orchestration Topologies
A topology is simply the shape of a chain — how its steps connect and feed one another. The two worked examples showed two shapes; there's a third that becomes important in later lessons:
* Implementation note: "Latency = slowest branch" in the parallel topology only holds if branches execute concurrently (e.g. asyncio.gather in Python). With synchronous code, parallel branches run sequentially and the latency equals the sum of all branches. True parallelism requires async orchestration.
Prompt Chaining vs. Agentic Systems
A common source of confusion: prompt chaining and agentic systems both involve multiple LLM calls, but they are fundamentally different architectures.
Prompt Chaining
- Fixed workflow — you define the steps at design time
- Deterministic path — the chain always executes the same sequence (or follows predefined conditional branches)
- Predictable cost — you know how many LLM calls a run will make
- Easier to debug — trace each step linearly, inputs and outputs are explicit
Agentic Systems
- Dynamic workflow — the model decides which step to take next
- Emergent path — execution varies based on intermediate results and tool availability
- Variable cost — the agent may loop, backtrack, or call tools unpredictably
- Harder to debug — non-deterministic execution requires richer observability
The Progression. Prompt chaining is not a lesser version of agents — it's a prerequisite. Every agentic system is built from chains and tool calls as composable primitives. Master the chain first: error propagation, validation gates, context management. These exact skills carry directly into agent design, where the stakes are higher because the model, not you, decides the execution path.
Exercise: Putting It All Together. Ready to see every technique in one pipeline? The Customer Support Ticket Pipeline exercise combines all four validation types, all five error handling strategies, and every context management technique into a single, runnable four-step workflow.
Lesson Recap
What You Now Know
- Task decomposition — break complex tasks into focused, sequential LLM calls; each step does one thing well, improving accuracy and reducing hallucination
- Hybrid chains — production chains interleave LLM calls with tool calls, API requests, and programmatic logic; not every step is a prompt
- Error propagation — the chain's dependency structure means early mistakes compound; this is the core risk of the pattern
- Validation gates — four approaches (programmatic, LLM-based, rule-based, confidence scoring) to catch errors between steps before they cascade
- Error handling — retry, re-prompt with feedback, fallback, self-critique, and logging; match the strategy to the severity
- Observability — trace inputs, outputs, latency, cost, and validation outcomes at every step; without traces, debugging chains is guesswork
- Context management — selective passing and contextual reiteration keep each step focused without overloading the LLM
- Topologies — sequential, parallel with fan-in, and conditional; most real workflows combine all three
- Chaining vs. agents — chains are fixed workflows; agents are dynamic; chains are the composable primitives that agents are built from