35–45 min read · Hands-on coding
An LLM is a capable new employee who knows almost everything but has no context about your specific situation. If you give vague instructions, you get vague results. If you give clear, structured instructions, you get consistent, production-worthy outputs. The craft of writing those instructions is prompt engineering — and it is one of the highest-leverage skills an AI engineer can have.
A well-crafted prompt can turn a mediocre model response into one that’s correct 90% of the time. A badly-crafted one will frustrate you into thinking the model is broken when the instructions were the problem all along.
LLMs do not infer intent from vague requests — they pattern-match to the most likely response for the exact words you used. If your words are ambiguous, the output will reflect that ambiguity.
Treat the model like a brilliant contractor who has just joined your team. They are capable of extraordinary work but have no knowledge of your organisation, your standards, or what “good” looks like for your use case. You must be explicit.
| Vague (bad) | Clear (good) |
|---|---|
| “Write a summary.” | “Write a 3-sentence summary for a non-technical executive. Focus on business impact, not technical details.” |
| “Fix my code.” | “Find and fix the bug in this Python function. Return only the corrected function, no explanation.” |
| “Translate this.” | “Translate the following from English to French. Preserve the formal register.” |
Every adjective you add is a constraint. Every constraint narrows the output distribution. Narrower distributions produce more consistent results.
The system message is not just a nice-to-have — it is where you define who the model is for the entire session. Use it to set:
A well-structured system prompt for a customer support bot:
system_prompt = """You are Aria, a friendly customer support agent for Acme Software.
Your job:
- Answer questions about Acme's products (pricing, features, integrations).
- Help customers troubleshoot common issues.
- Escalate complex problems by saying "I'll connect you with our support team."
Rules:
- Never make up product features that are not in the context provided.
- Never discuss competitors by name.
- Keep replies under 150 words.
- Always end with: "Is there anything else I can help you with?"
Tone: warm, professional, direct."""
Notice how the prompt separates job description, rules, and tone. This structure is easy to iterate on — you can tweak just the rules without touching the persona.
When your prompt contains multiple distinct pieces of information — a document, a question, some context, some instructions — the model can mix them up or give one piece more weight than intended.
Wrapping each piece in XML-style tags makes the structure unambiguous:
user_message = """
<document>
{document_text}
</document>
<question>
{user_question}
</question>
Answer the question using only information from the document.
If the answer is not in the document, say "I don't know."
"""
The model has seen enormous amounts of XML and HTML during training, so it correctly interprets these as structural separators. This approach is especially useful when injecting retrieved documents into a RAG system (Chapter 4) or when passing code alongside instructions.
You can also ask the model to respond using XML tags:
"Respond using this format:
<answer>Your answer here</answer>
<confidence>high | medium | low</confidence>"
This is a lightweight alternative to full structured outputs when you just need a few fields — you parse the tags yourself with a simple string extract.
Sometimes the clearest way to show the model what you want is not to describe it in words, but to show it examples of correct input-output pairs. This is called few-shot prompting — “few-shot” means you are providing a few examples.
Few-shot is particularly effective for:
The examples go in the messages list as user/assistant pairs, before the real user message:
messages = [
{"role": "system", "content": "Classify the sentiment of each review as: positive, neutral, or negative."},
# Example 1
{"role": "user", "content": "This product changed my life. Absolutely love it."},
{"role": "assistant", "content": "positive"},
# Example 2
{"role": "user", "content": "Works fine, nothing special."},
{"role": "assistant", "content": "neutral"},
# Example 3
{"role": "user", "content": "Broke after two days. Total waste of money."},
{"role": "assistant", "content": "negative"},
# Real input
{"role": "user", "content": "Decent quality, but the shipping took three weeks."},
]
The model sees the pattern — single-word reply, no explanation — and applies it consistently to the real input. Without the examples, it might reply with a full sentence like “The review appears to be neutral in sentiment.”
Three examples is usually enough. More than five rarely helps and increases token cost. Choose examples that cover edge cases, not just easy cases.
LLMs generate text token by token. When they produce an answer immediately, they have committed to it before “reasoning” about it. Asking the model to show its reasoning before giving the final answer — known as chain-of-thought (CoT) prompting — forces it to work through the problem step by step, which consistently improves accuracy on complex tasks.
The simplest form: add “Think step by step” to your prompt.
| Without CoT | With CoT |
|---|---|
| “Is this contract clause legally enforceable? Answer yes or no.” | “Is this contract clause legally enforceable? Think through the relevant legal considerations step by step, then give your final answer.” |
For production use, you often want the reasoning hidden from the user and only the final answer surfaced. Use XML tags for this:
system_prompt = """When answering complex questions:
1. First, think through the problem inside <thinking> tags.
2. Then give your final answer inside <answer> tags.
The user will only see the answer tag."""
Then strip the <thinking> block from the response before showing it to the user. The model gets to reason; the user gets a clean answer.
CoT is most valuable for: multi-step reasoning, maths, logic puzzles, legal or medical analysis, and any task where a fast guess is worse than a slower, considered response. For simple classification tasks or extraction, CoT adds tokens without improving accuracy.
| Situation | Technique |
|---|---|
| Output is vague or inconsistent | Be more specific; add constraints to system prompt |
| Output format is wrong | Add format examples (few-shot) or use XML tags |
| Multi-part input is getting confused | XML tags to separate sections |
| Accuracy on complex tasks is low | Chain-of-thought |
| Classification is inconsistent | Few-shot with representative examples |
| Model invents facts | Restrict to provided context; add “if not in context, say I don’t know” |
Always start with the simplest technique and add complexity only if it helps. A clear system message often solves the problem on its own. Reach for few-shot or CoT when basic clarity is not enough.
<document> and <question> tagsAnswer each question from memory, then click to reveal.
It is easier to demonstrate a format than to describe it. Written instructions can be ambiguous or interpreted in unexpected ways. An example shows the exact pattern — length, tone, structure — unambiguously. The model pattern-matches to examples more reliably than it follows abstract descriptions.
→ Few-shot promptingCoT prompting instructs the model to work through the problem step by step before giving its final answer. It is worth the extra tokens for complex reasoning tasks — multi-step maths, logic, legal/medical analysis — where a fast guess is likely wrong. For simple extraction or classification, CoT does not improve accuracy and is not worth using.
→ Chain-of-thoughtThe system message controls persona, tone, domain restrictions, output format, and rules that apply for the whole session. It does not provide a hard security boundary — a determined user can often override it with clever prompting. It is effective for steering normal use, not for enforcing security constraints.
→ System promptsMultiple pieces of raw text in a prompt can blur together — the model may give some sections undue weight or confuse which part is the instruction and which is the data. XML tags make the boundaries explicit and unambiguous, because the model has learned to treat them as structural separators.
→ XML tags