Home / Articles / Why Adding One More Prompt Quietly Wrecks an LLM App

This article is published in English.

Why Adding One More Prompt Quietly Wrecks an LLM App

Prompt pile-ups create contradictions, hidden business logic and debugging pain. Move decisions to code, version and test prompts, and keep context small.

2178 words

Every LLM project hits a moment when the model starts acting up. Answers come out vague, so somebody rewrites the prompt. Still not right, so an extra instruction goes in. Then a few examples, which finally work. Next come the rules: "don't hallucinate," "be concise," "stick to the supplied context." Eventually a teammate asks whether you can just add one more prompt, and you do.

That sentence sounds harmless. Usually it isn't. Each time something breaks, the application collects another layer of prompts, wrappers, edge cases and unspoken assumptions. Version one might be this simple:

response = llm(
    "Answer the user's question clearly."
)

Several iterations later it looks like this:

response = llm(
    SYSTEM_PROMPT
    + SAFETY_PROMPT
    + DOMAIN_PROMPT
    + STYLE_PROMPT
    + RETRIEVAL_PROMPT
    + USER_CONTEXT
    + CONVERSATION_HISTORY
    + FEW_SHOT_EXAMPLES
    + USER_QUERY
)

Then everyone wonders why the thing became impossible to debug. The uncomfortable answer is that few teams have a prompt problem. They have a system design problem disguised as one. The prompt grows while the architecture shrinks, and every added line costs something, and the bill isn't only tokens. The bigger charge is complexity.

How the trap closes

It starts innocently. The model answers:

The customer appears to qualify for a refund.

You'd like something firmer, so you edit the prompt:

Determine whether the customer qualifies for a refund.
Return a clear yes or no.

Now the reply is:

Yes.

Fine. Then someone spots that it sometimes says yes to customers who clearly don't qualify, so a new instruction arrives:

Only approve refunds when the purchase is within 30 days
and the product is eligible under company policy.

Next the model is caught making up policy, so you add:

Do not invent policy.
Use only the provided policy context.

Legal gets involved:

Do not provide legal advice.

Product asks for a friendlier voice:

Use a warm and conversational tone.

Support wants shorter replies:

Keep responses under 100 words.

Then somebody notices the short replies skip important details:

Be concise but comprehensive.

Congratulations, you've built a contradiction engine.

A prompt that does everything

That's the first hidden cost. The more problems you fix inside the prompt, the more jobs the model inherits. Soon one string is acting as classifier, policy engine, retrieval layer, formatter, validator, business-rules engine, safety layer and UI copywriter. Far too much for a block of text. Look at this one:

PROMPT = """
You are a customer support assistant.

First determine the user's intent.

Then decide whether the request is eligible.

Then retrieve the correct policy from the supplied context.

Then determine whether the policy applies.

Then calculate the refund amount.

Then explain the decision.

Never contradict company policy.

Never hallucinate.

Be concise.

Be friendly.

If the user is angry, acknowledge their frustration.

If the user's request is ambiguous, ask a clarifying question.

If the request involves legal concerns, avoid legal advice.

Return valid JSON.
"""

It looks sophisticated, and it's a warning sign. The design is forcing the model to make too many decisions. A better architecture handles the deterministic decisions in code.

Let code do what code can do

When a rule is deterministic, keep it deterministic. Say your policy gives customers 30 days to request a refund. Don't coax an LLM into enforcing that. Write the check:

from datetime import datetime, timedelta

def refund_eligible(purchase_date: datetime) -> bool:
    age = datetime.utcnow() - purchase_date
    return age <= timedelta(days=30)

Then let the model do what it's good at, which is explaining the verdict:

def build_explanation(order, eligible):
    prompt = f"""
    Explain this refund decision to the customer.

    Order age: {order.age_days} days
    Eligible: {eligible}

    Do not change the decision.
    """

    return llm(prompt)

That's a far healthier split. Code decides and the model communicates, and keeping those roles apart matters.

Length hurts more than tokens

