Learn LangChain - Retrieval & Basic RAG
Episode 11 of 23

Learn LangChain - Retrieval & Basic RAG

Assembling the first RAG pipeline: VectorStoreRetriever and similarity_search with top-k and metadata filtering, the retrieve-prompt-generate combination using LCEL, and how to evaluate answer quality.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

Our long journey has finally reached the point we've been aiming at since episode 9: RAG. You already have chunks (episode 9) and a vector index (episode 10). Now we combine both into a system that can answer questions based on your own documents, not just the model's built-in knowledge.

This episode builds a basic RAG pipeline end-to-end. We start with the retriever: VectorStoreRetriever and similarity_search with top-k settings and metadata filtering. Then the RAG pipeline: the retrieve → prompt → generate flow assembled with LCEL. Finally, we discuss evaluation — how to judge whether the generated answers are truly high quality.

VectorStoreRetriever

The bridge between a vector store and the pipeline is the retriever. vectorstore.as_retriever() wraps the vector store so you can fetch relevant documents with a consistent interface — and this retriever is itself a Runnable, so it can be plugged straight into an LCEL chain.

PythonCreating a retriever from a vector store
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
dokumen_relevan = retriever.invoke("Apa itu RAG?")
 
for doc in dokumen_relevan:
    print(doc.page_content[:80])

search_kwargs={"k": 3} requests the three most relevant documents per search. Because the retriever is a Runnable, an invoke call returns a list of Documents — ready to be consumed by the next component.

similarity_search and Top-k

Behind the retriever is similarity search. similarity_search is the direct way to query the vector store without going through the retriever interface — useful when you need more control, for example fetching raw documents along with their similarity scores.

Pythonsimilarity_search with scores
hasil = vectorstore.similarity_search_with_score(
    "Bagaimana cara menyimpan vektor di Postgres?",
    k=3,
)
for doc, skor in hasil:
    print(round(skor, 4), doc.page_content[:60])

Top-k determines how many documents are returned. A small value (2-4) produces focused context but can miss information; a large value gives more complete context but can pull the model off track. A value of 3-5 is generally a balanced starting point.

Info

The score returned by similarity_search_with_score is a distance, not a similarity — the smaller it is, the more relevant. Its exact interpretation depends on the metric your vector store uses.

Metadata Filtering

The metadata from episode 9 pays off here: with filters, you restrict the search to a subset of documents — for example a specific source, date, or category — so results are more relevant and cheaper.

PythonMetadata filtering during retrieval
dokumen = vectorstore.similarity_search(
    "Strategi investasi jangka panjang",
    k=3,
    filter={"sumber": "laporan-2026.pdf"},
)

The filter syntax can differ between vector stores — Chroma uses a dict like the one above, while Qdrant uses more explicit conditions. The key: make sure metadata is written consistently during add_documents in episode 10, because no filter is useful if the metadata isn't structured.

RAG Pipeline with LCEL

Now let's assemble everything. The classic RAG pipeline consists of four steps: retrieve context, format the context into text, inject it into the prompt, and generate the answer. With LCEL the whole flow is just a few lines.

PythonComplete RAG pipeline
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI
 
prompt = ChatPromptTemplate.from_messages([
    ("system", "Jawab berdasarkan konteks di bawah. Jika konteks tidak cukup, katakan tidak tahu. Konteks: {context}"),
    ("human", "{pertanyaan}"),
])
model = ChatOpenAI(model="gpt-4o-mini")
 
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)
 
rag_chain = (
    {
        "context": retriever | format_docs,
        "pertanyaan": RunnablePassthrough(),
    }
    | prompt
    | model
    | StrOutputParser()
)
 
jawaban = rag_chain.invoke("Apa itu RAG dan bagaimana cara kerjanya?")
print(jawaban)

This flow deserves a careful read. retriever | format_docs fetches documents then combines their contents into a single context block. RunnablePassthrough passes the original question through. Both are assembled into a dict filled into the prompt, then processed by the model and parser. This way, the format_docs function and the retriever's results can be tested separately before being combined.

Evaluating Answer Quality

RAG isn't a one-shot deal — quality has to be measured. Basic evaluation checks two things: whether the retrieved context is relevant, and whether the generated answer is correct and faithful to the context.

PythonChecking the relevance of retrieved context
pertanyaan = "Apa peran checkpointer di LangGraph?"
konteks = retriever.invoke(pertanyaan)
 
for doc in konteks:
    print(doc.metadata.get("sumber"), doc.page_content[:70])

Start here: when an answer feels wrong, check the context first. If the context isn't relevant, the problem is in chunking (episode 9) or the embedder (episode 10), not the prompt. If the context is relevant but the answer strays, fix the system prompt. Also train a set of test questions and compare RAG answers against expected answers — regular manual comparison is far more valuable than no evaluation at all. You'll go deeper into more systematic evaluation frameworks — datasets, automatic evaluators, and regression testing — through LangSmith in episode 19.

Success

The golden rule of RAG debugging: trace backwards. Wrong answer → check the context. Wrong context → check chunks and embeddings. The prompt is the last component to blame, not the first.

Conclusion

Your basic RAG pipeline now works end-to-end: the retriever fetches relevant documents with top-k and metadata filtering, LCEL assembles retrieve → prompt → generate into a single chain, and you have an evaluation method to maintain quality. From here, the development possibilities only move forward to more advanced techniques.

Key takeaways:

  • as_retriever() wraps a vector store into a Runnable that plugs directly into an LCEL chain.
  • search_kwargs={"k": N} sets top-k; similarity_search_with_score gives distance scores to judge relevance.
  • Metadata filtering restricts the search to a document subset and improves relevance.
  • RAG pipeline = retrieve → format context → prompt → generate, assembled with RunnablePassthrough.
  • Debug from the back: wrong answer → check context, wrong context → check chunking and embeddings.
  • Regular evaluation with test questions maintains RAG quality as data and prompts change.

In episode 12 we move on to sharper techniques: Advanced RAG — multi-query retrieval, reranking, hybrid search, and contextual retrieval for answering far harder questions. See you there!

Learn LangChain - Retrieval & Basic RAG | Learn LangChain