Build a RAG Chat Assistant

Build a chat assistant with RAG pipeline in this hands-on Applied AI engineering tutorial. Learn to combine retrieval with LLM generation, troubleshoot edge cases, and get ready for the next lesson.

Focus: build a chat assistant with rag pipeline

Sponsored

We've all seen chat assistants that sound confident but hallucinate answers, or that can't answer questions about your own documents. Building a chat assistant with a RAG pipeline is the industry-standard way to fix that: you ground every answer in your own data, so responses are accurate, up-to-date, and traceable. In this lesson, you'll learn how to build a chat assistant with a RAG pipeline from scratch, using Python 3.10+ and practical, hands-on steps you can apply immediately.

The problem this lesson solves

Without retrieval, your chat assistant is just a general-purpose language model. It has no access to your private documents, your latest product specs, or your internal knowledge base. The result: it makes things up, it gives stale answers, and it can't cite its sources.

Consider a support bot for a SaaS product. The model may know the internet's version of your API, but it won't know that you renamed a parameter last Tuesday. Users get frustrated, trust erodes, and your team ends up rewriting answers by hand.

RAG solves this by splitting the problem: first retrieve relevant chunks from your data, then generate an answer grounded in those chunks. This is the core of building a chat assistant with a RAG pipeline — and it's why every enterprise AI team uses it.

Here's what you'll achieve by the end of this lesson:

  • Explain the core idea behind building a chat assistant with RAG pipeline
  • Complete a practical exercise that builds a working RAG assistant
  • Connect this to the next step in your Applied AI engineering track

Core concept / mental model

Think of RAG like a reference desk at a library. You don't ask the librarian (the LLM) to recite every book from memory — that would be unreliable. Instead, you tell the librarian your question, they walk to the shelves (your vector database), pull the most relevant books (retrieved chunks), and then hand you a synthesized summary with page numbers (citations).

In technical terms, RAG is Retrieval-Augmented Generation. The pipeline has four parts:

  1. Ingestion: you split your documents into chunks and embed them into vectors.
  2. Storage: you store those vectors in a vector database (e.g., Chroma, FAISS, or Pinecone).
  3. Retrieval: given a user query, you embed it and find the most similar chunks via similarity search.
  4. Generation: you pass the retrieved chunks as context to an LLM, which answers the query based solely on that context.

Definitions you'll need

  • Chunk: a small slice of text (200–1000 characters) that's self-contained enough to answer a question.
  • Embedding: a numeric vector that captures the meaning of a text; similar texts have similar vectors.
  • Top-k: the number of chunks you retrieve for each query.
  • Context window: how much text the LLM can see in one prompt.

Why RAG beats fine-tuning for this use case

Fine-tuning changes the model's weights — expensive, slow, and it still can't guarantee accuracy on private data. RAG keeps the model frozen and swaps out the context. This means:

  • Fresh data: update your vector store without retraining.
  • Traceability: you can show the exact source chunks.
  • Cost: no GPU training needed, just API calls.

How it works step by step

Building a chat assistant with a RAG pipeline follows a repeatable sequence. Let's walk through each stage so you understand the why before you write the code.

Step 1: Prepare your documents

Gather all the text you want the assistant to know about: manuals, FAQs, Slack exports, academic papers. The quality of your chunks decides the quality of your answers, so start by cleaning the data — remove boilerplate, fix encoding, and split long files into coherent sections.

Step 2: Chunk the text

You can't embed an entire 200-page PDF in one vector — it would be impossible to retrieve a specific answer. Instead, split your text into chunks with a smart strategy:

  • Fixed-size: split every N characters with overlap. Simple, but may cut sentences in half.
  • Recursive character: split on paragraph or sentence boundaries. Better for prose.
  • Semantic: use the model to identify natural breaks. Best, but more API calls.

A good chunk size is 300–800 characters with an overlap of 10–20%. This keeps context coherent while still allowing precise retrieval.

Step 3: Embed and store

Embed each chunk using a model like text-embedding-ada-002 (OpenAI) or all-MiniLM-L6-v2 (open-source). Store the vectors in a vector database. Here's the flow:

from sentence_transformers import SentenceTransformer
import chromadb

# Load a free, local embedding model
model = SentenceTransformer('all-MiniLM-L6-v2')

# Create a Chroma client and collection
client = chromadb.PersistentClient(path="./my_db")
collection = client.get_or_create_collection(name="docs")

chunks = [
    "RAG stands for Retrieval-Augmented Generation. It combines retrieval with LLM generation.",
    "Chunking is the process of splitting documents into smaller pieces for embedding."
]
embeddings = model.encode(chunks).tolist()

# Add to the collection
collection.add(
    ids=["0", "1"],
    documents=chunks,
    embeddings=embeddings
)

Pro tip: You don't need to store embeddings manually if you use Chroma's built-in embedding function. But showing the explicit encoding helps you understand what's happening under the hood.

