BIA 660 · Web Mining · Stevens Institute of Technology · Spring 2026

Introduction to Large Language Models
— the interactive lab

One session: from calling an API to auditing what it returns

By this week, you have built the classical text-mining stack by hand — scraping, preprocessing, classification, clustering, topic modeling, sentiment, word vectors. This lab asks one question of every technique: what does an LLM do differently, where does it win, and where does it fall over?

How to use this page

Work through the sections in order and press Run on each demo. The outputs are not simulated — they come from a real run of the lab (a local llama3 via Ollama, plus frontier models over API). Each demo keeps its real Python underneath — expand “Show the code” anywhere, then open the notebook and run it yourself: where your outputs differ from these is worth a second look.

The lab in one arc

§1Call four providers with one identical prompt — APIs vs. a free local model
§2Re-do six classic text-analysis tasks with an LLM, next to the traditional tool
§3Break it: sarcasm, false confidence, and scores that follow the prompt, not the data
§4Measure it: accuracy, reliability, agreement, robustness, LLM-as-judge
§5Decide: a task-shape guide for when the LLM is the right tool

The habit this lab builds

Every demo follows the same rhythm: observe the raw text, execute a small step, compare outputs side by side, and question the result — does this make sense? what would change it? The section that matters most is §3: you’ll first watch the LLM succeed, then watch it fail in ways that look exactly like success. That contrast — not the API syntax — is what you should carry into your project.

§0 · Local LLM setup

Everything in this lab runs free and offline on your laptop. Two pulls from Ollama and the environment is ready — no API key required to participate.

Install once

ollama pull llama3 # chat model (~4.7 GB) ollama pull nomic-embed-text # embedding model (~270 MB, used in §2.7)
runs check_ollama() — the first cell of the notebook
Show the code — the lab’s chat() helper
import ollama, json, time, re
DEFAULT_MODEL = "llama3"
EMBED_MODEL   = "nomic-embed-text"

# One helper, reused all session long
def chat(prompt, system=None, model=DEFAULT_MODEL, temperature=0.0, fmt=None):
    messages = ([{"role": "system", "content": system}] if system else [])
    messages.append({"role": "user", "content": prompt})
    kwargs = dict(model=model, messages=messages,
                  options={"temperature": temperature})
    if fmt == "json":
        kwargs["format"] = "json"
    return ollama.chat(**kwargs)["message"]["content"]

§1 · One prompt, four providers

Model builders vs. inference providers: the same model is often served in several places at different speeds and prices. Send one identical prompt to OpenAI, Anthropic, Google, and a local llama3 — and compare what comes back.

The shared prompt — a small financial-analysis task

Act as a financial analyst. Based on the following metrics, provide a one-sentence conclusion starting with 'Signal: [Good/Bad/Neutral]' followed by the short reason why. Net sales increased 14% to $213.4 billion in the fourth quarter, compared with $187.8 billion in fourth quarter 2024. Operating income increased to $25.0 billion … Net income increased to $21.2 billion, or $1.95 per diluted share …

A brief note on parameters

ParamWhat it does
temperaturerandomness — 0 = deterministic, 1+ = creative
top_pnucleus sampling (alternative to temperature)
max_tokenscap the output length
stop / stream / seedend strings · stream tokens · best-effort reproducibility

Rule of thumb: temperature=0 for extraction and classification (most of this class), ≈0.7 for creative writing. Parameters are tied to the model, not the company — reasoning models swap temperature for reasoning_effort; embedding models take only input. Always check the current docs.

§2 · Six classic tasks, redone with an LLM

Each demo pairs a technique you have already built by hand with its LLM counterpart — and ends with what to watch out for. Work left to right.

One prompt replaces the whole NLTK pipeline

Prior lectures: tokenization → POS → NER → regex, all hand-rolled. With an LLM: describe the schema you want, in words.

Input — a raw job post

Avery Dennison is looking for a Materials Scientist - Machine Learning. Requirements: Ph.D. in Polymer Science, Chemical Engineering, or Physics. Experience with Python, TensorFlow, PyTorch, C++. Familiarity with Molecular Dynamics and Finite Element Analysis. Location: Mentor, OH. Contact: xxx@stevens.edu
{"company": "Avery Dennison", "job_title": "Materials Scientist - Machine Learning", "skills": ["Python", "TensorFlow", "PyTorch", "C++"], "location": "Mentor, OH", "email": "xxx@stevens.edu"}
✅ What the LLM did: one prompt replaced tokenization + POS + NER + regex email extraction — and it adapted to our schema, not NLTK’s.
⚠️ But: when the text doesn’t contain every field we ask for, the model may fill the gap from its pretraining memory. That’s the next experiment.
Show the code
out = chat(
    prompt=f"Extract entities from this job post. Return JSON with keys: "
           f"company, job_title, skills (list), location, email.\n\nPOST:\n{job_post}",
    system="You are an information-extraction assistant. Output ONLY valid JSON.",
    fmt="json",
)

