¶How do AI, machine learning, deep learning, and generative AI differ?
A study assistant can produce a convincing explanation and still teach you the wrong answer. Before choosing a model, you need to decide what counts as an acceptable answer and which parts of the job actually require learned behavior.
I would start with a small, inspectable program, not a chatbot interface. If a system cannot answer 3 questions correctly from 2 short notes you wrote yourself, adding a larger model makes the failure harder to diagnose, not easier to excuse.
¶Separate the field, the method, and the task
Artificial intelligence (AI) is the broad field of building systems that perform tasks associated with intelligence: recognizing speech, planning routes, or answering questions. AI does not necessarily mean learning from data. A rule-based tutoring system can select a hint using rules written by a programmer. Its behavior changes only when someone changes those rules.
Machine learning (ML) is an approach within AI in which a system learns patterns from examples rather than having every decision explicitly programmed. For the study assistant, a learned classifier could assign a question to “databases” or “networks.” A classifier selects a category; it need not compose an explanation. Unless its examples represent the questions students actually ask, its learned decisions may be poor.
Deep learning is a family of machine-learning methods built with neural networks containing multiple processing layers. These networks learn useful representations of their inputs rather than relying entirely on manually selected characteristics. The network mechanics come later. For now, the engineering distinction matters: deep learning describes how a model is built, not whether its answer deserves trust.
Generative AI describes systems that produce content, such as text, images, or audio. Modern generative systems commonly use deep learning, but “generative” names the task, not a separate rung above deep learning. A large language model (LLM) is a language-focused model trained on substantial text data; a generative LLM can compose answers rather than merely select stored ones. Not every deep-learning model generates content.
¶A useful answer does not prove that learning occurred
This Python example uses 0 training examples. The answers dictionary contains original study notes; lookup_answer performs a fixed lookup after strip and casefold normalize the question.
answers = {
"what is a stack?": "A stack removes the most recently added item first.",
"what is a queue?": "A queue removes the earliest added item first.",
}
def lookup_answer(question):
key = question.strip().casefold()
return answers.get(key, "No matching note.")
questions = [
"What is a stack?",
"What is a queue?",
"Explain a stack.",
]
for question in questions:
print(question, lookup_answer(question), sep="\n")
The third question returns No matching note. despite asking about a known topic. That is a matching failure, not a Python exception. Calling get successfully does not mean the assistant met the student’s need. A generated response could fail differently: it might describe a stack as removing the earliest item first, with no error message at all.
I prefer this visibly limited behavior as a starting point because you can locate the limitation precisely. It is not the finished assistant. When varied wording matters, you need a more capable method; when exact stored answers suffice, generation adds an unnecessary opportunity to alter them.
Interview check: Is every AI application a machine-learning application? No: programmer-written rules can support intelligent behavior without learning. Now apply that distinction to your assistant: which questions require composing an explanation, and which only require returning the right note?
¶Which study questions need generation, and which need ordinary software?
A study assistant that explains recursion beautifully but invents an assignment deadline is not almost finished. It is wrong at the point where a student needs certainty. I would rather ship a plain answer copied from an approved course record than a polished paragraph that changes “12 September” to “15 September.” Fluency is not the acceptance criterion.
Before choosing a model, separate the work into three categories: returning an existing fact, calculating a result, and composing an explanation. These categories describe operations, not entire questions. A single request can require all three.
¶Keep exact answers on an exact path
Consider these requests for the study assistant. The dates and marks are invented project data, not facts about a real course.
| Student request | Operation | Acceptable behavior |
|---|---|---|
| “When is Assignment 1 due?” | Look up a fact | Return the stored deadline without changing it. |
| “I scored 18 out of 24. What percentage is that?” | Calculate | Return 75%, using arithmetic. |
| “Explain recursion using a staircase analogy.” | Compose | Produce an explanation that preserves the concept. |
| “Given my deadline, suggest a three-day revision plan.” | Look up, then compose | Keep the deadline fixed while proposing a schedule. |
I keep calculations and authoritative facts outside the generator whenever the required operation is already known. Generating a percentage adds uncertainty to something Python can calculate directly. Unless the student actually wants an explanation of the calculation, prose contributes little.
This tiny example needs no packages or network connection. Run it with Python 3. The keys represent already-selected operations; this is not yet a system that understands arbitrary questions.
course_facts = {
"assignment_1_deadline": "12 September",
}
def lookup_fact(key):
return course_facts.get(key, "No recorded answer.")
def percentage(earned, possible):
if possible <= 0:
raise ValueError("possible must be greater than zero")
return 100 * earned / possible
print(lookup_fact("assignment_1_deadline"))
print(f"{percentage(18, 24):.0f}%")
print(lookup_fact("exam_room"))
The output is 12 September, 75%, and No recorded answer., on separate lines. lookup_fact() uses dict.get() so an absent record becomes an explicit missing-answer response rather than a KeyError. Meanwhile, percentage() rejects a nonpositive denominator with ValueError and the message possible must be greater than zero. Bad input should not quietly become a plausible result.
¶Generate where wording is part of the work
An explanation has many acceptable forms. A student might need shorter sentences, a worked example, or a different analogy. Generation is useful here because enumerating every useful explanation in advance would be impractical. It still needs checking: a staircase analogy fails if it never explains how recursion stops.
The uncomfortable part is that an impressive conversational interface can conceal an unnecessary model call. If there are only 8 supported requests, explicit menu choices may be a better first interface than unrestricted chat. You can add flexible language later, provided that interpreting a request does not silently change the underlying fact or calculation.
For the mixed revision-plan request, obtain the deadline first, then ask for a proposed plan. A missing deadline must remain missing; the application should not invent one to finish the conversation. The later retrieval chapters address finding information inside notes and PDFs. Here, the engineering decision is simply which operation owns the answer.
Try classifying these requests: “List the chapter titles,” “Compare these two definitions,” and “How many days remain until my exam?” For the last request, what happens if the exam year or the student’s current date is unknown?
¶How do we create a reproducible Python project with explicit dependencies?
A study assistant that runs only in your existing notebook environment is not yet a reproducible project. A hidden package installation can make your code succeed today and raise ModuleNotFoundError on another laptop tomorrow. Before adding a model, I want a second developer to be able to recreate the environment without guessing what I installed.
I would start with Python’s built-in venv, not an environment-management framework. It gives this project an isolated package directory without another tool to learn. Isolation is not complete reproducibility: Python versions, operating systems, and downloaded packages still matter. For this first stage, use Python 3.11 or newer and keep third-party dependencies at 0. The non-generative baseline does not need an AI SDK.
¶Create an environment you can throw away
Create an empty directory named study-assistant. Save the following as bootstrap.py inside it, then run python bootstrap.py from that directory. If your system exposes Python as python3, use that command instead. The script records your exact Python version and refuses a different version on subsequent runs; it does not install Python for you.
from pathlib import Path
import os
import platform
import subprocess
import sys
import venv
root = Path(__file__).resolve().parent
if sys.version_info < (3, 11):
raise SystemExit("Python 3.11 or newer is required.")
version_file = root / "python-version.txt"
version = platform.python_version()
if version_file.exists():
recorded = version_file.read_text(encoding="utf-8").strip()
if recorded != version:
raise SystemExit(
f"Python version mismatch: expected {recorded}, got {version}"
)
else:
version_file.write_text(version + "\n", encoding="utf-8")
files = {
"requirements.txt": "# No third-party dependencies yet.\n",
".gitignore": ".venv/\n__pycache__/\n",
}
for name, content in files.items():
path = root / name
if not path.exists():
path.write_text(content, encoding="utf-8")
environment = root / ".venv"
venv.EnvBuilder(with_pip=True).create(environment)
python = environment / (
"Scripts/python.exe" if os.name == "nt" else "bin/python"
)
subprocess.run(
[str(python), "-m", "pip", "install",
"-r", str(root / "requirements.txt")],
check=True,
)
subprocess.run([str(python), "-m", "pip", "check"], check=True)
print(f"Environment ready: {python}")
The final printed path identifies the interpreter your application should use. On macOS and Linux, run project scripts with .venv/bin/python; on Windows, use .venv\Scripts\python.exe. Activation is optional: it changes command lookup in your shell, not the environment’s contents. I prefer explicit interpreter paths in setup instructions because an apparently activated terminal can still differ from the interpreter selected in your editor.
If setup reports No module named venv, your Python distribution may require its virtual-environment component to be installed separately. Follow that distribution’s installation instructions, then rerun the script. If you get Python version mismatch, install the recorded version rather than silently editing the record. When intentionally upgrading Python, update the record and recreate .venv; reusing its old contents defeats the check.
¶Declare what the project actually needs
Commit bootstrap.py, python-version.txt, requirements.txt, and .gitignore to Git. Do not commit .venv: recreate it. The requirements file is deliberately empty apart from a comment. Explicitly declaring “nothing external yet” is better than installing 12 packages because a tutorial might eventually use them.
Once external packages become necessary, record exact tested versions, including indirect dependencies—the packages your chosen packages require. I avoid treating pip freeze from a personal environment as a dependency design: it records installed packages, including unrelated experiments. A clean project environment makes that snapshot more useful, but version pins alone do not guarantee identical downloads across platforms. Artifact hashes and platform-specific locking become relevant when those differences matter.
Checkpoint: recreate .venv and rerun setup. Unless both commands succeed, application debugging is premature. pip check verifies declared package compatibility, not application behavior. What should the first executable baseline prove once this clean environment is ready?
¶Can a keyword-based assistant establish a useful baseline using licensed notes?
A fluent answer can hide a broken application. Before adding a model, you need something less impressive: a program that returns an approved note for a recognizable question and refuses questions it cannot handle. I would start with explicit keyword rules here. They make the first failures visible without introducing credentials, downloads, or a paid service.
This is your baseline: a deliberately simple implementation against which later changes can be compared. It will not explain unfamiliar wording. That limitation is useful, provided you record it rather than quietly rewriting every question until the demonstration passes.
¶Use material you can actually distribute
For this exercise, use the 2 original notes below. I release these two note sentences under CC0 1.0, so you can copy, modify, and redistribute them. That permission does not extend to other book content. Keep the permission alongside the notes if you move them into another file.
I would not begin with a downloaded textbook PDF. Being available online does not establish permission to redistribute it. Your own notes are suitable only when you control the wording and have not copied protected passages or diagrams. For third-party material, check that its license permits your intended use and preserve any required attribution. Unclear permission is a reason to choose different material, not a task for the assistant to resolve.
¶Make the behavior inspectable
Save this as baseline.py in the repository from the previous section. Run it with python baseline.py; it needs 0 external packages. The small NOTES mapping keeps text and permission together. The KEYWORDS mapping is an explicit rule table, not a learned model.
import re
NOTES = {
"stack": {
"text": "A stack removes the most recently added item first.",
"permission": "CC0-1.0",
},
"queue": {
"text": "A queue removes the earliest added item first.",
"permission": "CC0-1.0",
},
}
KEYWORDS = {
"stack": {"stack", "stacks"},
"queue": {"queue", "queues"},
}
def answer(question):
words = set(re.findall(r"[a-z]+", question.lower()))
matches = [
name for name, keywords in KEYWORDS.items()
if words & keywords
]
if not matches:
return {"status": "no_match", "text": "No matching note."}
if len(matches) > 1:
return {
"status": "ambiguous",
"text": "Ask about one topic at a time.",
}
name = matches[0]
return {
"status": "matched",
"text": NOTES[name]["text"],
"note": name,
}
if __name__ == "__main__":
assert answer("What is a STACK?")["note"] == "stack"
assert answer("Explain queues.")["note"] == "queue"
assert answer("Explain recursion.")["status"] == "no_match"
assert answer("Compare stack and queue.")["status"] == "ambiguous"
assert answer("")["status"] == "no_match"
print("5 checks passed.")
print(answer("What is a stack?"))
Follow the first question through answer(). Lowercasing turns uppercase letters into lowercase; re.findall() extracts alphabetic runs, so punctuation does not prevent a match. The stack rule matches, the queue rule does not, and the function returns the stored sentence unchanged. Repeated mentions still count as 1 topic because the words are held in a set.
The refusal paths matter just as much. With no matching rule, the result contains no_match and No matching note. With both topics present, it contains ambiguous. I prefer that explicit failure to silently selecting whichever rule appears first. If an assertion fails, Python raises AssertionError; inspect the returned dictionary before changing the expected result.
These 5 checks establish behavior, not educational quality. “Why is a stack better than a queue?” returns an ambiguity response; “Explain last-in-first-out” returns no match. Worse, “Is a stack a queue?” mentions both topics but cannot be answered by either stored sentence. Adding keywords helps only when matching a topic is enough; it does not create an explanation.
Try one supported question, one paraphrase without either topic name, and one question containing “stack” that the note cannot answer. Record the desired response before running them. Which failures should a future assistant fix—and which should remain refusals?
¶What should acceptance tests prove, and how would you explain the baseline in an interview?
A study assistant that returns something for every question can look better than one that admits a miss. Until the answer is wrong. I would rather ship an obvious “no match” than quietly return a paragraph about queues when a student asks about stacks. Acceptance tests make that preference executable: they check whether the program meets a user-facing requirement, not merely whether it runs without an exception.
¶Test the promise, including its limits
Start with 5 acceptance cases using notes you wrote yourself or have permission to reuse. For a controlled fixture, use exactly 2 notes: “A stack removes the most recently added item first” and “A queue removes the earliest added item first.” Give each a stable source label. Keep this fixture separate from your growing collection so that adding a document does not silently change the expected answers.
| Input | Required behavior |
|---|---|
stack | Return the stack note and its source label. |
STACK | Return the same result, if case-insensitive matching is part of the contract. |
photosynthesis | Report no match; do not select either note. |
| An empty string or 3 spaces | Reject the input with a readable message, not a traceback. |
stack queue | Apply the documented multiple-match rule consistently. |
That last case forces a decision. I prefer returning both matching notes in a stable order for this baseline, unless the interface explicitly promises only 1 result. If you keep only the highest-scoring match, specify how ties are resolved. Otherwise, a harmless change in file ordering can change the answer.
Put these checks in test_acceptance.py. Python’s unittest is sufficient here; I would not add another dependency just to check a handful of deterministic outcomes. Use assertEqual for expected text or source labels and assertIn when checking required content. An AssertionError should identify the violated promise, such as “unrelated question returned a note,” rather than merely saying “wrong.”
Do not accept “nonempty output” as success. An error message is nonempty too. Check the selected note, source label, and no-match behavior separately, as long as the program exposes those distinctions. If everything is fused into printed text, that is a reason to separate the answer-producing function from command-line display—not a reason to weaken the tests.
¶Explain what the baseline earns
Interview question: “Why build keyword matching when the project is supposed to use an LLM?”
“It gives me a deterministic comparison point before adding a model. I can check input handling, note selection, and source display without a network dependency. It cannot reliably handle paraphrases or explain relationships between notes. I would add a model only after identifying a requirement that this simpler implementation cannot satisfy, then test the replacement against the same user-facing expectations.”
Do not claim the baseline understands the material. Try the exercise last in, first out: if your matcher misses the stack note because the words differ, record that limitation rather than disguising it with a special case. Passing 5 cases proves those cases passed, not that arbitrary questions work.
The next chapter needs this repository and its explicit expectations as a fixed starting point. Keep the baseline runnable while you build the foundations needed to explain what a learned system changes.