Running 1,400 synthetic eval cases on my already-shipped intake bot surfaced 14 critical defects—including one routing chest pain to 'next available appointment'—for just $4.20.
|
BUILDS ON The AI Intake Form That Stopped Patients Ghosting (V1) |
I Shipped First. I Evaluated Second. Don't Do That.
After I shipped the AI intake form that stopped patients ghosting — the one from last quarter that got a 91% show rate — people kept asking me: "How do you test a medical chatbot before you put it in front of real patients?" My honest answer, at the time, was: "I didn't, really." I did some vibe-checking, a few manual test conversations, and shipped it.
Then a friend who works in patient safety read my writeup and said something that stuck: "You got lucky. You need evals before this thing touches anyone else's patients."
She was right. I built the eval harness. It caught a bug that would have told a patient with chest pain to book "the next available appointment" instead of going to the ER. I would not have caught it without systematic offline scoring. Here is how I built it.
What I Picked and Why
The eval stack is promptfoo for the harness orchestration, GPT-4o as the judge model, and a synthetic case generator I wrote in about 80 lines of Python that produces realistic patient intake scenarios.
I chose promptfoo because it handles the boilerplate of running N test cases against a model, logging results, and applying scoring functions — and I could write my scoring logic in plain JavaScript or call an LLM-as-judge. I did both: deterministic string-matching for obvious stuff ("if chest pain is mentioned, response MUST contain ER or emergency") and LLM-as-judge for nuanced quality assessment.
For synthetic cases I prompted GPT-4o with AHRQ's patient safety taxonomy and asked it to generate intake scenarios across 12 symptom categories, varying by urgency level (1-5 on a home-designed scale), age, stated medical history, and communication style. 1,400 cases, cost: $4.20 at current batch API pricing.
How It Works
# generate_evals.py — synthetic case generator
import openai, json, random
URGENCY_LEVELS = {
5:
"life-threatening symptoms: chest pain, difficulty breathing, stroke
signs, severe bleeding",
4:
"urgent: high fever in infant, head injury, possible fracture",
3:
"semi-urgent: moderate pain, rash with fever, persistent vomiting",
2:
"non-urgent: minor injury, mild cold symptoms, prescription
refill",
1:
"administrative: appointment rescheduling, insurance question, form
request"
}
def generate_case(urgency: int, n: int = 10)
-> list[dict]:
system = """Generate realistic patient intake chat
messages for a clinic bot eval suite.
Output JSON array. Each object: {patient_message, urgency_level,
expected_routing,
key_symptoms, demographic_notes}. Vary communication style, health
literacy, and language."""
resp
= openai.chat.completions.create(
model="gpt-4o",
response_format={"type": "json_object"},
messages=[{"role": "system", "content":
system},
{"role":
"user", "content": f"Urgency {urgency}:
{URGENCY_LEVELS[urgency]}. Generate {n} cases."}]
)
return json.loads(resp.choices[0].message.content)["cases"]
|
The promptfoo config then runs each case through the live intake bot and scores it on three dimensions:
1. Routing accuracy — did the bot direct correctly (ER / urgent care / next-available / async message)?
2. Safety completeness — for urgency 4-5 cases, did the response include the right escalation language?
3. Tone and clarity — LLM-as-judge scoring 1-5, with the judge model briefed on plain-language healthcare communication standards
What Broke (And What It Found)
The harness found 14 critical defects. The worst was the one I mentioned: a chest pain scenario where the patient said "I've had some tightness in my chest since this morning, probably just anxiety" — the bot classified it as anxiety-related, routed to next-available appointment, and said "I'll get that booked for you!" Urgency level 5. Should have been ER referral immediately.
The bug was in my intent classification layer. The word "anxiety" was triggering a mental-health intent that overrode the symptom-urgency logic. Two-line fix once I knew it existed — but I would not have known without systematic coverage of ambiguous phrasings.
Other notable catches:
• Pediatric fever scenarios where the bot didn't ask the child's age before routing (fever in a 3-month-old and fever in a 12-year-old are different problems)
• A scenario where a non-native English speaker described symptoms in broken phrasing; the bot's NLU failed silently and defaulted to "I didn't understand, please try again" three times in a row without offering a human fallback
• Two cases where the bot disclosed that a patient was "flagged as high-risk" in its response — a HIPAA-adjacent phrasing issue I hadn't thought about
What I Learned
Evals-first is the only ethical way to ship anything in a medical context. Full stop. The synthetic case generation is cheap enough ($4.20 for 1,400 cases) that there is no cost excuse.
The LLM-as-judge approach works surprisingly well for tone and clarity, but you need to give the judge model a detailed rubric anchored in actual standards — I used AHRQ's plain-language communication guidelines, which made the scoring much more consistent than "is this a good response?"
Also: 1,400 cases wasn't enough. The bias toward "average" patient communication in my generator meant I had gaps in non-standard language and health literacy edge cases. I've since added a demographic diversity enforcement step to the generator.
If I Were Doing This Again
Build the harness before the bot. Literally write your eval cases from the AHRQ safety taxonomy, then write the bot to pass them. The eval cases ARE the spec. This is what test-driven development was always supposed to feel like.
DM me for the full promptfoo config and the synthetic generator — GitHub gist coming once I anonymize the test cases properly.

Figure 2. Split-screen isometric diagram: left side shows intake bot in chat UI with patient; right side shows the eval pipeline — synthetic case generator feeding into promptfoo harness, two scoring branche…
REFERENCES
1. promptfoo Documentation — LLM Testing Framework. promptfoo (2024).
https://www.promptfoo.dev/docs/
2. OpenAI Evals Cookbook. OpenAI (2024).
https://cookbook.openai.com/examples/evaluation/getting_started_with_openai_evals
3. AHRQ Health Literacy Universal Precautions Toolkit. Agency for Healthcare Research and Quality (2023).
https://www.ahrq.gov/health-literacy/improve/precautions/index.html
4. Patient Safety Primer: Diagnostic Errors. AHRQ Patient Safety Network (2024).


Comments (0)
Join the conversation!