Welcome back. In our previous lessons, you successfully constructed the data foundation for our application. You've loaded documents, split them, generated embeddings, and, most recently, set up a persistent FAISS vector store with a Retriever interface. We now have all the necessary components in place.
This lesson is the culmination of our work in this module. We will assemble these individual components—the retriever, a prompt template, and a language model—into a complete, end-to-end Retrieval-Augmented Generation (RAG) chain using the LangChain Expression Language (LCEL). You will learn how to orchestrate the flow of data, from the user's initial question to the final, context-aware answer generated by the LLM. This is where the power of LCEL for composing complex logic truly becomes apparent.
The RAG Chain Architecture
The RAG process involves two primary stages: retrieval and generation. In LCEL, we can represent this as a pipeline. The key challenge is that our prompt template requires two pieces of information: the retrieved context and the original question. However, our chain starts with only one input: the user's question.
To solve this, we will use RunnableParallel. This LCEL component allows us to create a "fork" in our data pipeline.
- One path will take the user's question, pass it to our retriever, and format the resulting documents into a single string for the
context. - The other path will simply pass the user's question through unchanged to be used as the
question.
RunnableParallel will then combine the outputs of these two paths into a dictionary, which perfectly matches the input schema of our prompt template. The following diagram illustrates the flow we are about to build.

This flowchart shows the LCEL pipeline for our RAG chain. The user_input is fed into a RunnableParallel block. One branch uses the retriever to fetch and format documents, while the other uses RunnablePassthrough to carry the original question forward. The combined output populates the qa_prompt, which is then passed to the LLM to generate the final answer.
Assembling the RAG Chain
Let's get straight to the implementation. We will be working in our src/lcel_quickstart/chain.py file, bringing together the get_retriever function from the last lesson with the core LCEL components.
The article "LangChain: Jump into LCEL and Runnables" by Jarek Grygolec provides an excellent, concise example of building this exact chain. It clearly demonstrates how to use RunnableParallel to structure the inputs for the prompt.
LangChain: Jump into LCEL and Runnables - Level Up Coding
This article provides a practical demonstration of building a RAG chain with LCEL. It shows the complete workflow, from setting up the retriever to composing the final chain.
Focus on the final code block in the article, under the heading Taking It All Together. Pay close attention to how RunnableParallel is used to construct a dictionary with context and question keys. The context is generated by a sub-chain (retriever | RunnableLambda), and the question is passed through using RunnablePassthrough.
Now, let's implement this in our chain.py file.
First, ensure you have all the necessary imports at the top of the file. We'll need ChatPromptTemplate, ChatOpenAI, StrOutputParser, and the runnables for composing the chain.
# src/lcel_quickstart/chain.py (add imports)
import os
from operator import itemgetter # Used for more complex chains, good to have
from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda, RunnableParallel, RunnablePassthrough
from langchain_core.vectorstores import VectorStoreRetriever
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from .data_pipeline import load_vector_store, VECTOR_STORE_PATH
Next, we'll define a helper function to format the retrieved Document objects into a single string. This is the function that will follow the retriever in our chain.
# src/lcel_quickstart/chain.py (add helper function)
def format_docs(docs: list) -> str:
"""Converts a list of documents into a single string."""
return "\n\n".join(doc.page_content for doc in docs)
Now, we can define the function that builds our complete RAG chain. This function will instantiate the prompt, model, and parser, and then pipe them together with the retriever using RunnableParallel.
The video "LangChain Expression Language" from Coding Crash Courses provides a great visual breakdown of how this composition works.
LangChain Expression Language - The ONLY video you need to TRULY understand LCEL
This video walks through a real-world RAG chain example, explaining how the different runnables (RunnableParallel, RunnablePassthrough) work together to pipe data from the retriever into the prompt.
Watch the final section on the real-world example. The presenter clearly explains how a single string input is branched using RunnableParallel to create the context and question dictionary required by the prompt template. This is the exact logic we are implementing.
Here is the complete code for our get_rag_chain function. Add this to chain.py.
# src/lcel_quickstart/chain.py (main RAG chain function)
# ... (imports and format_docs function) ...
def get_retriever() -> VectorStoreRetriever:
# ... (from previous lesson, no changes needed) ...
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
if not os.path.exists(VECTOR_STORE_PATH):
raise FileNotFoundError(
"FAISS index not found. Please run data_pipeline.py to create it."
)
vector_store = load_vector_store(embedding_model)
return vector_store.as_retriever(search_kwargs={"k": 3})
def get_rag_chain():
"""
Builds and returns a complete RAG chain.
"""
vectorstore = get_retriever()
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI(model="gpt-3.5-turbo")
# This is the core of the RAG chain
chain = (
RunnableParallel({
"context": vectorstore | format_docs,
"question": RunnablePassthrough()
})
| prompt
| model
| StrOutputParser()
)
return chain
if __name__ == '__main__':
# Test the full RAG chain
rag_chain = get_rag_chain()
query = "What is LCEL?"
print("--- RAG Chain Demonstration ---")
print(f"Query: '{query}'\n")
answer = rag_chain.invoke(query)
print("Answer:")
print(answer)
Notice the structure within RunnableParallel:
- The
contextkey is assigned a sub-chain:vectorstore | format_docs. When the chain is invoked with a question, that question is passed to thevectorstore(our retriever), which returns documents. These documents are then passed toformat_docs. - The
questionkey is assignedRunnablePassthrough(). This takes the initial input to the chain (the user's question) and passes it through unchanged. - The output of this parallel step is a dictionary like
{'context': '...', 'question': '...'}. This is then piped directly into ourprompt.
Running the Chain
You are now ready to test the complete pipeline. Make sure your faiss_index directory exists from the previous lesson. If not, run uv run src/lcel_quickstart/data_pipeline.py one more time.
Then, execute the main chain.py script:
uv run src/lcel_quickstart/chain.py
The script will load the vector store, build the RAG chain, and invoke it with the query "What is LCEL?". The output will be a concise answer generated by the LLM, based only on the relevant chunks retrieved from the blog post document we indexed earlier.
Conclusion
In this lesson, you have successfully assembled a complete RAG chain, the core of our question-answering application. You have bridged the gap between retrieval and generation using the powerful composition patterns of LCEL.
Here are the key takeaways:
- RAG Chain Structure: The canonical RAG chain retrieves context and combines it with the original question in a prompt to generate an answer.
RunnableParallel: This is the key LCEL primitive for structuring input for subsequent steps. We used it to create a dictionary containing both the retrievedcontextand the originalquestion.RunnablePassthrough: This simple but essential runnable allows you to carry over the initial input of a chain to be used later, which was crucial for populating thequestionfield in our prompt.
You now have a functioning RAG pipeline. However, a good RAG system should not only provide an answer but also cite its sources. In the next lesson, we will modify this chain to include the source of the retrieved documents in the final output, making our application more transparent and trustworthy.