LangChain Expression Language & Chains

Master LangChain Expression Language (LCEL), sequential, parallel, router, and custom chains with the pipe operator, RunnableParallel, and @chain.

Jun 4, 2026Updated Jul 10, 202630 min readFollow

Topics You Will Master

What LCEL is and how runnables chain together via the | pipe operator
Building a sequential chain: ChatPromptTemplate | ChatOllama | StrOutputParser
Inspecting the full AIMessage response object from a chain
Extending chains by composing one chain's output into another chain's input

LangChain Expression Language (LCEL) is the way we connect LangChain building blocks, called runnables, into a pipeline. In simple words, a runnable is any LangChain block that can be invoked. LCEL lets us snap these blocks together, and the output of one block automatically becomes the input of the next. The | pipe operator is the main syntax, and .pipe() is the same thing written as a method.

In this tutorial, we will learn every LCEL chain pattern: sequential, parallel, router, custom runnables, and the @chain decorator.

Prerequisites: LangChain, langchain-ollama, and python-dotenv installed. Ollama running locally with qwen3 pulled. See the Prompt Templates guide.

LangChain & Ollama - Local AI Development

Build production-ready LLM apps entirely on your own hardware. No API keys, no cloud costs.

Enroll on Udemy →

How Does LCEL Work?

LCEL chains work on a single principle: runnables pass output forward. The .invoke() return value of one runnable becomes the input of the next. If we hold on to this one sentence, every pattern in this lesson will make sense.

Let's see the chain types we will build, one by one:

  • Sequential Chain: steps run one after another in a linear pipeline
  • Parallel Chain: multiple sub-chains run at the same time, results returned as a dict
  • Router Chain: output of a first step decides which sub-chain handles the next step
  • Chaining Runnables: one chain's string output feeds into another chain's prompt variable
  • Custom Chain: our own Python functions wrapped as runnables with RunnableLambda or @chain

Diagram of an LCEL chain where the pipe operator connects a prompt to the LLM to an output parser in one declarative expression

The pipe operator chains prompt → LLM → parser into one declarative LCEL expression.


Setup

First, we load our environment variables, the same way we did in the previous lessons:

PYTHON
from dotenv import load_dotenv

load_dotenv('.env')
OUTPUT
True

On Linux/macOS: adjust the path if .env is in a parent directory: load_dotenv('./../.env')

Then, we import the template classes from the Prompt Templates lesson and connect to our local qwen3 model:

PYTHON
from langchain_ollama import ChatOllama
from langchain_core.prompts import (
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
    ChatPromptTemplate
)

base_url = "http://localhost:11434"
model = 'qwen3'

llm = ChatOllama(base_url=base_url, model=model)
llm
PYTHON
ChatOllama(model='qwen3', base_url='http://localhost:11434')

Here, we can see our llm object is ready and pointed at the local Ollama server. Everything in this lesson runs on this one model.


How Does a Sequential Chain Work?

What Problem Does the Pipe Solve?

Before we meet the | operator, we must see the problem it solves. Here is the manual two-step approach we used in the previous lesson: create a prompt, invoke it, then invoke the LLM with the result:

PYTHON
system = SystemMessagePromptTemplate.from_template(
    'You are {school} teacher. You answer in short sentences.'
)
question = HumanMessagePromptTemplate.from_template(
    'tell me about the {topics} in {points} points'
)

messages = [system, question]
template = ChatPromptTemplate(messages)

question = template.invoke({'school': 'primary', 'topics': 'solar system', 'points': 5})

response = llm.invoke(question)
print(response.content)
OUTPUT
1. The Sun is the center, holding the solar system together with gravity.
2. Eight planets orbit the Sun: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune.
3. The asteroid belt lies between Mars and Jupiter, with rocky objects.
4. Gas giants (Jupiter, Saturn, Uranus, Neptune) are large and mostly made of gas.
5. Distant regions like the Kuiper Belt and Oort Cloud contain icy bodies and comets.

Here, we can see the pattern: the template's output becomes the LLM's input, and we are the ones carrying it from one step to the next. Two invokes for two steps. With more steps, this carrying work grows, and this is exactly the job LCEL takes off our hands.

How Do We Build the Chain with |?