Step 4: Retrieve relevant chunks

When a user asks a question, you embed the query and search for the closest chunks. Most vector databases do this with cosine similarity or Euclidean distance. Choose a top_k that fits your context window — usually 3–5 chunks.

Step 5: Ground the LLM answer

You construct a prompt that includes the retrieved chunks and the user's question, with instructions to answer only from the context. This is where the magic happens — the LLM can't hallucinate because you've given it the exact material.

Hands-on walkthrough

Now let's put it together. We'll build a chat assistant that answers questions about a fictional product manual. You'll need Python 3.10+, the chromadb, sentence-transformers, and openai packages (or a local LLM like Llama 3.2 via Ollama).

Setup

pip install chromadb sentence-transformers openai

If you want to avoid API costs, install Ollama and pull a small model:

ollama pull llama3.2:3b

Full RAG pipeline

The following script does everything: ingests a few document chunks, builds the vector store, retrieves an answer, and prints the response with sources.

import chromadb
from sentence_transformers import SentenceTransformer
import os

# Use OpenAI API (or set to 'ollama' for local)
USE_OLLAMA = os.getenv("USE_OLLAMA", "false").lower() == "true"

if USE_OLLAMA:
    from langchain_community.chat_models import ChatOllama
    llm = ChatOllama(model="llama3.2:3b", temperature=0.0)
else:
    from openai import OpenAI
    client = OpenAI()

# 1. Chunk data
with open("product_manual.txt", "r") as f:
    text = f.read()
chunks = [text[i:i+500] for i in range(0, len(text), 250)]  # overlap

# 2. Embed + store
model = SentenceTransformer('all-MiniLM-L6-v2')
chroma_client = chromadb.PersistentClient(path="./manual_db")
collection = chroma_client.get_or_create_collection(name="manual")
collection.add(
    ids=[str(i) for i in range(len(chunks))],
    documents=chunks,
    embeddings=model.encode(chunks).tolist()
)

# 3. Retrieve
def retrieve(query, top_k=3):
    q_emb = model.encode(query).tolist()
    res = collection.query(query_embeddings=[q_emb], n_results=top_k)
    return res["documents"][0]

# 4. Generate
def ask(question):
    docs = retrieve(question)
    context = "\n\n".join(docs)
    prompt = f"""Answer the question using ONLY the context below. If the context doesn't suffice, say "I don't know".

Context:
{context}

Question: {question}
"""
    if USE_OLLAMA:
        return llm.invoke(prompt).content
    else:
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}]
        )
        return resp.choices[0].message.content

print(ask("What is the return policy?"))

Expected output (example):

Our return policy allows returns within 30 days of purchase, provided the product is unused and in original packaging.

A minimal example with LangChain (optional)

If you prefer higher-level abstractions, LangChain gives you a RetrievalQA chain:

from langchain.chains import RetrievalQA
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_openai import ChatOpenAI

embedding = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
vectorstore = Chroma(persist_directory="./manual_db", embedding_function=embedding)
qa = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(model="gpt-4o-mini"),
    chain_type="stuff",
    retriever=vectorstore.as_retriever()
)

print(qa.run("How do I reset the device?"))

Pro tip: Keep the raw chunk text in your database and return source metadata along with the answer — it makes debugging and citations trivial.

Compare options / when to choose what

You have several choices when building a chat assistant with a RAG pipeline. The right one depends on your scale, budget, and pivot-tolerance.

Vector databases

Option Best for Cost Effort
Chroma (local) Prototyping, small datasets Free Low
FAISS (local) High performance, offline Free Medium
Pinecone (managed) Production scale, multi-tenant $$$ Low (managed)
Weaviate / Qdrant Hybrid search, production $$ Medium

Choose Chroma when you're learning or building a demo. Choose Pinecone or Weaviate when you need uptime, scaling, and built-in filtering.

Embedding models

  • all-MiniLM-L6-v2 (local, free, 384 dims): good for English, limited multilingual.
  • text-embedding-3-small (OpenAI, $0.02/1M tokens): fast, surprisingly good, supports dimensions tuning.
  • BGE-M3 (local, 1024 dims): excellent multilingual, heavier.

LLM providers

  • OpenAI GPT-4o-mini: cheap, reliable, strong instruction-following.
  • Anthropic Claude: excels at long context and safety.
  • Local models (Ollama): privacy, zero per-request cost, but need decent hardware.

Chunking strategies

Strategy Pros Cons
Fixed-size Simple, fast Cuts sentences, loses meaning
Recursive character Preserves structure Slightly slower
Semantic Best accuracy Expensive (more calls)

When to choose what: For a production assistant, use recursive chunking with 10% overlap, a managed vector DB if you have >100k docs, and a cloud LLM for quality. For a local offline tool, use fixed-size + Chroma + Ollama.