The hallucination experiment — same passage, three prompts

This passage hints at famous specifics without stating a single one — a deliberate trap. Run it three ways and compare field by field.

The bait passage

Apple's long-serving CEO delivered the keynote at the company's annual developer conference this week. The event was held at Apple's California headquarters campus and drew thousands of developers from around the world. Management also highlighted continued growth in services revenue during the most recent quarter and previewed new AI capabilities.
Show the code — the two prompts
# STRICT — the guardrail
"IMPORTANT: If a field is not explicitly stated in the passage, return null. "
"Do NOT use outside knowledge. Do NOT guess."

# LOOSE — no guardrail, plus a framing that primes completion
"Complete the company briefing below. Fill in every field."

Zero-shot sentiment vs. the supervised classifiers you just built

Prior lecture: TF-IDF features + Naive Bayes / SVM / logistic regression, labeled training data, F1/AUC. Now: no training data at all — describe the task in words, and run it on the same 30-review Amazon sample from the word-vector exercise.

The entire “model” — a system prompt

Classify product reviews as POSITIVE or NEGATIVE. Respond with exactly one word: POSITIVE or NEGATIVE. Nothing else.

Classification report (n = 30)

precisionrecallf1
POSITIVE1.000.870.93
NEGATIVE0.881.000.94
accuracy0.93

Confusion matrix (rows = gold)

✅ No training data, no features, still competitive. This is the biggest shift from traditional ML: the model is already “trained.” Your job becomes prompt design and evaluation, not feature engineering.
⚠️ But LLMs are non-deterministic at higher temperatures — that’s demo 2.3.
Show the code
def classify(text):
    ans = chat(prompt=text, system=SYSTEM, temperature=0.0).upper()
    if "POSITIVE" in ans: return "POSITIVE"
    if "NEGATIVE" in ans: return "NEGATIVE"
    return "UNKNOWN"

for i, row in tqdm(enumerate(sample.itertuples()), total=len(sample)):
    predictions.append(classify(row.text))

print(classification_report(clean["gold"], clean["pred"],
                            labels=["POSITIVE","NEGATIVE"]))

Same input, same prompt — can you trust a single answer?

A deliberately ambiguous review

“The product is great and functional, but the color is not what I expected.”
5 runs each — spot the difference

Takeaway: temperature=0 gives (nearly) deterministic output but can still drift across runs and model versions. For labeling pipelines, always run multiple passes and vote — next demo.

Majority voting — the easy reliability upgrade

Ten harder reviews, five votes each at temperature=0. Watch the one case where a perfectly unanimous vote is still wrong.

GoldMajorityThe five votes
⚠️ Row 5: five identical votes, all wrong. Unanimity measures the model’s consistency, not its accuracy — a thread picked back up in §3.2.
Show the code
def classify_vote(text, n=5, temperature=0):
    labels = []
    for _ in range(n):
        ans = chat(prompt=text, system=SYSTEM, temperature=temperature).strip().upper()
        hits = re.findall(r"POSITIVE|NEGATIVE", ans)
        labels.append(hits[0] if hits else "NEGATIVE")
    return Counter(labels).most_common(1)[0][0], labels

Topic labeling — interpretation, not clustering

The LLM doesn’t replace LDA — it replaces the human who used to name LDA’s clusters. First the naive version: keywords only.

A coherent topic (what LDA might produce)

electionvotecandidatecampaignpolldemocratrepublicansenatordebateballot

coherent words → “Election Politics” ✓ reasonable

Pure noise — a junk cluster on purpose

carrecipedogquantumlemoncongresssocketoperaxeroxpineapple

noisy words → “Food Recipe Book” ✗ confidently invented
⚠️ Failure mode: overconfident labeling. Given meaningless keywords, the LLM still produces a confident-sounding label. It will not volunteer “your topic model may be bad.”

The production fix: keywords + representative documents + a self-rating

Give the model what a human analyst would get — a few documents near the cluster centroid — and permission to report low coherence.

Cluster A — battery complaints

{"label": "Short Battery Life", "description": "Devices draining quickly or dying after a short period.", "coherence": "high"}

coherence: HIGH

Cluster B — deliberate junk

{"label": "Miscellaneous News", "description": "A collection of unrelated news articles.", "coherence": "low"}

coherence: LOW — the model pushed back