So, here comes the | pipe to the rescue. LCEL joins the two steps into one chain. We define the chain once, then call .invoke() directly on it with a dict of values:

PYTHON
system = SystemMessagePromptTemplate.from_template(
    'You are {school} teacher. You answer in short sentences.'
)
question = HumanMessagePromptTemplate.from_template(
    'tell me about the {topics} in {points} points'
)

messages = [system, question]
template = ChatPromptTemplate(messages)

chain = template | llm

Here, template | llm means: send the output of template into llm. The pipe is the carrier now, not us. Let's invoke the chain:

PYTHON
response = chain.invoke({'school': 'primary', 'topics': 'solar system', 'points': 5})
print(response.content)
OUTPUT
1. The Sun is the center, holding the solar system together with gravity.
2. Eight planets orbit the Sun: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune.
3. The asteroid belt lies between Mars and Jupiter, with rocky objects.
4. Moons orbit planets, like Earth's Moon and Jupiter's many moons.
5. The Kuiper Belt and Oort Cloud are regions of icy bodies beyond Neptune.

Same answer as the manual version, but now one .invoke() drives the whole pipeline. And because the variables live in a dict, we can switch the school value and get a different depth of explanation, with no code change:

PYTHON
response = chain.invoke({'school': 'phd', 'topics': 'solar system', 'points': 5})
print(response.content)
OUTPUT
1. The Sun is the central star, providing gravity and energy.
2. Eight planets orbit it: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune.
3. Smaller bodies include asteroids, comets, and dwarf planets like Pluto.
4. The solar system has distinct regions: inner rocky planets, outer gas giants, asteroid belt, and Kuiper Belt.
5. It formed from a collapsing cloud of gas and dust around 4.6 billion years ago.

What Does the Chain Return?

Now, what exactly does the chain return? Without a parser, chain.invoke() returns a full AIMessage object containing the content, the response_metadata, the token counts, and a run ID:

PLAINTEXT
response
PLAINTEXT
AIMessage(content='1. The Sun is the central star, providing gravity and energy.  \n2. Eight planets orbit it: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune.  \n3. Smaller bodies include asteroids, comets, and dwarf planets like Pluto.  \n4. The solar system has distinct regions: inner rocky planets, outer gas giants, asteroid belt, and Kuiper Belt.  \n5. It formed from a collapsing cloud of gas and dust around 4.6 billion years ago.', additional_kwargs={}, response_metadata={'model': 'qwen3', 'created_at': '2025-10-22T06:48:23.0623152Z', 'done': True, 'done_reason': 'stop', 'total_duration': 2236060800, 'load_duration': 69630100, 'prompt_eval_count': 37, 'prompt_eval_duration': 15459300, 'eval_count': 402, 'eval_duration': 2052522300, 'model_name': 'qwen3', 'model_provider': 'ollama'}, id='lc_run--d02d48ee-498c-4a32-949a-dceb964ae040-0', usage_metadata={'input_tokens': 37, 'output_tokens': 402, 'total_tokens': 439})

Here, we can see the full object: the answer text is buried inside content, surrounded by metadata. This is useful for debugging, but most of the time our program just wants the text. Digging .content out at every step gets tiring.

How Do We Get a Plain String?

So, we append StrOutputParser as the third block, and it extracts the plain text string from the AIMessage:

PYTHON
from langchain_core.output_parsers import StrOutputParser

chain = template | llm | StrOutputParser()
response = chain.invoke({'school': 'primary', 'topics': 'solar system', 'points': 5})
print(response)
OUTPUT
1. The Sun is the center, holding the solar system together with gravity.
2. Eight planets orbit the Sun: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune.
3. The asteroid belt lies between Mars and Jupiter, with rocky objects.
4. Moons orbit planets, like Earth's Moon and Jupiter's many moons.
5. Distant regions include the Kuiper Belt and Oort Cloud, home to icy bodies.

Notice that we now print response directly, with no .content. Let's evaluate response to confirm it is a plain Python string, not a message object:

PLAINTEXT
response
PLAINTEXT
'1. The Sun is the center, holding the solar system together with gravity.  \n2. Eight planets orbit the Sun: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune.  \n3. The asteroid belt lies between Mars and Jupiter, with rocky objects.  \n4. Moons orbit planets, like Earth\'s Moon and Jupiter\'s many moons.  \n5. Distant regions include the Kuiper Belt and Oort Cloud, home to icy bodies.'

