Prompt engineering has acquired an unfortunate reputation. For some, it evokes the image of someone tweaking magic phrases to coax an AI into cooperation. For others, it is a gimmick that will be obsolete once models get smarter. Neither characterisation is accurate. Prompt engineering, done properly, is the practice of giving a language model the context, constraints, and structure it needs to produce reliable, useful outputs. It is closer to writing a good technical specification than to casting spells.
For software development teams using LLMs in their products or workflows, prompt engineering is a practical skill with concrete patterns. Here are the ones that work.
System Prompts: Setting the Stage
The system prompt is the single most important piece of your prompt architecture. It defines the model's role, behaviour, constraints, and output expectations. A good system prompt does for an LLM what a well-written job description does for a new hire -- it establishes context and boundaries so that every subsequent interaction starts from the right place.
A well-crafted system prompt is like a precise technical specification -- it sets context, constraints, and output format.
Effective system prompts share several characteristics:
- They define a specific role. "You are a senior code reviewer focusing on Python best practices and security" produces better results than "You are a helpful assistant." Specificity narrows the model's output distribution toward your desired behaviour.
- They state explicit constraints. What should the model never do? What topics should it decline to address? What format must outputs conform to? Constraints prevent the model from helpfully wandering into territory you do not want it in.
- They describe the output format precisely. If you need JSON, specify the schema. If you need a specific structure, provide it. Ambiguity in output format requirements is the most common source of parsing failures in production systems.
- They are tested and versioned. System prompts are code. Treat them that way.
A Practical Example
Consider a system prompt for a code review assistant. A weak version might say: "Review the following code and provide feedback." A production-quality version defines the review criteria (correctness, performance, security, readability), the feedback format (structured JSON with severity levels, line references, and suggested fixes), exclusions (do not comment on formatting if a linter is configured), and the tone (direct, technical, non-condescending).
The difference in output quality between these two approaches is substantial, and it is entirely a function of how much context and structure you provide in the system prompt.
Few-Shot Prompting: Teaching by Example
Few-shot prompting means including examples of desired input-output pairs in your prompt. It is one of the most reliable techniques for improving output quality and consistency, particularly for tasks where describing the desired behaviour in words is harder than showing it.
When you struggle to describe what you want in words, show it in examples. Models learn from examples at least as well as from instructions.
For software teams, few-shot prompting is particularly useful for data extraction (show examples of how different document formats map to your target schema), classification (show examples of how different inputs map to categories), and code generation (show examples of the coding style, patterns, and conventions you expect).
Practical tips for few-shot prompting:
- Include 2-5 examples. More is not always better -- too many examples consume context window and can cause the model to over-fit to the example patterns.
- Choose diverse examples that cover the range of inputs the model will encounter, including edge cases.
- If outputs have a specific format, ensure all examples use that exact format consistently.
- Order matters. Put the most representative example first and the most unusual one last.
Your system prompt has the highest leverage of any component in your prompt architecture.
Chain of Thought: Thinking Step by Step
Chain of thought (CoT) prompting asks the model to show its reasoning before producing a final answer. For tasks that require multi-step logic -- debugging, analysis, planning, complex code generation -- this technique significantly improves accuracy.
The mechanism is straightforward: by generating intermediate reasoning steps, the model maintains a coherent logical thread rather than jumping directly to a conclusion. This reduces the chance of errors in complex reasoning chains and makes the output auditable -- you can see where the model's logic went wrong when it does.
In production systems, you can use CoT in two ways. The first is visible CoT, where the reasoning is part of the output and is shown to the user or logged for debugging. The second is hidden CoT, where you instruct the model to reason step by step but then extract only the final answer for the user-facing output. Hidden CoT is useful when you want the accuracy benefits without exposing the reasoning process.
When to Use Chain of Thought
- Complex code analysis where the model needs to trace data flow or execution paths.
- Debugging tasks where the model should systematically eliminate potential causes.
- Multi-criteria evaluation where multiple factors must be weighed.
- Planning tasks where the model needs to consider dependencies and ordering.
For simple, pattern-matching tasks (formatting, basic extraction, straightforward generation), CoT adds latency and cost without improving quality. Use it selectively.
Structured Outputs
If your application consumes model output programmatically -- and most production applications do -- you need structured output. Parsing natural language responses is fragile and error-prone. Structured output (JSON, XML, or any consistent format) is parseable, validatable, and reliable.
Most frontier models now support native structured output modes that constrain the model to produce valid JSON conforming to a specified schema. Use this feature when available. It eliminates an entire category of parsing errors.
When native structured output is not available, you can achieve similar results through prompt engineering:
- Provide the exact JSON schema you expect in the system prompt.
- Include a few-shot example showing the expected output format.
- Add an explicit instruction: "Respond with valid JSON only. Do not include any text before or after the JSON object."
- Validate the output against your schema and retry on failure (with the validation error included in the retry prompt).
Version and test prompts with the same rigour you apply to production code.
Handling Edge Cases
Edge cases in prompt engineering are inputs that fall outside the model's expected operating range. The model might encounter an input in a language it was not designed for, a document format it has never seen, or a question it cannot answer from the provided context.
The worst outcome is a confident-sounding wrong answer. The best outcome is a clear signal that the input is outside the system's capabilities. Your prompts should explicitly handle edge cases.
- Define what to do when uncertain. "If you are not confident in your analysis, indicate this with a confidence score below 0.5 and explain what additional information would be needed."
- Handle missing information explicitly. "If the document does not contain the requested information, return null for that field rather than guessing."
- Set boundaries. "If the input is not a Python code file, respond with an error object indicating the expected input type."
These instructions seem obvious, but without them, models default to being maximally helpful -- which means producing plausible-sounding output even when the correct response is "I do not have enough information to answer this."
The most dangerous failure mode of an LLM is not an error. It is a confident, plausible, wrong answer. Design your prompts to make the model say "I don't know" when appropriate.
Prompt Versioning
In a production system, prompts change over time. New edge cases are discovered. Requirements evolve. Models are updated and behave differently. Without versioning, these changes are untracked, untested, and irreversible.
At Pepla, we version prompts with the same discipline we apply to code:
- Each prompt has a unique identifier and semantic version number.
- Changes are reviewed through pull requests, with before/after evaluation results.
- Prompt versions are tagged in production logs, so outputs can be traced back to the exact prompt that produced them.
- Rollback is a configuration change, not a code deployment.
- Evaluation datasets are maintained alongside prompts, ensuring that every prompt version has been validated against representative inputs.
This may sound like overhead, but it pays for itself the first time a prompt change causes a regression in production. Without versioning, diagnosing and reverting the change is a scramble. With versioning, it is a routine operational procedure.
Common Anti-Patterns
A few patterns to avoid:
- The megaprompt. Cramming every instruction, constraint, example, and edge case into a single enormous prompt. Long prompts dilute the model's attention. Split complex requirements into multiple focused prompts in a pipeline instead.
- Vague quality adjectives. "Write high-quality code" is meaningless. "Write code that follows PEP 8, includes type hints for all function signatures, and handles potential exceptions with specific error messages" is actionable.
- Ignoring the model's tendencies. Each model has behavioural tendencies. Some are verbose. Some over-explain. Some default to certain patterns. Learn your model's tendencies and address them in your prompts rather than hoping they will not surface.
- Optimising prompts on a single example. A prompt that works perfectly for one input may fail on the next. Always evaluate across a diverse set of examples before declaring success.
Evaluate prompts across diverse examples, not just the convenient ones -- edge cases reveal the real quality.
Practical Takeaways
- Invest time in your system prompt. It has the highest leverage of any single component in your prompt architecture.
- Use few-shot examples for any task where showing is easier than telling.
- Apply chain of thought for complex reasoning tasks; skip it for simple pattern matching.
- Demand structured output for any programmatically consumed response.
- Explicitly handle edge cases and uncertainty in your prompts.
- Version and test prompts with the same rigour you apply to code.
- Evaluate across diverse examples, not just the convenient ones.




