Cet article est publié en anglais.
Context, Constraints, Output: A Prompt Template for Reliable AI Code
Vague prompts give vague code. Use a three-part template of context, constraints and output format to get production-ready functions on the first try.
Ask an AI assistant to "write a function that validates user input" and the reply is often code-shaped text: it runs, it has a signature, and it does nothing you needed. There's no error handling, and no hint of what "valid" was supposed to mean.
That result is usually blamed on the model, but most of the time the prompt is at fault. Treat a prompt like an API contract instead of a casual request and output quality jumps, and the model hasn't gotten any smarter. It just stopped being forced to guess. What follows is a three-part structure that fixes the pattern.
Why vague prompts give vague code
With a loose request, the model fills every gap with an assumption, and those assumptions rarely match yours. Which language version? What's your team's error handling convention? Must it be safe under concurrent access? Should failure return an error or panic? A one-line request answers none of this, so the model falls back on the most common pattern it has seen. For a real codebase, that common pattern is frequently the wrong one, and the gap only shows up in review or, worse, in production.
There's no magic phrase that fixes it. The remedy is supplying what a new engineer would be told on day one: context, constraints and a clear definition of done.
The three sections
Every code-generation prompt breaks down into three parts, in this order.
- Context describes where the code will live: language and version, framework, patterns it has to match, and dependencies already in use.
- Constraints list the rules that aren't negotiable, such as error handling style, performance targets, concurrency safety, input validation, and what to avoid (no external libraries, no global state, and so on).
- Output format says how the answer should arrive. Only the function, or the function plus a usage example? Comments or none? Tests included?
Leave one out and things go wrong. No context gives code that doesn't fit your stack. No constraints gives code that "works" but ignores your conventions. No output format wraps three paragraphs of explanation around the fifteen lines you wanted.
Same task, two prompts
A concrete comparison makes the difference easy to see, so here's one task written both ways.
The vague version of a request looks like this:
Write a function to process a batch of orders.
Now the structured version:
Context: Go 1.22 service, part of an order-processing microservice.
Uses the existing Order struct (ID string, Amount float64, Status string).
Errors are wrapped with fmt.Errorf and %w, never panicked.
Constraints:
- Process orders concurrently, max 10 goroutines at a time
- If any single order fails, log it and continue (don't abort the batch)
- Return a slice of errors for failed orders, not a single error
- No third-party libraries, standard library only
Output format: just the function and a short usage example, no explanation.
On the first try, it produced this:
func ProcessOrders(ctx context.Context, orders []Order, process func(Order) error) []error {
sem := make(chan struct{}, 10)
var wg sync.WaitGroup
var mu sync.Mutex
var errs []error
for _, o := range orders {
wg.Add(1)
sem <- struct{}{}
go func(o Order) {
defer wg.Done()
defer func() { <-sem }()
if err := process(o); err != nil {
mu.Lock()
errs = append(errs, fmt.Errorf("order %s: %w", o.ID, err))
mu.Unlock()
}
}(o)
}
wg.Wait()
return errs
}
// usage
errs := ProcessOrders(ctx, orders, chargeOrder)
for _, e := range errs {
log.Println(e)
}
That's usable as it stands. The prompt wasn't clever. It simply removed each ambiguity the model would otherwise have settled alone: the concurrency limit, behavior on failure, the error format and the dependency scope.
It works in any language
The template isn't tied to one language. These are the same three sections on a different stack:
Context: Python 3.11 data pipeline script, uses pandas, runs as a scheduled job.
Constraints:
- Validate that required columns exist before processing, raise ValueError with column name if missing
- Handle empty DataFrames without crashing (return empty result, log a warning)
- No mutation of the input DataFrame
Output format: function only, type hints included, no docstring needed.
And the result:
import pandas as pd
import logging
def clean_transactions(df: pd.DataFrame, required: list[str]) -> pd.DataFrame:
missing = [col for col in required if col not in df.columns]
if missing:
raise ValueError(f"missing required column(s): {', '.join(missing)}")
if df.empty:
logging.warning("received empty DataFrame, returning empty result")
return df.copy()
result = df.copy()
result = result.dropna(subset=required)
return result
The syntax differs but the discipline is identical. It works across languages because it targets the model's real failure mode, which is filling gaps with generic defaults in place of yours.
Does the structure actually help?
An informal tally over a couple dozen prompts, tracking how often a generated function needed rework before being merged, gave these rough figures:
Vague prompts |################........| ~80% needed rework
Context only |##############..........| ~55% needed rework
Context + constraints |######...................| ~25% needed rework
All three sections |##.......................| ~10% needed rework
It isn't a controlled experiment, only a personal log, but the trend was steady. Spending ten seconds on constraints costs less than ten minutes debugging code that quietly assumed the wrong thing, even for throwaway scripts.
Smaller habits that pay off
Specify the output format. Leave out a phrase like "just the code" and the answer arrives wrapped in an explanation to scroll past each time. That adds friction when iterating quickly.
Use negative constraints. "No third-party libraries" or "don't use recursion here" removes a whole category of output that would otherwise be rejected and re-requested. Ruling things out up front beats catching them afterward.
Stack constraints through follow-ups. When the first answer is close, don't restart. Add one more constraint and ask for a revision. "Same function, but make the goroutine limit configurable" gets further than a fresh prompt, because the model already holds a working version as its baseline.
None of this replaces reading the code that comes back. It just leaves less to fix.