And if we inspect chain itself, LangChain shows us the full pipeline with all three components in order:

PLAINTEXT
chain
PYTHON
ChatPromptTemplate(input_variables=['points', 'school', 'topics'], ...)
| ChatOllama(model='qwen3', base_url='http://localhost:11434')
| StrOutputParser()

Here, we can see our sequential chain exactly as we built it: the template fills the prompt, the LLM answers, and the parser cleans the answer into a string. This three-block pattern is the workhorse of LangChain, and we will reuse it in every pattern below.


How Do We Compose Two Chains?

Now, here is a question: the chain's output is a plain string, and a prompt template's input is a dict of variables. Can we feed one chain's output into another chain's prompt variable? Yes, and this is called composing chains. It lets us analyze or transform an LLM's output in a single .invoke() call.

Let's say we want a second chain that judges how difficult a given text is to understand:

PYTHON
analysis_prompt = ChatPromptTemplate.from_template('''analyze the following text: {response}
                                                   You need tell me that how difficult it is to understand.
                                                   Answer in one sentence only.
                                                   ''')

fact_check_chain = analysis_prompt | llm | StrOutputParser()
output = fact_check_chain.invoke({'response': response})
print(output)
OUTPUT
The text is easy to understand as it presents basic, clear information about the solar system using simple language and familiar concepts.

Here, we passed the previous response into the {response} variable by hand. It works, but we are the carrier again. To run both chains start to finish in one call, we join them using a dict that maps the first chain's output to the next prompt's variable:

PYTHON
composed_chain = {"response": chain} | analysis_prompt | llm | StrOutputParser()

output = composed_chain.invoke({'school': 'phd', 'topics': 'solar system', 'points': 5})
print(output)
OUTPUT
The text is relatively simple and accessible, requiring basic knowledge of astronomy concepts like planetary orbits, dwarf planets, and the solar system's structure.

Here, we can see the flow: our input dict goes into chain, which produces the solar system text. That text lands under the key "response", which is exactly the variable name analysis_prompt expects. The second LLM call then judges the first LLM's answer, and we get the judgment back, all from one .invoke().

Note

{"response": chain} is LCEL shorthand for a RunnableParallel with a single key. The value of "response" is populated by running chain with the same input dict, and the result is passed to analysis_prompt as {response}.


How Does a Parallel Chain Work?

Till now, our steps ran one after another. But sometimes we want two independent answers from the same input, and there is no reason to wait for one before starting the other. This is the job of the parallel chain: multiple sub-chains run at the same time on the same input, and the results come back together as a dict. We use RunnableParallel to define the parallel structure.

Diagram of RunnableParallel running two chains concurrently on the same input and merging their outputs into one dictionary

RunnableParallel runs both chains concurrently; their results arrive together as one dict.

Building Two Sub-Chains

First, we build a fact chain, the same pattern we already know:

PYTHON
system = SystemMessagePromptTemplate.from_template(
    'You are {school} teacher. You answer in short sentences.'
)
question = HumanMessagePromptTemplate.from_template(
    'tell me about the {topics} in {points} points'
)

messages = [system, question]
template = ChatPromptTemplate(messages)
fact_chain = template | llm | StrOutputParser()

output = fact_chain.invoke({'school': 'primary', 'topics': 'solar system', 'points': 2})
print(output)
OUTPUT
1. The solar system has the Sun at its center, with eight planets orbiting around it.
2. It includes moons, asteroids, comets, and other celestial objects in space.

Then, we build a second chain that writes a poem instead. Notice that only the human template changes; the system template is reused:

PYTHON
question = HumanMessagePromptTemplate.from_template(
    'write a poem on {topics} in {sentences} lines'
)

messages = [system, question]
template = ChatPromptTemplate(messages)
poem_chain = template | llm | StrOutputParser()

output = poem_chain.invoke({'school': 'primary', 'topics': 'solar system', 'sentences': 2})
print(output)
OUTPUT
The sun shines bright, a golden sphere,
Planets dance in silent cheer.

Both chains work on their own. Now, let's run them together.

