Skip to content
HG/os
LangChain Workspace

01 · Prompt Chaining

Sequential LLM calls where each output feeds the next — LCEL pipe syntax, input/output mapping, accumulating state, streaming, and error handling.

langchainlcelprompt-engineeringtutorial

Pattern: Sequential LLM calls where each output feeds the next. You'll learn: LCEL pipe syntax, input/output mapping between steps, accumulating state, streaming, and error handling.

1. What is prompt chaining?

Prompt chaining is the most fundamental composition pattern in LangChain. Instead of asking a single model call to do everything ("research this topic, outline it, and write an intro"), you break the work into discrete steps and pipe the output of one step into the input of the next.

topic  →  [Step 1: Research angle]  →  [Step 2: Outline]  →  [Step 3: Intro paragraph]

Each step is a small, focused prompt. This is the LLM equivalent of the Unix philosophy: small tools, composed together.

Why not just one big prompt?

One mega-promptChained prompts
Model juggles many goals at once → quality dropsEach call has one clear job → higher quality
Hard to debug ("which part went wrong?")Inspect the output of every step
Can't reuse piecesEach sub-chain is reusable
One temperature / model for everythingTune model, temperature, parsing per step
Failure = redo everythingRetry or cache individual steps

The trade-off is latency and cost: N steps means N sequential round-trips. Chain when the steps genuinely depend on each other. If they don't, reach for parallelization (lesson 02).

2. The building blocks

Prompt chaining in modern LangChain is built on LCEL — the LangChain Expression Language. LCEL lets you compose components with the | (pipe) operator, exactly like a shell pipeline.

The Runnable interface

Everything in LCEL is a Runnable — a standard object with .invoke(), .batch(), .stream(), and their async variants. Because every piece speaks the same interface, they snap together:

chain = prompt | llm | parser

Reading left to right: the prompt produces a message, the llm consumes it and produces an AIMessage, and the parser converts that message into a plain string.

The core pieces

ComponentRole
ChatPromptTemplateTurns input variables into a list of chat messages
init_chat_model(...) (the LLM)Calls the model, returns an AIMessage
StrOutputParserExtracts the .content string from the AIMessage
RunnablePassthroughPasses input through unchanged (used to keep earlier values around)
| (pipe)Connects two runnables — left output becomes right input

Initializing the model

We use init_chat_model rather than a provider-specific class like ChatOpenAI. It returns the same chat-model interface but lets you switch providers by changing config — not code. The model is read from .env in provider:model form, so a model swap never touches the notebook:

import os
from langchain.chat_models import init_chat_model
 
# .env → CHAT_MODEL=openai:gpt-4o-mini  (init_chat_model reads the provider from the prefix)
llm = init_chat_model(os.environ["CHAT_MODEL"], temperature=0.7)

To switch to Anthropic, you'd only edit .env (CHAT_MODEL=anthropic:claude-...) and install the matching integration package (e.g. langchain-anthropic).

3. Building the chain step by step

Step 1 — Define each prompt

Each step is its own prompt | llm | parser sub-chain:

angle_chain   = angle_prompt   | llm | parser   # topic   → angle
outline_chain = outline_prompt | llm | parser   # angle   → outline
intro_chain   = intro_prompt   | llm | parser   # outline → intro

The key detail: the output variable of one step must match the input variable of the next. angle_chain produces a string that gets fed into outline_prompt, which expects an {angle} variable.

Step 2 — Wire them together and accumulate state

The naive way to chain is angle_chain | outline_chain | intro_chain — but that throws away the intermediate results. You only get the final intro, not the angle or outline that produced it.

To keep every intermediate value, use RunnablePassthrough.assign():

from langchain_core.runnables import RunnablePassthrough
 
full_chain = (
    {"angle": angle_chain}                       # {topic} → {angle}
    | RunnablePassthrough.assign(outline=outline_chain)   # → {angle, outline}
    | RunnablePassthrough.assign(intro=intro_chain)       # → {angle, outline, intro}
)

RunnablePassthrough.assign(key=some_chain) runs some_chain and adds its result to the dict under key, without dropping the keys already there. This is how you "accumulate" outputs across a pipeline — the final result is a dict with all three values.

Step 3 — Run it

result = full_chain.invoke({"topic": "LangChain prompt chaining"})
result["angle"]    # → str
result["outline"]  # → str
result["intro"]    # → str

4. Streaming the final step

You don't have to wait for the whole chain. Run the early steps with .invoke(), then .stream() the last one token by token — ideal for user-facing apps where the intro appears live:

intermediate = ({"angle": angle_chain}
                | RunnablePassthrough.assign(outline=outline_chain)
               ).invoke({"topic": "..."})
 
for chunk in intro_chain.stream({"outline": intermediate["outline"]}):
    print(chunk, end="", flush=True)

5. Error handling between steps

Real pipelines fail — rate limits, malformed output, timeouts. LCEL gives you two clean hooks:

.with_retry() — retry a flaky step with exponential backoff:

robust_outline = outline_chain.with_retry(stop_after_attempt=3)

.with_fallbacks() — fall back to another runnable (e.g. a cheaper or different model) if the primary fails:

safe_intro = intro_chain.with_fallbacks([backup_intro_chain])

Because both return Runnables, you drop them straight back into the pipe — the rest of the chain doesn't change.

6. When to use this pattern

Good fit

  • Multi-stage content generation (research → outline → draft → edit)
  • Extract → transform → format pipelines
  • Any task where step N genuinely needs the output of step N-1

Reach for something else

  • Steps are independent → parallelization with RunnableParallel (lesson 02)
  • You need to pick one of several paths → routing with RunnableBranch (lesson 03)
  • The model needs to decide its own steps dynamically → agents (lesson 06)

7. Cheat sheet

ConceptWhat it does
| pipe operatorConnects runnables — output of left becomes input of right
StrOutputParserConverts AIMessage → plain str for the next step
RunnablePassthroughPasses the current input through unchanged
RunnablePassthrough.assign(key=chain)Runs a chain, adds its result as a new key
.stream()Yields output tokens one by one
.with_retry()Retries a step on failure with backoff
.with_fallbacks([...])Swaps in a backup runnable if the primary fails

▶️ Run it: open the runnable notebook on GitHub and execute top to bottom.