Lesson 04 of N

Prompt Chaining

Breaking complex tasks into sequential LLM calls — with validation gates to stop errors from cascading.

← 03 Implementation 05 Routing →

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.

One giant prompt — hard for the LLM to get right "Analyse this data, validate it, score risks, categorise them, and write a report" vs. Step 1 Analyse data LLM output Step 2 Validate findings LLM output Step 3 Score & categorise LLM output Step 4 Write report LLM Done Simpler instructions Better accuracy per step Less hallucination Enhanced reasoning Each step is focused, testable, and replaceable independently

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.

Step 1 small mistake ! Step 2 builds on bad data ! Step 3 error compounds ! Step 4 completely wrong output ! A small mistake in step 1 becomes a catastrophic failure by step 4 Like a rounding error in the first line of a financial report — every subsequent calculation is wrong

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.

Step 1 LLM call OK? pass fix Step 2 LLM call OK? pass fix Step 3 LLM call OK? pass fix Clean output Every step's output is validated before moving forward. Errors are caught and fixed at the source.

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.)

Programmatic Check
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.

Rule-Based Validation
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:

Simple Robust 1. Retry Just re-run the same prompt. Works for transient LLM issues — random failures, API hiccups. Step fail → retry pass Next 2. Re-prompt with Feedback Validation says why it failed ("summary too long"). Feed that critique back into a revised prompt. The key to iterative refinement — the LLM learns from its own mistakes. 3. Fallback Mechanisms Retries exhausted? Terminate and log, route to a simpler alternative task, or return a safe default. Prevents infinite retry loops. The system degrades gracefully instead of crashing. 4. Critique and Refinement Two-step: first prompt the LLM to critique its own output against criteria. Then prompt it to refine based on that critique. Self-correction without external validation. 5. Logging and Monitoring Not a fix strategy — a diagnostic one. Log inputs, outputs, and validation results at every step. Without logs, debugging a failing chain is guesswork. With them, you can pinpoint exactly which step produced bad output and why.

Production Chains Need Tracing. Strategy #5 deserves special emphasis. In production, every chain step should capture:

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:

What Each Step Receives Step 1: Classify Ticket INPUT "My order #4821 arrived damaged. I need a replacement ASAP. - Maria" OUTPUT (full) category: "damaged_product" urgency: "high" order_id: "#4821", customer: "Maria" sentiment: "frustrated", confidence: 0.94 ... Step 2: Draft Response RECEIVES (selective) category: "damaged_product" urgency: "high" customer: "Maria", order: "#4821" NOT passed: sentiment, confidence, raw text OUTPUT: polished email draft Step 3: Translate RECEIVES email draft (from step 2) target_lang: "Spanish" REITERATED from step 1 urgency: "high" (keep tone urgent) NOT passed: category, order_id Pass Everything (bloated) Step 3 prompt: "Here is the original ticket, the classification result with all 8 fields, the full draft with edit history, and the customer profile. Translate to Spanish." Selective + Reiterate (focused) Step 3 prompt: "Translate this email to Spanish. This is a high-urgency response — preserve the urgent, empathetic tone. [email draft here]" LLM drowns in irrelevant context Token waste, distracted output, risk of hitting context window limits LLM gets exactly what it needs Fewer tokens, focused output, urgency constraint preserved

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.

Topic "Agentic AI" Researcher Agent system: "research specialist..." outputs: # OVERVIEW # KEY POINTS # DETAILS LLM research output → input Drafter Agent system: "content drafter..." receives topic + research as context LLM Article polished output Each agent is stateless — the research variable is the entire context transfer between them.

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.

agentic_workflows/prompt_chaining.py
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.

Four Agents, One Fan-In Startup Pitch "AI code review SaaS" Pitch Analyst business proposition, revenue model, team LLM analysis Market Assessor TAM, competition, growth potential, timing LLM market Risk Analyst financial, technical, market, execution risks LLM market risks Investment Recommender weighs market opportunity against identified risks LLM FAN-IN The recommender receives labelled sections from two distinct sources — not a single linear handoff. This is the simplest topology beyond a straight chain: multiple streams converging at a decision point. 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:

Sequential A B C Each step depends on the previous. Research → Drafter exercise Parallel + Fan-In A B C D Multiple streams merge at a decision point. Pitch exercise — branches run sequentially Conditional A if? B C Path depends on prior output. Coming in later lessons Topology Comparison Sequential Simple, predictable Latency = sum of all steps Easiest to debug Parallel + Fan-In Independent streams merge Latency = slowest branch* Richer synthesis at merge Conditional Dynamic path selection Skips unnecessary work Bridges toward agentic

* 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

← 03 Implementation 05 Routing →