Long prompts cost money through tokens, true enough, but that's the easy part. The harder part is cognitive load. Imagine a prompt holding this:

Rule A
Rule B
Rule C
Exception D
Exception E
Special handling for Enterprise users
Special handling for German users
Special handling for refund requests
Special handling when context is missing
Special handling when the user is angry
Special handling when the user asks twice
...

Now ask which instruction is governing a given response. Good luck. Nobody can answer that reliably, so why would an LLM app stay predictable as the pile grows?

Prompts turn into technical debt

Code becomes technical debt, and so do prompts. A prompt crosses the line when:

  • nobody knows why a given instruction exists
  • removing one line breaks something elsewhere
  • different teams maintain their own copies
  • changes ship untested
  • it carries patches for problems from the past
  • the rules contradict one another

You often end up with logic like:

if customer.is_enterprise:
    prompt += ENTERPRISE_PROMPT

if customer.is_refund_request:
    prompt += REFUND_PROMPT

if customer.is_angry:
    prompt += EMPATHY_PROMPT

if customer.is_new_user:
    prompt += ONBOARDING_PROMPT

Debugging then becomes archaeology. You're no longer debugging one prompt. You're debugging the way prompts get composed.

Composition is where it bites

Dynamic prompts are powerful and easy to get wrong. Take this assembly:

parts = [
    SYSTEM_PROMPT,
    USER_PROFILE,
    RETRIEVED_CONTEXT,
    CONVERSATION_HISTORY,
    USER_QUERY,
]

prompt = "\n\n".join(parts)

It looks fine until you ask what happens when the user profile is stale, when retrieved context contradicts the system prompt, when old conversation that should have been forgotten sneaks in, or when the current request clashes with earlier context. At that point the prompt isn't a string anymore. It's a program generated at runtime, so treat it like one.

Version prompts like code

One of the cheapest improvements teams make is versioning. Rather than an anonymous blob:

SYSTEM_PROMPT = """..."""

give it an identity:

PROMPT_VERSION = "refund_assistant_v12"

Keep prompts in their own files:

prompts/
├── refund_assistant_v10.txt
├── refund_assistant_v11.txt
└── refund_assistant_v12.txt

And log the version on every call:

logger.info(
    "llm_request",
    extra={
        "prompt_version": PROMPT_VERSION,
        "model": MODEL_NAME,
    }
)

Now when output quality suddenly changes you can ask what happened and get a real answer. Without versioning the reply is "I think someone edited the prompt last week," and that's folklore, not engineering.

When fixes mask other problems

Suppose version one scores:

Success rate: 78%

Three prompt tweaks later:

Success rate: 82%

You keep going:

84%

And again:

85%

Then you change the retrieval pipeline, and suddenly:

72%

Why the drop? The prompt gains had been compensating for weak retrieval. You weren't improving the system; you were covering up a different flaw. That's why prompt changes with no evaluation are risky.

Test prompts as you'd test software

Skip eyeballing five examples. Build a dataset:

test_cases = [
    {
        "input": "Can I get a refund?",
        "expected_intent": "refund"
    },
    {
        "input": "I want to cancel my order.",
        "expected_intent": "cancellation"
    },
    {
        "input": "Where is my package?",
        "expected_intent": "shipping"
    },
]

Run the prompt against it automatically:

def evaluate(prompt, cases):
    results = []

    for case in cases:
        output = run_llm(prompt, case["input"])

        results.append({
            "input": case["input"],
            "output": output,
        })

    return results

A prompt edit now works as an experiment, and you can compare results side by side:

Prompt v11
Accuracy: 84%

Prompt v12
Accuracy: 86%

Prompt v13
Accuracy: 78%

That beats announcing that version 13 feels better.

The few-shot habit

Few-shot examples help, and they're addictive. The model flubs a case, so you add an example. Then another. Then another, until you have:

examples = [
    example_1,
    example_2,
    example_3,
    example_4,
    example_5,
    example_6,
    example_7,
    example_8,
    example_9,
    example_10,
]