Running Both in Parallel

Now, we create one chain out of two with RunnableParallel. The keyword names fact and poem will become the keys of the output dict:

PYTHON
from langchain_core.runnables import RunnableParallel

chain = RunnableParallel(fact=fact_chain, poem=poem_chain)

We call .invoke() once, with a dict containing all the variables that both sub-chains need:

PYTHON
output = chain.invoke({'school': 'primary', 'topics': 'solar system', 'points': 2, 'sentences': 2})
print(output['fact'])
print('\n\n')
print(output['poem'])
OUTPUT
1. The solar system has the Sun at its center, with eight planets orbiting around it.
2. It includes moons, asteroids, comets, and dwarf planets like Pluto, all held by gravity.

The sun, a fiery heart, spins bright and bold,
planets dance in orbits, each a world of gold.

Here, we can see both results arriving together: the facts under output['fact'] and the poem under output['poem']. Each sub-chain picked the variables it needed from the shared input dict, and neither waited for the other.

Tip

RunnableParallel runs both chains concurrently in separate threads. For local LLM calls this reduces total wall-clock time compared to running them sequentially.


How Does a Router Chain Work?

Now, let's learn the pattern that makes decisions. A router chain classifies the input first, then sends it to a different sub-chain based on the result. In simple words, the router is a fork in the road, and the model's own answer decides which way we go. The decision happens while the program runs.

The best way to learn this is by building a real example: a review reply system. A positive review should get a thank-you reply, and a negative review should get an apology. Let's build it in steps.

Diagram of a router chain where a classifier runs first and a RunnableLambda routes the input to the correct reply chain

The classifier runs first; a RunnableLambda then routes the input to the correct reply chain.

Step 1, Sentiment Classifier Chain

First, we need a chain that reads a review and answers with one word, Positive or Negative. Notice how strict the prompt is: we ask for a single word, because this output will drive a Python if condition, and an if cannot handle a chatty answer.

PYTHON
prompt = """Given the user review below, classify it as either being about `Positive` or `Negative`.
            Do not respond with more than one word.

            Review: {review}
            Classification:"""

template = ChatPromptTemplate.from_template(prompt)

chain = template | llm | StrOutputParser()

review = "Thank you so much for providing such a great plateform for learning. I am really happy with the service."
chain.invoke({'review': review})
OUTPUT
'Positive'

It works. One word, exactly as instructed.

Step 2, Sub-Chains for Each Route

Next, we build the two destination chains, one for each outcome. The positive chain encourages the user to share their experience:

PYTHON
positive_prompt = """
                You are expert in writing reply for positive reviews.
                You need to encourage the user to share their experience on social media.
                Review: {review}
                Answer:"""

positive_template = ChatPromptTemplate.from_template(positive_prompt)
positive_chain = positive_template | llm | StrOutputParser()

And the negative chain apologizes first, then points the user to a support email:

PYTHON
negative_prompt = """
                You are expert in writing reply for negative reviews.
                You need first to apologize for the inconvenience caused to the user.
                You need to encourage the user to share their concern on following Email:'udemy@kgptalkie.com'.
                Review: {review}
                Answer:"""

negative_template = ChatPromptTemplate.from_template(negative_prompt)
negative_chain = negative_template | llm | StrOutputParser()

Step 3, Router Function

Now, the fork itself. The router is a plain Python function that reads the classifier's answer and returns the matching chain:

PYTHON
def rout(info):
    if 'positive' in info['sentiment'].lower():
        return positive_chain
    else:
        return negative_chain

Here, notice two small but important choices. We use .lower() because the model might answer Positive or positive, and we use in instead of == because the model might add a stray space or period. Both choices keep the routing safe from tiny changes in the model's wording.

Step 4, Assembling the Full Router Chain

Now, we put the pieces together. The dict will run two things on our input: the classifier chain fills the sentiment key, and a small lambda passes the original review text through unchanged. Why do we need the lambda? Because the reply chains need the review text too. Without it, only the sentiment would survive to the next step. RunnableLambda(rout) wraps our router function so it can sit inside the pipeline:

PYTHON
from langchain_core.runnables import RunnableLambda

full_chain = {"sentiment": chain, 'review': lambda x: x['review']} | RunnableLambda(rout)

