System Prompts
Suppose we are building a math tutor chatbot. A student arrives, anxious and hopeful, and types: “How do I solve 5x + 2 = 3 for x?”
And Claude, catastrophically helpful as always, hands over the complete step-by-step solution. Subtract 2 from both sides, divide by 5, here is your answer, have a lovely day.
The student copies it down, learns nothing, and fails the exam.
What a Tutor Actually Does #
A real tutor gives hints before solutions. A real tutor walks the student through the problem step by step, patiently, and demonstrates with similar problems rather than solving the one on the worksheet.
And there are things a real tutor never does: like blurt out the answer, or tell the student to go use a calculator.
The problem is not what Claude knows. The problem is how Claude behaves. System prompts let us calibrate Claude’s behaviour without changing the student’s question.
The System Prompt #
System prompts guide how to respond. Claude gets these instructions before the conversation begins. You write it as a plain string and pass it to the create call.
const system = `
You are a patient math tutor.
Do not directly answer a student's questions.
Guide them to a solution step by step.
`
Claude will try to respond the way someone in the specified role would respond. It also helps keep Claude on task. So a tutor who stays a tutor, message after message.
const message = await client.messages.create({
model,
max_tokens: 1000,
messages,
system,
});
The first line assigns the role: “You are a patient math tutor”, and the lines after it give specific behavioural instructions. Note what the system prompt does not contain: mathematics. It controls how Claude responds, never what it responds about.
With the tutor prompt, Claude asks: “What do you think would be a good first step to isolate x? Consider what operation we might need to perform on both sides to start moving terms around.”
Same model. Same question.
A More Flexible Chat Function #
Hard-coding the system prompt into our chat function would be a tragedy for reusability.
Instead, we accept it as an optional parameter.
async function chat(
messages: Anthropic.MessageParam[],
system?: string,
): Promise<string> {
const message = await client.messages.create({
model,
max_tokens: 1000,
messages,
...(system ? { system } : {}),
});
const block = message.content[0];
return block?.type === "text" ? block.text : "";
}
The API does not accept an empty or null system prompt. So you must include the system parameter only when you actually have one. The conditional spread ...(system ? { system } : {}) adds the key when a prompt is provided and adds nothing at all when it is not.
Now both worlds are available on demand.
// Without a system prompt
const answer = await chat(messages);
// With a system prompt
const system = `
You are a patient math tutor.
Do not directly answer a student's questions.
Guide them to a solution step by step.
`;
const tutorAnswer = await chat(messages, system);
That is the way.
Repo #
Code exercises set up here