End-to-End Walkthrough

Every piece covered so far, wired into one runnable script — from raw files to a grounded, cited answer

Why

Every page so far has shown one piece of the pipeline in isolation — a loader snippet here, a splitter snippet there. This page exists to remove any doubt about how those pieces actually connect: one script, start to finish, that a naive-but-correct RAG system genuinely looks like. Everything past this page (advanced retrieval, search types, evaluation, production concerns) is an improvement layered on top of exactly this script, not a different architecture.

Explanation
Step 1-2: load and split the corpus (indexing, run once)
from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 1. Document Loader — see Document Loaders loader = DirectoryLoader("./knowledge_base", glob="**/*.txt", loader_cls=TextLoader) documents = loader.load() # documents: list[Document], each with .page_content and .metadata # 2. Text Splitter — see Text Splitters splitter = RecursiveCharacterTextSplitter( chunk_size=500, # tokens/characters per chunk, tune against an eval set chunk_overlap=50, # ~10% overlap, preserves context across cuts ) chunks = splitter.split_documents(documents) print(f"{len(documents)} documents -> {len(chunks)} chunks")
Step 3: embed and store (indexing, run once)
from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma # 3. Embeddings — see Embeddings embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 4. Vector Database — see Vector Databases vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db", # survives process restarts )

Everything above this line runs once (or on a schedule, per Production Concerns → incremental indexing) — it builds the index. Everything below runs on every single user question.

Step 4-5: retrieve and augment (live, per query)
from langchain_core.prompts import ChatPromptTemplate # 5. Retriever — see Retrieval Basics (start here; swap in MMR/hybrid/reranking later) retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) # 6. Augmentation — see Augmentation & Prompting SYSTEM_PROMPT = """Answer the question using ONLY the numbered context below. Cite every claim with its [N] reference. If the context does not contain the answer, say "I don't know" — do not guess.""" prompt = ChatPromptTemplate.from_messages([ ("system", SYSTEM_PROMPT), ("human", "Context:\n{context}\n\nQuestion: {question}"), ]) def format_context(retrieved_chunks): return "\n\n".join( f"[{i+1}] (source: {c.metadata.get('source', 'unknown')}) {c.page_content}" for i, c in enumerate(retrieved_chunks) )
Step 6: generate (live, per query)
from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) def answer(question: str): retrieved = retriever.invoke(question) # Retrieval context = format_context(retrieved) # Augmentation messages = prompt.format_messages(context=context, question=question) response = llm.invoke(messages) # Generation return response.content, retrieved result, sources = answer("How much paid leave do new hires get?") print(result) # "New hires get 15 days of paid leave per calendar year, prorated based # on their start date. [1]" for i, chunk in enumerate(sources, 1): print(f"[{i}] {chunk.metadata.get('source')} — {chunk.page_content[:80]}...")

This is the same flow traced with concrete numbers in RAG Pipeline Overview — this page just makes it copy-pasteable.

What changes on the way to production

Nothing about the shape of this script changes as the system matures — every improvement below replaces one line, not the architecture:

  • DirectoryLoader(..., loader_cls=TextLoader) becomes a mix of format-specific loaders once real PDFs, HTML, and database rows enter the corpus — see Document Loaders
  • chunk_size=500 stops being a guess and becomes a value chosen by testing candidates against a labeled eval set — see Text Splitters → picking chunk size and Evaluation
  • vectorstore.as_retriever(search_kwargs={"k": 5}) becomes search_type="mmr" once duplicate results are observed, or gets wrapped in an EnsembleRetriever once exact-keyword misses are observed — see Advanced Retrieval and Search Types
  • the retriever gets wrapped in a ContextualCompressionRetriever with a cross-encoder once retrieval precision is the bottleneck, not just recall — see Advanced Retrieval → Reranking
  • filter={} gets added to every vector search call once more than one tenant or permission level exists — see Production Concerns → access control
  • answer() gets wrapped in tracing (so every retrieved chunk and generated answer is logged) before it ever serves real traffic — see Evaluation → tooling

None of these changes require touching the loader→splitter→embed→store→retrieve→augment→generate shape above — they tune or replace individual stages within it, which is exactly the point of learning the pipeline stage by stage rather than as one opaque block.