The prompt balloons, expenses rise, latency grows and the model starts latching onto examples that don't matter. A better question is why this case needed another example at all. Maybe the trouble isn't prompting. Maybe your task's boundaries are simply too wide.

Sometimes less context is the cure

Another lesson people learn painfully: when output disappoints, developers pile on context, meaning more documents, more history, more examples, more instructions. More isn't always better. Sometimes it just creates extra chances to contradict. Picture handing the model:

Document A:
The refund period is 30 days.

Document B:
The refund period is 60 days.

Document C:
Enterprise customers receive special treatment.

Document D:
International purchases follow different rules.

and then asking whether this client gets a refund. That isn't a prompting issue. It's a retrieval and conflict-resolution issue. The model shouldn't pick which policy applies. Your system should.

Business rules don't belong in prose

Here's where the line gets drawn. If a prompt contains something like:

If order_value > 500
and customer_age > 365
and refund_count < 2
and region == "US"
then...

you've got business logic hiding in text. Move it into code:

def can_auto_refund(order, customer):
    return (
        order.value <= 500
        and customer.age_days >= 365
        and customer.refund_count < 2
        and customer.region == "US"
    )

Then tell the model only what it needs:

prompt = f"""
Explain the following decision:

Eligible: {can_auto_refund(order, customer)}

Do not change the decision.
"""

The model now has less freedom, and that's a feature.

The 30-second test

A simple check: can an engineer understand this prompt within 30 seconds? If not, it probably needs refactoring. A good production prompt makes clear what input the model gets, what output it must produce, what it may do, what it must never do and what happens when information is missing. For instance:

SYSTEM_PROMPT = """
You summarize support tickets.

Rules:
1. Use only the provided ticket content.
2. Do not invent facts.
3. Return JSON.
4. Include:
   - summary
   - priority
   - customer_sentiment
5. If information is missing, use null.
"""

Simple, testable, versionable and predictable.

Treat the urge as a signal

When someone says "just add another prompt," ask why the system needs this decision at all. The cause is often one of these: retrieval returns the wrong thing, state is out of date, the data isn't validated, the model is overloaded, the output has no structure, evaluation doesn't exist, or business rules live in natural language. The prompt can paper over every one of those, which is exactly why it gets overused.

What experienced engineers do

Experienced engineers haven't given up on prompts. They've stopped assigning them work that belongs in other layers. A mature setup looks like this:

User Input
    ↓
Validation
    ↓
Intent / Routing
    ↓
Business Rules
    ↓
Retrieval
    ↓
Context Construction
    ↓
Focused Prompt
    ↓
LLM
    ↓
Output Validation
    ↓
Post-processing
    ↓
Response

Each layer has a single job. The LLM remains powerful; it's just no longer responsible for everything.

Prompts shrink as systems grow up

It sounds backwards, since you'd expect sophisticated AI systems to carry giant prompts. Often the healthier outcome is the opposite. A weak system looks like:

Bad system:
Huge prompt
+ lots of examples
+ lots of exceptions
+ lots of business logic

and turns into:

Mature system:
Better inputs
+ better retrieval
+ explicit rules
+ smaller prompt
+ validation
+ evaluation

As the surrounding design does more of the work, the prompt has less to carry. Aim for that.

What it really costs

Most people price a prompt as:

Input tokens × price

The true figure includes much more:

Token cost
+
Latency
+
Debugging time
+
Evaluation complexity
+
Regression risk
+
Maintenance
+
Onboarding
+
Unpredictability

A prompt that saves you twenty lines of code today can cost three days of debugging six months from now. That's the hidden cost.

Last word

Prompt engineering isn't the villain. The mistake is treating it as a stand-in for system engineering. Smart prompts help, focused prompts help more, and reliable pipelines beat both. Next time your model gives an unexpected answer, resist adding one more instruction. First ask whether the model really made a wrong decision. If so, ask whether that decision could be coded, retrieved, validated or orchestrated. You don't want the world's longest prompt. You want a system where the prompt doesn't carry everything, which is the gap between an LLM app that looks clever and one that survives production.