Capstone 1 — Build a Simple RAG Chatbot

This is the first of the program’s three linked portfolio pieces. You’ll build a chatbot that answers questions grounded in a small document set of your choosing, applying everything from Module 1 (tokens/context), Module 2 (prompting), and Module 3 (RAG).

Architecture you’re building

flowchart TB
    Docs["Your document set\n(notes, manual, policy doc...)"] --> Chunker["Chunker\n(Module 3.3)"]
    Chunker --> Embedder["Embedding model"]
    Embedder --> Store[("Vector store")]

    User["User question"] --> QEmbed["Embed query"]
    QEmbed --> Retrieve["Retrieve top-k chunks"]
    Store --> Retrieve
    Retrieve --> Prompt["System prompt:\n'answer only from context'\n(Module 2.3)"]
    Prompt --> LLM["LLM call"]
    LLM --> Answer["Answer\n(or 'not in my documents')"]

Brief

Pick a small document set you actually care about — your own notes, a product manual, a policy document, a handful of blog posts. 5-20 pages total is plenty; this is about the pipeline, not the corpus size.

Build a script or notebook that:

  1. Chunks the document set (Module 3.3) — make and note a deliberate choice about chunk size and overlap.
  2. Embeds each chunk and stores it in a vector store (a local library like a simple vector index is fine — this doesn’t need to be production infrastructure).
  3. Retrieves the top-k relevant chunks for an incoming question.
  4. Generates an answer using a system prompt (Module 2.3) that explicitly instructs the model to answer only from the retrieved context, and to say when the context doesn’t contain the answer.

A skeleton to start from (fill in embed() and llm_call() with your chosen provider’s SDK):

def embed(text: str) -> list[float]:
    """Call your embedding provider here."""
    raise NotImplementedError

def llm_call(system_prompt: str, user_message: str) -> str:
    """Call your chat completion provider here."""
    raise NotImplementedError

def build_index(documents: list[str]):
    chunks = [c for doc in documents for c in chunk_text(doc)]  # from Module 3.3
    return [(c, embed(c)) for c in chunks]

def ask(question: str, index):
    return answer_question(question, index, embed, llm_call)  # from Module 3.4

Requirements to hit before calling it done

  • Ask it at least 3 questions that are answerable from your documents, and confirm the answers are grounded and correct.
  • Ask it at least 2 questions that are not answerable from your documents, and confirm it says so rather than hallucinating (Module 4.1) an answer.
  • Note the chunk size/overlap you chose and why.

Deliverable

  • The working script or notebook
  • A short writeup (a paragraph or two) covering: what chunking strategy you used and why, one thing that surprised you about retrieval quality, and one case where the model still got something wrong despite having the right context in front of it

Why this matters later

This chatbot doesn’t need to be sophisticated — its value is that it’s yours, working end to end, before Track 2 introduces tool use and agents on top of the same foundational skills.