What works in production: keep the LLM out of the clustering itself (LDA/BERTopic stay deterministic); use a stronger model for the one-time labeling step; and never trust the coherence flag alone — validate with a human pass.
Show the code — the richer labeling prompt
LABEL_TMPL = (
  "Below are the top keywords and a few representative documents from one cluster.\n"
  "Decide on a 2-4 word label and a one-sentence description.\n"
  "Also rate the cluster's coherence: high / medium / low.\n"
  "If keywords and documents do not share a clear common theme, set coherence to 'low' "
  "and say so. It is acceptable to report low coherence — do not invent a theme.\n\n"
  "TOP KEYWORDS: {kw}\n\nREPRESENTATIVE DOCUMENTS:\n- {docs}"
)

Aspect-based sentiment, structured for a pipeline

Prior lecture: lexicon sentiment — sum the positive words, subtract the negative ones. It misses negation, context, sarcasm. The LLM extracts {aspect: sentiment} pairs directly, as JSON.

“The phone battery lasts all day which I love, but the camera is truly awful in low light, and the price is ridiculous for what you get.”

battery POSITIVE camera NEGATIVE price NEGATIVE

Harder: aspects the review never names

This reviewer never says “size,” “waterproofing,” “durability,” or “warranty” — they describe them obliquely. A bag-of-words extractor can’t catch these without a hand-built ontology. Click each extracted aspect to see its evidence in the text.

The review

✅ The LLM names aspects the review never spells out and ties each to supporting evidence — impossible for lexicon pipelines without an ontology you build by hand.
⚠️ Watch out — aspect-name drift. Across thousands of reviews the same concept surfaces as “weight,” “portability,” “size,” “bulkiness.” For pipelines: pin an allowed-aspect vocabulary in the prompt, or normalize afterward with an embedding-based merge.
Show the code
implicit_prompt = (
  "Extract aspect-sentiment pairs from the product review. "
  "Aspect names should reflect what the customer is actually talking about, "
  "even if they don't use that exact word. "
  "Return JSON: {aspects: [{aspect, sentiment, evidence}]}\n\nREVIEW:\n" + implicit_review
)

Three eras of “similar”: TF-IDF → MiniLM → LLM-era embeddings

One query, seven documents, three methods. The query shares zero words with the two documents that actually answer it.

Query

“portable power ran out too fast”

⚠️ Never compare cosine values across encoders — only ranks. Each method lives in a different vector space: TF-IDF is sparse (no shared words ⇒ exactly 0), MiniLM unrelated-sentence baseline is ≈0.0–0.4, nomic’s floor is ≈0.4. A 0.65 from one is not “better than” a 0.38 from another.
When to pick which: TF-IDF still wins on exact-keyword domains (legal citations, SKUs, code symbols) · MiniLM is the cheap, fast, offline default for business text · LLM-era embeddings win on nuance, long documents, and multilingual content.
Show the code — all three methods
# 1) TF-IDF cosine
v = TfidfVectorizer().fit(docs + [query])
tfidf_scores = cosine_similarity(v.transform([query]), v.transform(docs))[0]

# 2) Sentence-transformer (BERT-era, 384-dim)
st = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
st_scores = cosine_similarity([st.encode(query)], st.encode(docs))[0]

# 3) LLM-era embedding via Ollama (768-dim)
def embed(text):
    return ollama.embeddings(model="nomic-embed-text", prompt=text)["embedding"]
ollama_scores = [cos(embed(query), embed(d)) for d in docs]

§3 · Where LLMs struggle — failures that look like successes

Three subtle cases: sarcasm, self-consistency that isn’t correctness, and numerical scores that follow the prompt instead of the data. Each one arrives after you’ve watched the model succeed in §2 — that’s deliberate.

You first: label these five reviews

Bojić et al. (2025, Scientific Reports) measured LLM–human agreement across sentiment, political leaning, emotional intensity, and sarcasm — sarcasm shows the largest gap. Commit to your own labels before any model answers. No changing your mind after.

label all five to unlock
In this run: both llama3 (8B, local) and claude-sonnet-4-5 (API) labeled all five NEGATIVE — every one of these is sarcastic. When you run the notebook yourself, expect the small local model to miss at least one of the subtler cases; the frontier model usually gets all five. If yours differs, you’ve just seen model variance first-hand.
Rule of thumb: prototype locally on llama3, validate the hard cases on a frontier model, then decide which to deploy. Bigger models read subtext better — and cost more.

The agreement trap

A tempting pipeline: run the LLM N times, the runs agree, Cohen’s κ ≈ 1.0, declare the labels reliable. But a model can be consistently wrong — Bojić et al. report LLMs beating humans on inter-rater reliability, which measures agreement, not accuracy.

