← All courses ← Course home

Lesson 07 of 08

Evaluate the writing style

First read real replies. Write short notes on what went wrong. Group those notes into a few problem types. Then build checks only for those problems.

Agent brief (llms.md)

Why this matters on the device

A baked writing model can still sound like the original base model. It can leak a secret from your writing. It can refuse a simple rewrite. Or it can break the format the app needs. Those problems can hide behind a nice demo prompt. Treat checking as part of what you deliver: traces from the Mac merge and from the device, a short list of problem types you can explain, and a check that can run again after the next bake.

This lesson does not replace merge-then-export. You still merge the LoRA into the base, export the ordinary graph, and keep the tokenizer sidecar. Evaluation judges that baked artifact. It does not invent a runtime adapter slot.

Look at mistakes before you write a test. Read failures first. Do not start with a leaderboard, a 1-to-5 score, or a similarity number. The intro this lesson follows is Hamel Husain and Shreya Shankar, Evals for AI systems. Cite that talk once in this course and write the rest in your own words.

The steps, one box at a time

CollectMac and device JSONL
CollectMac and device JSONL
Open codeNotes on each reply
CollectMac and device JSONL
Open codeNotes on each reply
AxialVoice, secrets, refusal, format
CollectMac and device JSONL
Open codeNotes on each reply
AxialVoice, secrets, refusal, format
AutomateCode check, then binary judge
CollectMac and device JSONL
Open codeNotes on each reply
AxialVoice, secrets, refusal, format
AutomateCode check, then binary judge
AlignConfusion matrix, not %
CollectMac and device JSONL
Open codeNotes on each reply
AxialVoice, secrets, refusal, format
AutomateCode check, then binary judge
AlignConfusion matrix, not %
CIHeld-out set on every bake

Environment setup

Stay in the same Mac uv project you used to train and merge. MPS or CPU only. Do not add CUDA.

cd writing-style-lora
uv run python -c "import torch; print(torch.__version__); print('MPS:', torch.backends.mps.is_available())"
mkdir -p evals/traces evals/open-codes evals/held-out
uv run python collect_traces.py --source mac --out evals/traces/mac.jsonl
uv run python collect_traces.py --source device --out evals/traces/device.jsonl

Collect traces from Mac and device

A trace is one prompt, one reply, and the extra fields that let you replay it. Collect the same review prompts on the merged Mac model and on the baked on-device chat from lesson 06. If those two disagree, the bake or the tokenizer sidecar is the first suspect, not the judge.

{
  "id": "rewrite-email-03",
  "source": "device",
  "platform": "iOS",
  "prompt": "Rewrite this note so it sounds like me. Keep the ask.",
  "reply": "Hey, can we move Tuesday? I am slammed after lunch.",
  "base_revision": "qwen3-0.6b",
  "artifact": "writing-style.aimodel",
  "tokenizer": "tokenizer",
  "runtime_lora_swap": false,
  "latency_ms": 840
}

Open coding

Open coding means writing short notes on each reply, in your own words. Read twenty to fifty traces before you name a score. For each reply, write what you see. Do not force the note into a bucket yet. A good open code is specific: what drifted, what leaked, what the app could not parse.

{
  "id": "rewrite-email-03",
  "open_codes": [
    "voice is closer to a generic assistant than to my short notes",
    "ends with a corporate sign-off I never use",
    "no secret leaked"
  ]
}

Axial buckets

Axial coding means grouping those notes into a few problem types you can count. For this course, start with four:

Pick one person who knows the product well to decide what good and bad look like. If two reviewers disagree, the bucket is still too vague. Do not average the disagreement away.

Automate: code checks, then a binary judge

First, automate the cheap, exact problems. Secrets and format belong to code. Voice is harder. Only after the axial buckets are stable should you add a binary LLM judge (a yes-or-no check). Use it only for the problem that code cannot see.

import json
import re
from pathlib import Path

SECRET = re.compile(
    r"(sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]+PRIVATE KEY-----)",
    re.I,
)
FORMAT_NOISE = re.compile(r"(?m)^(#{1,6}\s|```|Sources?:)", re.I)

def code_checks(reply: str) -> dict[str, bool]:
    return {
        "secret_leakage": bool(SECRET.search(reply)),
        "formatting": bool(FORMAT_NOISE.search(reply)),
    }

for path in Path("evals/traces").glob("*.jsonl"):
    for line in path.read_text().splitlines():
        row = json.loads(line)
        print(row["id"], row["source"], code_checks(row["reply"]))

A judge rubric should ask one yes-or-no question. "Sounds like me" is one question. "Sounds like me and is safe and is formatted" is three questions packed into one score.

RUBRIC = """
You are checking one writing-style reply against the author's voice.
Answer PASS or FAIL only.

PASS if the reply could pass as this author's short note: direct, lightly informal, no corporate closer.
FAIL if the reply sounds like a generic assistant, invents a sign-off, or ignores the rewrite ask.

Author samples:
{voice_samples}

Prompt: {prompt}
Reply: {reply}
"""

Align with a confusion matrix, not agreement percent

Set aside a held-out slice (prompts you did not use to tune the judge). Label them yourself. Run the judge. Build a two-by-two table, called a confusion matrix: human pass or fail versus judge pass or fail. A 90% agreement number can hide a judge that never fails anyone. You care about false passes on voice drift, and false fails on good short notes.

If the judge and the human disagree in a patterned way, fix the rubric or the bucket. Do not add a second judge to cover up the first.

Held-out set and CI

Keep a slice of prompts the trainer never saw. After every merge-and-export, run code checks on Mac traces and device traces. Then run the aligned binary judge on the voice bucket. Block the bake if any secret leaks. Also block it if the voice fail rate is worse than the limit you chose in advance. After the app is live, sample real device traces into the same JSONL shape. Open-code a batch each week. Do not let the live stream change the buckets by itself.

Misconceptions

When this goes wrong

Done when

Keep learning

An eval is still an evolved unit test: you encode what "right" looks like in software. For Foundation Models agents, tool paths, and CI gates, that harness is Test AI behavior before you ship.

The same look-then-score path on images is Evaluate generations. On labels it is Evaluate the clothing classifier.

PyTorch to Core AI in Xcode · From the metal to the model · Core AI models, typed · Model architectures in plain English · Diffusion LoRA to Core AI on device

Key concepts

  • Read traces first: Mac merged model and device chat for the same review prompts.
  • Open-code, then four buckets: voice drift, secret leakage, refusal, formatting.
  • Automate secrets and format with checks. Voice gets a binary judge.
  • Judge the baked model, not the adapter.

Takeaways

  • If Mac and device disagree, check the bake or the tokenizer sidecar first.
  • BLEU, embeddings, and 1-5 scores are not the eval.
  • Block a bake on secret leaks or a voice fail rate over your limit.