DEV Community

Multigrid
Multigrid

Posted on • Originally published at multigrid.ai

Why DeepSeek-R1 Ignores the System Prompt

There is no error. The request returns 200, the system message is accepted, and the model answers as though you had not sent one. That combination — accepted and ineffective — is what makes this hard to debug, and it is not the same failure as an unsupported role.

The symptom

The shape is always the same. A system message that works perfectly on a non-reasoning model — “Always answer in Dutch”, “Never use bullet points”, “You are a terse assistant that replies in under 40 words” — is sent to R1, and the answer comes back in English, in bullet points, at length. No 400, no warning field, nothing in usage to suggest the message was dropped. The tokens were charged, so it was in the prompt.

The first thing to rule out is the thing that is genuinely an error: echoing a previous turn’s reasoning_content back in messages returns a 400, and some conversation code that fails that way retries without the offending fields and quietly drops other parts of the history too. If your 200 responses are preceded by 400s in the log, fix that first — the rule about not replaying the trace is the cause.

What DeepSeek actually documents

The widely repeated claim is that R1 “does not support the system role”. That is not what the documentation says, and believing it sends you down the wrong path. The DeepSeek-R1 model card gives a short list of usage recommendations, and among them is the advice to avoid a system prompt and to place all instructions in the user prompt instead. It is guidance about what works, published by the people who trained the model, not a schema restriction.

The distinction is practical, not pedantic. If the role were rejected you would get an error and a clear fix. Because it is accepted, the system message is doing something — it is in the context, it is influencing the distribution — just far less than you expected. That means the fix is about placement and weight, not about finding a supported field.

The same model card is where the other well-known R1 recommendations live: a temperature range rather than a default, and a note about forcing the response to begin with a thinking token. It is a short document and it is worth reading in full once, because it explains several behaviours that otherwise look like bugs.

Why the reasoning phase swamps it

R1 does not answer your prompt. It generates a long reasoning trace conditioned on your prompt, and then generates an answer conditioned on your prompt and that trace. By the time the first token of the answer is produced, the immediate context is thousands of tokens of the model’s own text, and your system message is far behind it.

This is the mechanism, and it explains the exact pattern people report. Style and format instructions fail hardest, because style is decided token by token at generation time and the strongest local signal is the register of the trace that precedes it. Instructions about what to do — solve this, use this data, ignore that constraint — survive better, because they shape the reasoning itself and the reasoning is what the answer is drawn from.

It also explains why the failure is intermittent rather than absolute. Traces vary in length and content between runs, so how much material sits between your instruction and the answer varies too. A prompt that obeys the system message on a short trace and ignores it on a long one is the same prompt behaving consistently with this mechanism — why the trace varies at all is a separate page.

What the chat template does with it

If you are running the open weights rather than the API, there is a second thing going on. The system message is not a protocol-level concept at the model; it is text placed by the chat template in tokenizer_config.json, using DeepSeek’s own role tokens. What that template does with a system message — where it puts it, whether it wraps it, whether it prepends anything of its own — is a property of the checkpoint you downloaded.

from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("deepseek-ai/DeepSeek-R1")
print(tok.apply_chat_template(
    [{"role": "system", "content": "Answer in Dutch."},
     {"role": "user", "content": "What is 2+2?"}],
    tokenize=False, add_generation_prompt=True))
Enter fullscreen mode Exit fullscreen mode

Print that before you theorise. You will see exactly where your text landed, and you will see the generation prompt the template appends at the end. If your instruction is at the very top of a long template and the generation prompt is thousands of tokens away by the time the answer starts, you have just watched the mechanism in the previous section happen in one screenful.

Confirming that is what is happening

Before changing prompts, establish that the instruction is being diluted rather than misunderstood. The test is a small matrix: the same instruction in three positions, run several times each, scored by whether the answer obeyed it.

positions = {
    "system":     lambda q, i: [{"role": "system", "content": i},
                                {"role": "user", "content": q}],
    "user_first": lambda q, i: [{"role": "user", "content": i + "\n\n" + q}],
    "user_last":  lambda q, i: [{"role": "user", "content": q + "\n\n" + i}],
}

INSTRUCTION = "Give the final answer in Dutch."
QUESTION = "Why does salt lower the freezing point of water?"

for name, build in positions.items():
    obeyed = 0
    for _ in range(5):
        r = client.chat.completions.create(
            model="deepseek-reasoner",
            messages=build(QUESTION, INSTRUCTION),
            max_tokens=8192,
        )
        obeyed += is_dutch(r.choices[0].message.content)
    print(f"{name:12} {obeyed}/5")
Enter fullscreen mode Exit fullscreen mode

Five runs per cell is enough to see a difference between “never” and “usually”, which is the only resolution the decision needs. Run it more than once if the counts come out close — the trace varies between runs, so a single sample of one cell tells you very little on its own.

Two results are worth interpreting rather than just recording. If the system position scores zero and the user positions score well, you have confirmed dilution and the fix in the next section applies directly. If all three score badly, the instruction itself is the problem — it is ambiguous, or it conflicts with something else in the prompt — and moving it will not help.

Where to put the instruction instead

  1. Move it into the user turn. This is DeepSeek’s own recommendation and it is the fix that works most often. Concatenate the instruction with the question in a single user message rather than sending two messages.
  2. Put it last, not first. Question first, constraint immediately before the end of the user turn. Recency is doing real work here; an instruction at the top of a long user message competes with the same distance problem as a system message.
  3. Constrain the answer, not the thinking. Phrase it as a property of the final response — “Give the final answer in Dutch, in one sentence” — rather than as a global rule. The trace is allowed to be in whatever register it likes; you only care about what comes after the closing of the reasoning phase.
  4. Enforce format outside the model where you can. If the requirement is machine-checkable — JSON, a word limit, a language — validate the answer and retry rather than trusting the instruction. For structured output specifically, the non-reasoning endpoint with JSON output mode is a stronger guarantee than any wording.
  5. Split the work if the constraint is non-negotiable. Reason with R1, then pass its answer to the chat model with your system prompt for formatting. Two calls, and the second is cheap because the hard thinking is already done.

What does not work, and is worth naming so you do not spend an afternoon on it: repeating the system message before every user turn, shouting in capitals, or wrapping it in tags. All three add tokens at the same distance from the answer and change nothing about the mechanism.

Related

Top comments (0)