Back to blog
AI Engineering

Stop Rewriting Prompts: Treat Them Like Software

7 min readJuly 31, 2026

Here is a pattern I have watched play out on more than one team.

Someone notices the AI feature gives a bad answer. They open the file, tweak the prompt, refresh, see a good answer, and ship it. A week later a different bad answer appears. Someone tweaks again. Six weeks in, the prompt is four hundred lines of accumulated superstition, nobody knows which sentence does what, and removing anything feels dangerous.

The prompt has become the least engineered part of a codebase that is otherwise professionally maintained. That is the actual problem — not the model.

Prompts are code with unusual failure modes

A prompt has inputs, produces outputs, has edge cases, and can regress. It belongs in version control with everything else — but three properties make it harder to maintain than normal code:

  • Failures are silent. Bad code throws. A bad prompt returns a fluent, plausible, wrong answer with no stack trace.
  • Changes are non-local. Adding a sentence to fix one case can change behaviour on cases you were not thinking about.
  • Behaviour drifts underneath you. Update the model version and your carefully tuned instructions may behave differently.

None of these mean prompts are unmanageable. They mean the usual engineering discipline is more necessary here, not less.

Build the evaluation set first

This is the whole discipline in one habit. Before optimising a prompt, collect real inputs and write down what a good output looks like.

Twenty to fifty cases is plenty to start. Include the boring ones, the ambiguous ones, the ones where the correct answer is "I don't have enough information," and every failure a user has actually reported.

Now every prompt change is measurable. You stop asking "does this feel better?" and start asking "did this fix case 12 without breaking cases 3, 7, and 19?" That question has an answer.

Separate the stable parts from the variable parts

Treat a prompt as a function, not a paragraph.

const SYSTEM = `You are a support assistant for {product}.
Answer only from the provided context.
If the context is insufficient, say so explicitly.`

const buildPrompt = ({ product, context, question }: Args) => ({
  system: SYSTEM.replace('{product}', product),
  messages: [{ role: 'user', content: `Context:\n${context}\n\nQuestion: ${question}` }],
})

The benefits are the same ones you get from any other refactor: the stable instruction is written once, the variable parts are obvious, and the whole thing is testable in isolation. It also makes provider-level caching possible, because the stable prefix stays byte-identical across calls.

Write instructions the way you would write a spec

Most bad prompts are bad writing rather than bad prompting.

  • Be specific about the output shape. "Reply in under 80 words, no bullet points" beats "be concise."
  • State the constraint positively. "Answer only from the context provided" works more reliably than "do not make things up."
  • Give one good example over three vague rules. A single worked example of input and desired output usually outperforms a paragraph of description.
  • Delete anything you cannot justify. If nobody can explain why a line is there, it is probably superstition — remove it and check the evals.

Version the prompt with the model

Pin the model version explicitly and record which prompt version was evaluated against it. When you upgrade the model, re-run the evaluation set before assuming the upgrade is an improvement. A newer model is usually better in general and occasionally worse on your specific task.

Log, for every production call: prompt version, model version, inputs, and output. When a user reports something wrong three weeks later, this is the difference between a five-minute fix and an unreproducible mystery.

The smallest eval harness that works

People imagine evaluation needs a platform. It needs a JSON file and about thirty lines of code. Here is the whole thing.

// evals/cases.json — grow this every time a user reports something wrong
// [{ "input": "...", "expect": { "contains": ["refund"], "absent": ["guarantee"] } }]

import cases from './cases.json'

const results = await Promise.all(
  cases.map(async (c) => {
    const output = await runPrompt(c.input)
    const missing = (c.expect.contains ?? []).filter((s) => !output.includes(s))
    const leaked = (c.expect.absent ?? []).filter((s) => output.includes(s))
    return { input: c.input, pass: !missing.length && !leaked.length, missing, leaked, output }
  }),
)

const failed = results.filter((r) => !r.pass)
console.table(failed.map(({ input, missing, leaked }) => ({ input, missing, leaked })))
console.log(`${results.length - failed.length}/${results.length} passed`)
process.exit(failed.length ? 1 : 0)

Substring assertions feel crude, and they are. They also catch the overwhelming majority of real regressions: the disclaimer that vanished, the phrase legal insisted on, the promise the model must never make. Start crude, run it on every prompt change, and only reach for model-graded evaluation when the crude version genuinely cannot express what you need.

The process.exit(1) is the important line. It turns the file into something CI can fail on, which is what makes the discipline stick after the initial enthusiasm fades.

Where this pays off

A team with an evaluation set can change prompts confidently on a Friday. A team without one changes prompts nervously, mostly by adding text, and their prompt only ever grows.

The irony is that the discipline here is not new or AI-specific. It is tests, version control, and clear writing — the same three things that made the rest of the codebase maintainable. Prompts have just been exempted from them for a couple of years, and it shows.

Treat the prompt like software, and it starts behaving like software: understandable, changeable, and safe to touch.

Stop Rewriting Prompts: Treat Them Like Software | IP Tech