Building a RAG Chatbot with LangChain and FastAPI in Python

By Asadullah Khan · AI & Voice AI Engineer, Karachi, Pakistan

28 September 2026 · 9 min read

Retrieval-Augmented Generation (RAG) is the most practical way to make a large language model answer questions about your own data. Instead of fine-tuning, you retrieve the most relevant passages at query time and hand them to the model as context. This guide walks through a production-shaped RAG chatbot in Python using LangChain, a vector store and FastAPI.

Why RAG instead of fine-tuning

Fine-tuning changes how a model behaves; RAG changes what it knows. For most business use cases — support docs, internal wikis, product catalogs — RAG is faster to ship, cheaper to update and far easier to audit because every answer can be traced back to a source document.

  • Updates are instant: re-index a document instead of retraining a model.
  • Grounded answers: you can cite the exact chunk the model used.
  • Lower cost: no GPU training runs and no custom model hosting.
  • Better privacy: your data stays in your own vector store.

The four-stage architecture

  1. Ingest: load documents and split them into overlapping chunks.
  2. Embed: turn each chunk into a vector with an embedding model.
  3. Retrieve: at query time, find the closest chunks to the question.
  4. Generate: pass the question and retrieved chunks to the LLM and stream the answer.

Step 1 — Load and split your documents

Start by loading your source files and splitting them into chunks. Overlap matters: it prevents an answer from being cut in half at a chunk boundary.

from langchain_community.document_loaders import DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

loader = DirectoryLoader("./docs", glob="**/*.md")
documents = loader.load()

splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=120,
    separators=["\n\n", "\n", ". ", " "],
)
chunks = splitter.split_documents(documents)
print(f"Loaded {len(documents)} docs into {len(chunks)} chunks")

Step 2 — Embed and store in a vector database

Each chunk becomes a vector that captures its meaning. Store those vectors in a database that supports similarity search — pgvector, Qdrant, Chroma or Pinecone all work well.

from langchain_openai import OpenAIEmbeddings
from langchain_postgres import PGVector

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

vector_store = PGVector.from_documents(
    documents=chunks,
    embedding=embeddings,
    collection_name="handbook",
    connection="postgresql+psycopg://user:pass@localhost:5432/app",
)

Step 3 — Build the retrieval chain

The retriever fetches the top-k most similar chunks, and a prompt template instructs the model to answer only from that context. The instruction to say "I do not know" when the context is insufficient is what keeps a RAG system honest.

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI

retriever = vector_store.as_retriever(search_kwargs={"k": 4})
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

prompt = ChatPromptTemplate.from_messages([
    ("system",
     "Answer using only the context below. "
     "If the answer is not in the context, say you do not know.\n\n{context}"),
    ("human", "{question}"),
])

chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
)

Step 4 — Serve it with FastAPI

A thin FastAPI layer turns the chain into an HTTP service. Note the dependency injection and the explicit request model — both make the endpoint easy to test and document via the automatic OpenAPI schema.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title="RAG Chatbot API")

class Query(BaseModel):
    question: str = Field(min_length=1, max_length=1000)

class Answer(BaseModel):
    answer: str

@app.post("/chat", response_model=Answer)
async def chat(query: Query) -> Answer:
    try:
        result = await chain.ainvoke(query.question)
    except Exception as exc:
        raise HTTPException(status_code=502, detail="Upstream model error") from exc
    return Answer(answer=result.content)

Production tips that matter

  • Stream tokens to the client so users see progress immediately.
  • Log the retrieved chunk IDs with each answer for debugging and citations.
  • Cache embeddings so re-indexing only reprocesses changed documents.
  • Evaluate retrieval separately from generation — most bad answers are retrieval failures.
  • Add rate limiting and timeouts; LLM calls fail in ways a normal API does not.

Most RAG quality problems are retrieval problems. Fix the search before you blame the model.

Once the retrieval layer is reliable, RAG stops feeling like a demo and starts behaving like infrastructure. That is the point where it becomes genuinely useful to a business.