← Writing AI engineering

Prompts are dependencies you don't control

A prompt that had worked for weeks started refusing outright, after a change I didn't make. What that taught me about treating prompts as production dependencies.

Why did a working feature break when I changed nothing?

the mechanism
prompt v12 · unchanged → new model → HTTP 200
stop reason: refusal · well-formed, billed, not an error
versioned golden evals dollar cap → fallback model B
  1. 01 of 03

    A routine model move

    Nothing I owned changed. The provider shipped a better model, and the feature returned nothing useful, for every user.

  2. 02 of 03

    A polite refusal

    A refusal does not throw. It arrives as a success with prose where a structure should be, and it sits near a decision boundary, so retrying by hand makes it look transient.

  3. 03 of 03

    Treat it as a dependency

    A prompt is a dependency you do not control and it has no semantic versioning. So it gets what dependencies get: a version, a suite, a budget, and a route around the failure you cannot prevent.


One morning a feature in Alter stopped working. Not slowly, not intermittently: it just returned nothing useful, for every user, on a path that had been fine for weeks.

I’d deployed nothing. The prompt was unchanged, the code was unchanged, the schema was unchanged. What had changed was the model behind it, on a routine version move I’d made deliberately and tested.

The output wasn’t wrong. It was a refusal: a categorical decline of the entire task, politely explained. The new model had decided that this class of request was something it shouldn’t do.

A refusal arrives as a success

Every instinct I had was the wrong one.

A refusal doesn’t throw. It arrives with a 200, a well-formed response, and a token count you get billed for. If your code checks for exceptions, it sees success. If your code parses the response, it sees prose where it expected a structure and reports a parse failure, which sends you looking at your schema, which is fine, which wastes an hour.

It also doesn’t reproduce reliably. Refusals sit near a decision boundary, so the same input can pass on one attempt and decline on the next. You will absolutely retry it once by hand, watch it work, and conclude it was transient.

And the causal story is genuinely strange: it broke because someone else shipped an improvement. The change that caused it was, by every reasonable measure, a better model.

Read the stop reason first, and give refusal a route out

Read the stop reason before you read the output. Providers tell you why generation ended. A refusal and a bad answer look identical if you only inspect the text, and completely different if you inspect the metadata. This is now the first thing I check on any unexpected LLM result, and it has saved me hours since.

Make refusal a first-class outcome with a route out of it. The prompt itself was reframed. The task was legitimate, but the framing invited a safety read, and rewording it removed the ambiguity. More importantly, a detected refusal now falls back to a second, more capable model rather than failing the user. Not a retry; a different path, because retrying a decision is just asking the same question twice.

A prompt is a dependency you do not control, without semantic versioning

A prompt is a dependency you do not control, and it does not follow semantic versioning.

You would never let an unpinned third-party library silently change behaviour in production. But a prompt is exactly that: a contract with a system that’s being independently improved, on someone else’s schedule, in directions that are usually good and occasionally incompatible with what you built.

Which means prompts need the things dependencies need:

  • Versioning. Every prompt in Alter is a numbered, tested component, not a string in a file.
  • A test suite that runs against the real thing. Golden evals per prompt, scored by a judge rather than by string matching, because the failure modes are semantic.
  • A budget assertion. Each suite has a dollar cap checked at the end. A prompt change that gets 20% better and 300% more expensive is a change you should have to approve, not discover.
  • Structured output enforced by the protocol, not requested in the text. Tool-use with a strict schema and a validator, one retry-and-repair, then a safe fallback. “Please return JSON” is not a contract.
  • A fallback tier. Because the failure you can’t prevent is the one you have to route around.

Models fail politely, so manual testing feels reassuring

The uncomfortable part isn’t that models fail. It’s that they fail politely, in-band, with a valid response and a plausible explanation, at a rate that makes manual testing feel reassuring.

Every other production dependency I’ve worked with either works or throws. This one has a third state. It declines, or it drifts, or it quietly gets better at something adjacent and worse at yours. You cannot detect that by looking. You detect it by having a suite that runs, an assertion that fails, and a metadata field you’ve learned to read first.