"Prompt engineering" gets dismissed as a temporary hack. Real engineers will eventually just fine-tune models or train them to do the right thing without fiddling with text. This misunderstands what prompt engineering actually is. It's the same discipline as writing a precise API contract, a comprehensive type system, or a good test suite. The medium is different. The underlying problem is the same.
The medium is natural language, which resists precision. Natural language has ambiguity, implication, context-dependence, and a reader (the model) whose interpretation of any given sentence is partly a function of what it was trained on. Writing a prompt is harder than writing a function signature for the same reason that writing a legal contract is harder than writing code: you're trying to specify behavior in a medium that wasn't designed for specification.
But the goal is specification. That framing changes how you think about the problem.
What a prompt is
A prompt is a specification of desired behavior, written in natural language. The model is the runtime that interprets it. When you write a prompt, you're telling the runtime what you want it to do: what inputs to accept, what outputs to produce, what format to use, what constraints to respect.
A bad prompt is a bad spec. It's ambiguous: the model has multiple valid interpretations and picks one, possibly not the one you intended. It's underspecified: it describes the task but omits critical constraints, so the model makes up plausible defaults that don't match what you wanted. It's full of implicit assumptions, things you know about your system or your users that the model doesn't know and that you haven't explained.
The feedback loop is different from writing code. You can't compile a prompt and have the compiler tell you where the ambiguities are. You have to run the prompt against representative inputs and inspect the outputs. This is slower, and the failure signals are softer. An output that's wrong in a subtle way is harder to catch than a compiler error. But the discipline is the same: write a clear specification, test it on representative inputs, and iterate.
The parallels
Type systems catch specification errors at compile time: the program can't run if the types don't agree. Tests catch specification errors at run time: the program runs, but produces wrong outputs on the inputs you test. Prompts get tested at inference time, on real inputs, at the cost of latency and money.
In each case, the underlying principle is identical: be precise about what you want, verify it works on representative inputs, and run regressions when you change the spec. Type annotations and test suites don't make the need for precise specification go away. They're just tools for enforcing it. Prompts are another tool for the same problem, with a different enforcement mechanism (a language model's interpretation) and a different failure mode (outputs that are wrong in plausible-sounding ways).
The reason prompt engineering feels unfamiliar is that the feedback loop is statistical rather than deterministic. A type error is binary: either the types agree or they don't. A prompt failure is probabilistic: the prompt produces wrong outputs on some fraction of inputs, and you have to measure what fraction and on which inputs. That's more like testing than type-checking, which is a useful way to think about it: prompts have test coverage, not type soundness.
Common spec failures in prompts
The failure modes in prompts map exactly onto failure modes in other specifications.
"Summarize this." How long? One sentence or one paragraph? For what audience? An executive who wants conclusions, or an engineer who needs technical detail? What to include: key decisions, open questions, action items? Should the model include information that's implied but not stated, or only what's explicitly in the text? This prompt is like an API with a single parameter called "data" that returns "result." You've specified nothing.
"Be helpful." Helpful to whom? The user asking, or the operator's business? Under what constraints? This is the specification equivalent of "do the right thing": accurate, technically, and completely useless as a guide to behavior.
Underspecified output format is another common failure. If you don't specify that the model should respond in JSON, it's free to respond in JSON, Markdown, prose, or a hybrid. It will choose inconsistently across runs, which breaks any downstream parser that expected a specific format. The model didn't fail. You didn't specify the format.
Wrong assumptions about what the model knows are particularly subtle. Internal systems, proprietary terminology, company-specific conventions: if these appear in your prompt without explanation, the model will fill in the blanks with something plausible. What it fills in may be wrong. This is exactly the same failure as writing a function that assumes the caller knows your internal conventions, without documenting them.
Precision techniques
The engineering techniques for making prompts more precise are specification techniques, not prompt magic.
Few-shot examples are the most powerful. Showing the model three or five examples of correct input-output pairs is more reliable than describing the desired behavior in abstract terms, because natural language descriptions are ambiguous but examples are concrete. The model infers the pattern from the examples rather than interpreting a description. This is analogous to property-based testing: you specify behavior by example rather than by rule.
Output format specification, like "respond only with JSON matching this schema", constrains the model's decision space to a specific structure. This has two benefits: it makes outputs parseable downstream, and it reduces variance. When the model has fewer choices, it makes fewer surprising choices.
Chain-of-thought prompting ("think step by step before answering") instructs the model to make its reasoning explicit before producing a final answer. This improves accuracy on tasks that require multi-step reasoning, because the model is forced to do the reasoning visibly rather than skipping to a conclusion. It's also useful for debugging: you can read the reasoning and see where it went wrong.
Explicit constraints like "do not include information not present in the provided context," "do not suggest actions that require more than $50," or "always respond in the language of the user's query" are guard rails that prevent the model from making plausible but unwanted decisions in underspecified situations. These are the prompt equivalent of preconditions in a function contract.
Testing prompts
A prompt without a test suite will drift. When the prompt changes, behavior changes in ways you may not notice. You changed one behavior intentionally and changed two others accidentally. When the underlying model changes (and model providers do update models, sometimes with significant capability changes), behavior changes without any prompt modification at all. Either way, without tests, you won't catch it until a user does.
The fix is a set of representative inputs with expected outputs or evaluation rubrics, what the LLM evaluation community calls "evals." Run evals on every prompt change. Review cases that regress. Understand why the behavior changed before deciding whether the change is acceptable.
Evals can be as simple as a spreadsheet of fifty input-output pairs where you manually verify outputs. They can be as sophisticated as automated evaluation pipelines with LLM-as-judge scoring and regression alerts. The key property is that they exist and you run them. An eval you run once and ignore is worthless. An eval you run on every change is the thing standing between you and prompt regressions you don't find out about until a user complains.
The versioning problem
Prompts are code. They express logic: conditional behavior, constraints, format requirements, output structure. They change over time. They interact with the model in ways that are version-dependent. And yet most teams store prompts in ways they would never store code: as string literals inline in application logic, as values in environment variables, as entries in a CMS that has no diff history.
Prompts should live in version control. Changes to prompts should have changelogs, not because this is a bureaucratic process, but because "we changed the output format to always include a confidence score" is information future you will want when debugging a regression six months from now.
When a model provider updates the underlying model, even within the same version family, even in a patch release, behavior can change. Sometimes substantively. Organizations that treat prompt iteration as the key engineering discipline discover this during unplanned incidents. Model updates should trigger eval runs as automatically as code pushes trigger CI. Treat an unexpected change in model behavior the same way you'd treat an unexpected change in a library you depend on: investigate, understand the impact, and decide consciously whether to accept it.
The same problem it always was
The frustration with prompt engineering is usually frustration with writing precise specifications in a medium that resists precision. Natural language is not a programming language. Its semantics are context-dependent and partially implicit. Getting a model to do exactly what you want, consistently, across the full distribution of real inputs, requires the same discipline as getting any complex system to behave correctly, but the error signals are softer and the feedback loops are slower.
That frustration is valid and familiar to anyone who's written a large API contract or a comprehensive interface definition. The solution is the same as always: be more specific about what you want, test that you got it, verify that it still holds after changes, and treat regressions as bugs rather than as expected randomness.
"Prompt engineering" is a bad name for this. "Behavioral specification with natural language" is more accurate but harder to say. Either way, the craft is the same one engineers have practiced for decades: write down what you want precisely enough that the runtime does it correctly.
Back to writing