Rewriting the prompt is the wrong reflex

João RangelAugust 12, 202611 min read

In this article

Yo Claude

Chapter 04 of the official Anthropic prompt engineering tutorial opens with a prompt that anyone has written on a hurried day:

  Yo Claude. {EMAIL} <----- Make this email more polite...

The response that comes back starts like this:Dear Claude. The notebook points out the diagnosis:“Claude thinks ‘Yo Claude’ is part of the email it’s supposed to rewrite! You can tell because it begins its rewrite with ‘Dear Claude’.”The greeting that was an instruction was read as data, because nothing in the text indicated where the email started and where it ended.

Rewriting is the wrong reflection. The symptom indicates which structural piece is missing.

The piece here is a tag. The same three lines, one more markup:

  Yo Claude. <email>{EMAIL}</email> <----- Make this email more polite...

After that, the output stops greeting you. The best practices doc recommends wrapping each type of content in its own tag, with names like<instructions>,<context>,and<input>, because the model has no way to infer a boundary that no one drew. Those responsible for governance should read this defect with more appreciation. Instruction and third-party content in the same block of text, without a delimiter, is the surface where unreliable input becomes a command. In a tutorial, this costs a malformed email, but in a flow that processes customer documents, it is through this same gap that third-party content becomes injection.

Seventy percent

The case that carries the rest of the post is in the official cookbook , a classifier that fits insurance support tickets into 10 categories, some purposefully confusing with each other, such asBilling InquiriesandBilling Disputes. The notebook measures the same problem three times without changing the model (claude-haiku-4-5), the temperature (temperature=0.0), or the test set, the set of 68 hand-labeled tickets that serves as a benchmark. What changes from one round to the next is a structural piece.

The first version has no examples at all and doesn’t even have a system prompt, that block of instructions that stays outside the conversation and applies to the entire session. None of them pass this parameter, and everything lives in the user turn, within the message that the code sends:

  You will classify a customer support ticket into one of the following categories:
<categories>
    {{categories}}
</categories>
...
Respond with just the label of the category between category tags.

In the call, two more pieces come in, outside the prompt text. One is the prefill, in which you begin the model’s response with it, and the piece written in its place is the"<category>"that forces the continuation to be a label. The other isstop_sequences=["</category>"], the string that makes the generation stop as soon as it appears, cutting off anything that would come after. With just this, accuracy reaches ~70%, compared to a random baseline of ~10%, which is the floor of a uniform guess across 10 categories.

The last line of the template complicates a lazy reading of the thesis, because“Respond with just the label of the category between category tags”is an output restriction written in common words and has been there from the beginning. Prose works. What it doesn’t do alone is fix the form of the response, and what fixes it is the prefill along with the stop sequence. These two pieces live in the call code, they enter a diff, and someone approves them in a PR, while “we ask to be more direct” is not revisable.

It is worth noting the limit of the case, because the notebook states that“the problem definition, data, and labels used in this example were synthetically generated by Claude 3 Opus”. Material generated this way tends to be better-behaved than real tickets and gives a more generous accuracy ceiling. The progression between rounds remains informative, since all that changes between them is the piece, but the absolute number is not a promise for your domain.

What if it's just rewriting?

The objection is fair, and it is the first that occurs to anyone. The prompt doesn’t need a new piece; it just needs to be clearer. Perhaps, and the cookbook has a way to measure that.

RoundWhat changed in the promptAccuracyRandom baselineNone, guessing in 10 categories~10%simple_classifytags, prefill, stop sequence~70%rag_classify+ 5 labeled examples in<examples>94%rag_chain_of_thought_classify+<scratchpad>97%

Between the second and third rows of the table, the model, temperature, test set, and amount of instruction in prose do not change. What comes in are five labeled examples, pairs of ticket and correct category pasted inside the prompt itself for the model to see how the task is solved before trying, which is called few-shot. And they are not just any five. They are retrieved by similarity: the system returns the most similar examples to the ticket being classified. Chosen this way, they are worth more than five fixed ones for the most banal reason, which is that the billing ticket comes accompanied by billing cases and not by claims. This exchange is worth 24 accuracy points .

The notebook records the jump like this,“RAG boosted our accuracy from ~70% to 94%”, and then“Chain-of-thought reasoning pushed our accuracy to 97%”. This last jump comes from the<scratchpad>, a space in the prompt where the model writes the reasoning before the final response, what is called chain-of-thought and costs tokens in every call. The recommendation from the doc to include three to five examples has this number behind it.

In theevaluation/directory of the same case, a Promptfoo configuration sweeps the temperature at five points between 0.0 and 0.8, to see how much the result depends on this button. Temperature is how much the model allows itself to vary; at 0.0 it is as predictable as possible. The simple oscillates between 69.1% and 72.1%, the chain-of-thought between 94.1% and 95.6%, and the RAG drops from 94.12% at T=0.0 to 89.71% at T=0.8. The temperature does affect the number, yes, but 4.4 points in the worst-case scenario against the 24 that the piece bought determine where it is worth spending the afternoon.

