Practical AI training for everyone
Module 1
7 min read
'Write a function that validates an email' works fine in a blog post and produces something you would not merge. It has no idea which validation library your team already uses, whether you are rejecting plus-addressing on purpose, what your error-handling convention is, or that this function needs to be async because everything downstream of it is. This is not the model being careless; it is the model doing exactly what you asked, which was underspecified.
A senior engineer asked to write this function would ask three questions before typing anything. An AI assistant will not ask; it will guess, confidently, and hand you something that compiles. The fix is the same one you already use with junior engineers: hand over the constraints up front instead of discovering them in review.
Underspecified vs. specified
Underspecified: 'add a function to retry a failed API call.' Specified: 'Add a retry wrapper for our `fetchWithAuth` client in `src/lib/http.ts`. Exponential backoff starting at 200ms, max 3 attempts, only retry on 5xx and network errors: never on 4xx. Must preserve the original error on final failure, not swallow it. Match the style of `withRetry` in `src/lib/queue.ts` if one exists.' The second version is the one whose output you can actually review in thirty seconds instead of five minutes.
The habit that pays off most: paste the actual function signature, type definitions, and a nearby real example from your codebase, instead of describing them in prose. A model working from your real `interface Order` will not invent fields that do not exist. A model working from 'an order object with the usual fields' will invent exactly that.
This matters more as the codebase gets less generic. A CRUD endpoint is well-represented in training data and forgiving of vague prompts. Your internal billing reconciliation logic is not represented anywhere outside your repo, and a vague prompt against it produces confident nonsense.
For anything more than a small function, ask the model to describe its approach first — data flow, edge cases it intends to handle, what it is uncertain about — before generating code. Reading a five-line plan and correcting it costs you thirty seconds. Reading two hundred lines of code built on a wrong assumption and figuring out what went wrong costs you twenty minutes.
Plan-first prompt
'Before writing code: outline how you would implement idempotent webhook handling for our Stripe integration — what you would store, when you would dedupe, and what happens on a replayed event during a partial failure. Do not write the implementation yet.' You read the outline, catch the assumption that is wrong for your schema, correct it, then ask for the code.
Try it now
Take the next non-trivial function you need to write. Instead of a one-line prompt, paste the real surrounding types, name your error-handling convention, and ask for the plan before the code. Compare the first draft to what a one-line prompt would have produced.
A few quick questions. You'll see an explanation after each one.
Question 1 of 4
Why does 'write a function that validates an email' typically produce unmergeable code?