Let's inspect full_chain to see the complete pipeline structure:

PYTHON
full_chain
OUTPUT
{
  sentiment: ChatPromptTemplate(...)
             | ChatOllama(model='qwen3', base_url='http://localhost:11434')
             | StrOutputParser(),
  review: RunnableLambda(lambda x: x['review'])
}
| RunnableLambda(rout)

Step 5, Invoking with a Negative Review

Now, let's test the fork with a negative review and watch the router pick the right path:

PYTHON
review = "I am not happy with the service. It is not good."

output = full_chain.invoke({'review': review})
print(output)
OUTPUT
We sincerely apologize for the inconvenience caused and understand your dissatisfaction. We value your feedback and kindly ask you to share your concern via email at udemy@kgptalkie.com so that we can address your issue promptly. We are committed to resolving this matter to your satisfaction and appreciate your patience as we work to improve our services. Thank you for bringing this to our attention.

Here, we can see the whole journey: the classifier read the review and said Negative, the router returned negative_chain, and that chain wrote the apology with the support email. We never told the code which path to take. The input itself decided.

Note

Change review to a positive string and the router automatically dispatches to positive_chain instead, with no code change needed.


What Are RunnableLambda and RunnablePassthrough?

We have already met RunnableLambda in the router. Now let's understand the two helpers properly. RunnableLambda wraps any Python function as a runnable step, so our own logic can sit inside a chain. RunnablePassthrough passes its input through unchanged. We use it when we want to keep the original value next to the new ones we computed.

Helper Functions and Prompt

Let's say that after the LLM answers, we also want to know how long the answer is. We write two tiny functions for that:

PYTHON
from langchain_core.runnables import RunnablePassthrough, RunnableLambda

def char_counts(text):
    return len(text)

def word_counts(text):
    return len(text.split())

prompt = ChatPromptTemplate.from_template("Explain these inputs in 5 sentences: {input1} and {input2}")

Here, char_counts and word_counts are ordinary Python, nothing LangChain about them yet. Let's inspect the prompt to check the variables it detected:

PLAINTEXT
prompt
PYTHON
ChatPromptTemplate(input_variables=['input1', 'input2'], input_types={}, partial_variables={}, messages=[HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['input1', 'input2'], ..., template='Explain these inputs in 5 sentences: {input1} and {input2}'), additional_kwargs={})])

Plain Chain Output

First, the plain chain, so we have a baseline:

PYTHON
chain = prompt | llm | StrOutputParser()

output = chain.invoke({'input1': 'Earth is planet', 'input2': 'Sun is star'})

print(output)
OUTPUT
1. Earth is a planet, meaning it is a celestial body that orbits the Sun, has a solid surface, and meets specific criteria like clearing its orbit of other debris.
2. The Sun is a star, a massive, luminous sphere of plasma held together by gravity, where nuclear fusion powers its light and heat.
3. Planets, like Earth, do not produce their own light but reflect sunlight, while stars, like the Sun, generate energy through nuclear reactions.
4. Earth's classification as a planet distinguishes it from stars, which are much larger and emit light due to internal heat and fusion.
5. The Sun's role as a star is central to sustaining life on Earth, as its energy drives weather, climate, and the planet's orbital motion.

Enriched Output with Counts and Passthrough

Now, we extend the chain. After StrOutputParser returns the text, three steps run on that same text in parallel: char_counts, word_counts, and RunnablePassthrough, which passes the raw string through under the key output:

PYTHON
chain = prompt | llm | StrOutputParser() | {
    'char_counts': RunnableLambda(char_counts),
    'word_counts': RunnableLambda(word_counts),
    'output': RunnablePassthrough()
}

output = chain.invoke({'input1': 'Earth is planet', 'input2': 'Sun is star'})

print(output)
JSON
{
  "char_counts": 719,
  "word_counts": 122,
  "output": "1. Earth is a planet, meaning it is a celestial body that orbits the Sun and is characterized by its solid surface, atmosphere, and ability to support life.  \n2. The Sun is a star, a massive, luminous sphere of plasma held together by gravity, which generates energy through nuclear fusion in its core.  \n3. Planets like Earth are much smaller and cooler than stars, and they do not produce their own light but reflect sunlight.  \n4. The Sun's gravitational pull keeps Earth and other planets in orbit, forming the solar system.  \n5. While Earth is a planet, the Sun's status as a star highlights the fundamental difference between these two types of celestial objects in terms of size, composition, and energy sources."
}

