How to write system prompts that actually work

Most system prompts fail not because of the model but because of the wording. Here is what actually changes behavior and what models just ignore.

3 min · · Updated · development · промпты

A system prompt like "You are a helpful assistant, answer briefly and to the point" does nothing — the model already tries to be helpful by default. The difference between a prompt that actually changes behavior and one the model silently ignores comes down to specificity: hard constraints, format examples, and explicit if-then conditions work; vague wishes do not.

Request format for a system prompt

In the Anthropic Messages API, the system instruction is passed as a separate system field, not as a message inside the messages array:

bash
curl https://api.zukko.pro/v1/messages \
  -H "Authorization: Bearer fetch_your_key" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-5",
    "max_tokens": 500,
    "system": "Answer only based on the provided context. If the answer is not in the context, write: \"There is no answer to this question in the provided data.\"",
    "messages": [{"role": "user", "content": "Context: ... Question: ..."}]
  }'

In OpenAI Chat Completions, the system instruction is a separate message with role system at the start of the messages array:

bash
curl https://api.zukko.pro/v1/chat/completions \
  -H "Authorization: Bearer fetch_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.6-sol",
    "messages": [
      {"role": "system", "content": "Respond only with a JSON object of the form {\"category\": string, \"confidence\": number}. No text outside the JSON."},
      {"role": "user", "content": "Classify this review: Delivery was a week late"}
    ]
  }'

Mixing up these fields is a common mistake when porting code between Anthropic and OpenAI-compatible clients — for one, system is a top-level parameter; for the other, it is a role inside messages.

Specificity over vague wording

"Answer briefly" gets interpreted differently by the model each time — three sentences for one task, a full paragraph for another. A constraint with a number or a structure works instead:

Bad: Answer briefly and to the point.

Good: Answer in no more than 3 sentences. No preambles or hedging.

The same applies to format. "Return JSON" sometimes gets wrapped by the model in a text explanation or a ```json markdown block. If you need bare JSON with no wrapper, say so explicitly and show an example:

Return only a JSON object, no markdown formatting, no explanation before or after. Format: {"result": "...", "score": 0.0}

Explicit conditions instead of wishes

"If you're not sure, better not to answer" is too vague. The model has no idea what confidence threshold is meant. An explicit condition with a concrete action works instead:

If the context has no direct answer to the user's question, return exactly: "Not enough data to answer." Do not invent facts that are not in the context.

The second line here is not redundant: without an explicit ban on "filling in" facts, the model can blend context facts with general knowledge, especially on a topic it already knows well.

Role and boundaries, not personality

A system prompt often tries to describe a "personality" — "you are a friendly expert in X." That works worse than describing task boundaries: what the model should do, what it should not, and in what format it should answer. An example of a working prompt for a narrow task:

You process support tickets. Task: determine category (billing, technical, account, other) and priority (low, medium, high). Rules: - billing — anything about payments, invoices, refunds - technical — product malfunctions - account — access, passwords, profile settings - high priority — if the text contains "not working", "lost access", "urgent" Return JSON: {"category": "...", "priority": "..."}

Explicit categorization rules (which words map to which category) cut down random errors far more than any description of "you are a careful classifier."

Testing on edge cases

A prompt that looks solid on three examples often breaks on the fourth one — empty input, input in another language, input with typos. Keep a set of edge cases and run the prompt against them on every change:

bash
import requests

test_cases = [
    "Delivery was a week late",
    "",
    "asdkjaslkdj",
    "Everything's great, thanks!",
    "WHY WAS I CHARGED TWICE????",
]

for text in test_cases:
    resp = requests.post(
        "https://api.zukko.pro/v1/chat/completions",
        headers={"Authorization": "Bearer fetch_your_key", "X-Zukko-Cache": "true"},

The X-Zukko-Cache: true header enables free caching on repeated runs of the same test set, which matters when a prompt gets revised dozens of times. temperature: 0 here is for deterministic testing (and also enables the cache), but only works for models that accept the parameter: GLM does, while claude-sonnet-5 and claude-opus-5-5 may reject a temperature other than 1 with a 400 — don't pass temperature to them.

Free prompt length check

Before running a prompt against every test case, it helps to check once how many tokens it takes — especially if the system instruction is long and includes few-shot examples. The token-counting endpoint is free and does not execute the request itself:

bash
curl https://api.zukko.pro/v1/messages/count_tokens \
  -H "Authorization: Bearer fetch_your_key" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-5",
    "system": "Long system instruction with examples...",
    "messages": [{"role": "user", "content": "test question"}]
  }'

This helps catch a bloated prompt before it starts eating into context on every request — with a lot of few-shot examples, the system instruction alone can take up a noticeable share of the context window on long conversations (1,000,000 tokens for Claude and GPT, 128K for GLM).

Different models, different sensitivity to wording

Prompts for Claude are often written with XML-like markup (<context>...</context>), and for GPT-5.6 and GLM as numbered rule lists; which works better in your case, test on your own tasks. If a prompt gets ported between models through the same key — which is convenient since the protocol is shared — test it separately on each model rather than assuming wording that works on claude-sonnet-5 will produce the same result on gpt-5.6-sol unchanged.

Bottom line

A working system prompt comes down to concrete constraints (numbers, formats, rule lists), explicit if-then conditions instead of general wishes, and testing against edge cases rather than three convenient examples. Free token counting through /v1/messages/count_tokens and caching via the X-Zukko-Cache: true header make prompt iteration cheap, so there is no reason to skimp on test runs before shipping a prompt to production.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Writing system prompts that actually work · Zukko