“The movie was so incredibly detailed that I managed to finish my entire 400-page book during the opening credits.”
run 1 · NEGATIVErun 2 · NEGATIVE run 3 · NEGATIVErun 4 · NEGATIVE run 5 · NEGATIVE

majority: NEGATIVE · true label: NEGATIVE · self-agreement κ = 1.0 — and this time it happens to be right.

The trap: if all 5 runs had returned POSITIVE, you would have perfect self-consistency and perfect inaccuracy at the same time, and κ could not tell you. For any labeling pipeline you plan to trust, compare against human-labeled examples — never against the model’s agreement with itself.

“Just give each review a 0–10 score” — a common business ask

Three reviews — a rave, a genuinely mixed one, a rant. Two tests, both at temperature=0, five runs per condition (every condition returned five identical scores — that’s the stability temperature 0 buys; the question is what the stable number means).

Test 1 — same reviews, two rubrics

Test 2 — same reviews, two reviewer personas

If the score measured the review, the persona shouldn’t matter. Watch the mixed review.

lenient persona strict persona gap
⚠️ The +3.0 gap on the mixed review is the failure mode in raw form. The model isn’t measuring the review; it’s producing “the number a lenient/strict reviewer would say.” An LLM has no numerical measurement module — it picks the number that fits the surrounding words, including whatever persona or rubric you added. Stability at temperature 0 only means it lands on the same plausible number every time.
Practical fixes: anchor the rubric with one example per band · bucket to the coarsest classes downstream can use (detractor/passive/promoter beats 0–10) · pin every prompt element that touches the score and re-validate on any change · report an LLM ordinal with the prompt that produced it, the way you’d report a survey answer with the survey question.

§4 · How do you know the LLM did a good job?

Five measurements, all computed on the §2.2 classifier. Each answers a different question — no single number is “the” evaluation.

Accuracy — is it right?
0.929
F1, positive class, vs. human gold
Reliability — self-agreement
κ 1.000
run 1 vs. run 2, same prompt
Agreement — vs. humans
κ 0.867
LLM labels vs. human gold
Robustness — prompt rewrites
κ 0.851
v1 vs. v2 · (v1 vs. v3: 1.000)
LLM-as-judge
4.88/5
mean Likert over 8 items

The toolkit, in one table

EvaluationWhat it answersMethod
AccuracyIs the LLM right?Compare to human gold labels: F1, classification_report, confusion matrix
ReliabilityDoes it agree with itself across runs?Run N× at temperature 0; majority vote; inspect vote spread
AgreementDoes it agree with humans (or another LLM)?Cohen’s κ between LLM labels and human labels
RobustnessDo small prompt rewrites flip answers?2–3 paraphrased prompts; κ between runs — different from reliability
LLM-as-judgeCan a stronger model grade a weaker one?G-Eval / pairwise / Likert — needs strong justification in academic work

Grounding in current evidence, worth reading: Bojić et al. 2025 (LLMs beat humans on inter-rater reliability — agreement, not accuracy) and Bermejo et al. 2025 (GPT-4 / Claude 3.5 beat outsourced coders on 5 NLP tasks), both in Scientific Reports.

Show the code — all five metrics
f1_pos = f1_score(clean["gold"], clean["pred"], pos_label="POSITIVE")   # accuracy
kappa_self  = cohen_kappa_score(run1, run2)          # reliability (same prompt, 2 passes)
kappa_human = cohen_kappa_score(gold, pred)          # agreement with humans
kappa_v12   = cohen_kappa_score(v1, v2)              # robustness (paraphrased prompts)
judge = chat(f"REVIEW:\n{r}\n\nCLASSIFIER ANSWER: {p}", system=JUDGE)   # LLM-as-judge

§5 · When to use an LLM for text analysis

The lab’s closing synthesis: fit depends on task shape more than model choice. Pick a task to see its verdict, or read the full table.

What’s your task?

Main strength

Main pitfall

The full guide

TaskLLM fitMain strengthMain pitfall

What this means in practice

If the task is a strong fit ✅, zero/few-shot prompting is probably competitive with paid human annotation — effort shifts from “label more data” to “design better prompts and evaluation.” If it’s ⚠️ moderate, build a small expert-labeled validation set first (50–200 items) and treat the LLM as a collaborator you audit, not an authority. Everywhere: structured JSON output, temperature 0 for labeling, majority voting for high-stakes labels, version-pin the model — and never measure quality by the model’s agreement with itself (§3.2).

Suggested workflow for the course project

1Prototype on ~10 examples in a notebook. Eyeball the outputs.
2Manually label 50–200 items — this is your gold set.
3Measure F1 or Cohen’s κ. Iterate on prompt, few-shot examples, JSON schema.
4Still short? Fine-tune a smaller model, or fall back to traditional ML.

Good luck playing with LLMs! 🎓