Troubleshooting & edge cases

Even with a perfect pipeline, things go wrong. Here are the most common issues and how to fix them.

1. "I don't know" when the answer is in the docs

Symptom: The assistant says it doesn't know, but the chunk exists.

Cause: The chunk isn't being retrieved — either the query embedding is off, or top_k is too low.

Fix:

# Increase retrieval count
res = collection.query(query_embeddings=[q_emb], n_results=5)

Also, try a different embedding model. all-MiniLM may miss nuanced synonyms. Switch to text-embedding-3-small if you use OpenAI.

2. Hallucination even with RAG

Symptom: The answer includes facts not in the context.

Cause: The prompt isn't strict enough, or the retrieved chunks are irrelevant.

Fix: Re-read your prompt. Use explicit instructions:

prompt = "You are a strict assistant. If the context does not answer the question, say \"I don't know.\" Do NOT use outside knowledge."

Also, filter retrieved chunks by similarity threshold:

res = collection.query(query_embeddings=[q_emb], n_results=5, include=["distances"])
# Drop chunks with distance > 0.8 (cosine distance)

3. Query is too long or too short

Issue: Very long queries dilute the embedding signal; too short queries retrieve too many unrelated chunks.

Fix: Rephrase the query before embedding — either extract keywords or summarize the question with a quick LLM call.

4. Token limit exceeded in generation

Symptom: OpenAIError: This model's maximum context length is 8192 tokens.

Fix: Reduce top_k or chunk size. Calculate total tokens: (chunk_size * top_k) + question + prompt should be under the model limit.

top_k = 3
chunk_size = 400  # characters
# Rough estimate: 4000 tokens per 3000 chars — tune down

5. Vector database is empty or wrong results

Symptom: Retrieval returns empty or stale chunks.

Fix: Check that you're using the same collection name and path. Persistent Chroma can hold old data — delete and recreate when testing.

client.delete_collection("manual")
client.create_collection("manual")  # fresh start

6. Embedding dimension mismatch

Error: all embeddings must have the same dimension.

Fix: Stick to one embedding model for both ingestion and query. Never mix all-MiniLM with OpenAI embeddings.

What you learned & what's next

You've now built a chat assistant with a RAG pipeline from scratch. Let's recap what you mastered:

  • The core concept: RAG = retrieval + generation, and why it grounds AI answers in your own data.
  • The step-by-step workflow: chunking, embedding, storage, retrieval, and grounded generation.
  • Hands-on implementation: you wrote a working pipeline using Chroma, an embedding model, and an LLM.
  • Comparison skills: you know when to pick Chroma vs. Pinecone, local vs. cloud models, and chunking strategies.
  • Troubleshooting: you can diagnose retrieval misses, hallucination, token limits, and dimension errors.

Your next step

Your chat assistant works, but can it handle multi-turn conversations? In the next lesson, you'll add conversation memory and advanced prompt engineering so the assistant understands follow-up questions like "What about the Pro version?" from earlier context. You'll also learn how to evaluate your RAG pipeline's retrieval quality — the difference between a demo and a production system.

Keep practicing. Every real-world AI engineer starts exactly where you are now.

Practice recap

Take the product manual you used in this lesson and add a metadata field to each chunk (e.g., page number). Then, modify the ask() function to also return the source chunk IDs. Finally, ask a question that requires combining two chunks and verify the assistant uses both. This trains you for multi-chunk synthesis, a key RAG skill.

Common mistakes

  • Using top_k too low (e.g., 1) — you miss the best answer even if it's in your data.
  • Storing embeddings from different models in the same collection — causes dimension mismatch or poor retrieval.
  • Ignoring similarity thresholds — retrieving irrelevant chunks that cause hallucinations.
  • Not cleaning your documents before chunking — boilerplate and encoding issues degrade retrieval quality.

Variations

  1. Use text-embedding-3-small instead of all-MiniLM for better retrieval on complex queries (costs a bit).
  2. Leverage LangChain's RetrievalQA chain for a higher-level API and quicker prototyping.
  3. Try LlamaIndex (GPT Index) which offers data connectors and optimized retrieval abstractions.

Real-world use cases

  • Customer support bot that answers from FAQs and product manuals, reducing ticket volume by 40%.
  • Internal knowledge assistant for employee onboarding, retrieving policies and HR documents.
  • Legal or academic research tool that cites exact passages from a corpus of case law or papers.

Key takeaways

  • RAG grounds LLM answers in your own data — retrieval first, generation second.
  • Chunking and embedding quality determine retrieval accuracy more than the model size.
  • Choose Chroma for prototyping, cloud vector databases for production scale.
  • Always include similarity thresholds to filter out irrelevant chunks.
  • A strict prompt ('answer only from context') is your first defense against hallucination.
  • You're ready for the next lesson: conversation memory and RAG evaluation.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.