Here, we can see the result: the character count, the word count, and the untouched answer, all in one dict. We need RunnablePassthrough for the last key. Without it, the counts would survive but the answer itself would be lost. Passthrough keeps the original text flowing along with the values we computed from it.

Tip

RunnablePassthrough is the cleanest way to forward a value through a pipeline step without transformation. It avoids the need for a no-op lambda.


How Does the @chain Decorator Work?

Finally, let's learn the most flexible pattern of all. The @chain decorator from langchain_core.runnables turns any Python function into a full LCEL runnable. In simple words, we write ordinary Python inside the function, and outside, it behaves like every other chain: it can be used with |, .invoke(), .stream(), and .batch().

Diagram showing the @chain decorator wrapping Python logic as a first-class LCEL runnable supporting invoke, stream, and batch

@chain wraps your Python logic as a first-class LCEL runnable with invoke/stream/batch support.

We will rebuild the fact-plus-poem result from the parallel section, but this time with plain Python inside a decorated function. We call fact_chain.invoke() and poem_chain.invoke() ourselves and shape the dict by hand. We choose this over RunnableParallel when we want freedom, because inside this function we can do anything Python can do: add if conditions, loops, logging, retries. The trade-off is in the note below. Let's see the code as below:

PYTHON
from langchain_core.runnables import chain

@chain
def custom_chain(params):
    return {
        'fact': fact_chain.invoke(params),
        'poem': poem_chain.invoke(params),
    }

params = {'school': 'primary', 'topics': 'solar system', 'points': 2, 'sentences': 2}
output = custom_chain.invoke(params)
print(output['fact'])
print('\n\n')
print(output['poem'])
OUTPUT
1. The solar system has the Sun at its center.
2. Eight planets orbit the Sun in elliptical paths.

The sun reigns bright, a golden core,
Planets dance in orbits, vast and wide.

Here, we can see the same fact-plus-poem result as the parallel section, built this time by our own Python function.

Note

Unlike RunnableParallel, the @chain decorator runs sub-chains sequentially inside the function. Use RunnableParallel when true concurrency matters.

On Linux/macOS: all code above runs identically. No OS-specific differences.


Quick Reference

Let me tabulate all the patterns we learned for your better understanding, so that you can pick the right one for your use case.

LCEL Chain Patterns

Pattern Syntax Use when
Sequential a | b | c Steps run in order, each output feeds next
With parser template | llm | StrOutputParser() Want a plain string instead of AIMessage
Composed {"key": chain} | next_prompt | llm First chain's output becomes a variable in next prompt
Parallel RunnableParallel(a=chain1, b=chain2) Run multiple chains concurrently
Router {...} | RunnableLambda(router_fn) Dispatch to different chains based on runtime value
Custom @chain decorator Wrap arbitrary Python logic as a runnable

Key Imports

PYTHON
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import (
    RunnableParallel,
    RunnableLambda,
    RunnablePassthrough,
    chain
)

What You Built

In this lesson, we moved from carrying data between template.invoke() and llm.invoke() by hand to building full LCEL pipelines.

We now have five patterns in our toolkit:

  • Sequential: the workhorse, template | llm | StrOutputParser()
  • Composed: feeding one chain's string output as a variable into the next chain's prompt
  • Parallel: running a fact chain and a poem chain at the same time with RunnableParallel, getting both results in one dict
  • Router: classifying a review as positive or negative first, then sending it to the matching reply chain
  • Custom: wrapping our own Python logic as a normal LCEL runnable with RunnableLambda or the @chain decorator

Every pattern looks the same from the outside: one .invoke() call. The pipeline handles all the plumbing inside, and that is the promise of LCEL. This is how chains work. In the coming lessons, every agent and every RAG system we build will stand on this foundation.

Found this useful? Keep building with me.

New tutorials every week on YouTube: or go deeper with a full structured course.

Find this tutorial useful?

Subscribe to our YouTube channels for more practical production walk-throughs.

Discussion & Comments