For those who need to account for what changed, “we improved the prompt” dies on the first follow-up question, while “we added five labeled examples and accuracy went from ~70% to 94% in the same test set of 68 tickets” survives an audit. And it survives especially in the inverse situation, when accuracy declines and someone needs to know what to revert.

Three corrections, one symptom

In exercise 7.1 of the same tutorial, four support emails need to turn into A, B, C, or D, and the one checking the correctness is just one line of code:

  re.search(ans, response[-1])

The parser is the piece of code that reads the model’s response and extracts the data of interest from it. This one only looks at the last character, which needs to be the letter of the category. Any “Sure! The category is B, because the customer…” breaks the parser by carrying the correct classification within the sentence. In exercises 6.1 and 6.2, from the previous chapter, the checker is more tolerant and looks for the pattern in the entire response; the relentlessness that matters here is that of 7.1.

Thehints.pystores two official solutions for this set, separated by the cause they attack. The one for 6.1 solves by output restriction, and there are two pieces, not a phrase.

  Do not include any extra words except the category.
  # prefill of the assistant turn
"("

The one for 7.1 solves by few-shot, with a block<examples>bringing three complete question and answer pairs, plus the prefill"The correct category is:". Compared to the one for 6.1, it’s a whole different prompt. The 6.2 stays in the middle without a written solution, only with the statement that asks“your favorite output formatting technique to make Claude wrap JUST the letter… in<answer></answer>tags”.

Notice what solution 6.1 has that is uncomfortable because it starts with “Do not”, the framing that the doc advises against (“tell Claude what to do instead of what not to do”). The official material solves the exercise by disobeying its own Anthropic recommendation and the output comes out right anyway, because what saved the response was the prefill(opening the form in place of the model. Hence comes the gauge, which is to read the wrong output and install the piece that closes the boundary that the model had no way to see. When each correction is labeled by the cause, it's possible to write a prompt changelog that someone still understands six months later, and without this, every change becomes “draft adjustment” in the commit description.

And the bill?

About bloated prompts inflating the response and the cost per call, there is no public number. The documentary support is qualitative and nothing beyond that. The doc observes that the formatting style of the prompt can influence the style of the response and gives as an example that removing markdown from the prompt tends to reduce the volume of markdown in the output. From there on, it’s worth as an impression of those operating prompts in production, with the weight that an impression has. Measure on your own account before taking it as fact.

The hard number stays on the other side, with structured outputs. The doc states it without euphemism. Using a schema, the formal description of the format the response needs to have, makes the API inject an additional system prompt explaining that format to the model. This increases the input token count in every call. And the first request with a new schema still pays the latency of compiling the grammar, which then stays cached for 24 hours since the last use. Measuring output tokens per call along with accuracy is the minimum defensible because costs that no one measures only appear in the end-of-month closure.

Don’t structured outputs solve this?

It’s worth reconstructing the objection in its best version, that of the own documentation . Structured outputs restrict generation token by token against a compiled grammar, meaning the API translates the schema into a set of rules and, at each step, only allows the model to choose from the continuations that keep the response within the format. The promise is literal on the page,“Always valid: No more JSON.parse() errors”and“No retries needed for schema violations”. If the output already comes guaranteed at this layer, writing format instructions by hand becomes manual work for a problem that the API solves at the right layer, and whoever makes this argument has the doc on their side.

The counter-argument lives on the same page, and that’s why it’s worth it. Two lines of output, both valid against the same schema:

  {"category": "Billing Disputes"}
{"category": "Billing disputes"}

“Structured outputs don’t guarantee the capitalization of string enum and const values.”The enum is the closed list of values that the field accepts. The doc is specific about where the divergence usually falls,“typically in the first letter of a word following a space”, which affects labels like those of this classifier and recommends comparing enum values in a case-insensitive manner. A downstream router that compares exact strings breaks with the two lines above parsing perfectly. There is still a second hole. The API returns instop_reasonthe reason it stopped generating, and when it isrefusalormax_tokens, the doc warns that the output may not match the schema. Schema and prompt piece operate in different layers. And since “the schema guarantees” is the phrase that makes a team disable downstream validation, continuing to checkstop_reasonafter adopting structured outputs is necessary code, not diligence.

In the end

Diagnosing by cause only works if there is a test set and someone willing to run the measurement. The cookbook has 68 labeled tickets exactly for this reason, and putting together a set like this is the boring work that the method demands upfront. Below a handful of calls per day, or in a prototype that will still change shape next week, rewriting by eye is cheaper and probably right. The gauge works when there is something downstream that breaks, a router or a report that someone signs off. And the leading case has the limit already declared, with synthetic data and labels and a more generous ceiling than the real.

How many labeled examples would you have today if you needed to prove tomorrow that the last change to your prompt improved something?

Keep learning with Hcode

Sign up for the course platform and follow the next blog guides.