Prompt engineering for code generation is the art of formulating precise requests to an LLM (Large Language Model) like llama3.1:8b on Ollama, to obtain reliable, maintainable, and directly usable code. Unlike free-form text generation, prompt engineering for code requires rigor: specificity of intent, structured format, concrete examples (few-shot), and step-by-step reasoning (chain-of-thought). This guide covers production-tested techniques from Federico Calò's backend—from prompt structure basics to automatic output evaluation and intelligent iteration. We'll use Ollama on-prem locally and Python to wire the end-to-end workflow, avoiding cloud provider dependencies.
Introduction: Why Prompt Engineering for Code Matters
In 2026, code generation via LLM is a given: feature flag implementation, boilerplate reduction, refactoring scaffolding. But the quality of generated code is directly proportional to prompt quality. A vague prompt generates vague code; a precise prompt with clear examples and constraints generates production-ready code.
The challenge: on-prem LLMs like llama3.1:8b have context window constraints (8k tokens for standard Ollama) and haven't seen your entire codebase. So the prompt must be self-contained and specific enough to disambiguate the task.
This guide teaches the 6 pillars of prompt engineering for code:
- Spec-first prompting: declare intent and output format before the request
- Few-shot learning: provide concrete examples in the prompt itself
- Chain-of-thought reasoning: guide the model to reason step by step
- Role prompting: assign a role to the model for context awareness
- Structured output: force the model to output JSON, code, or specific formats
- Iteration and feedback: refine prompts based on real results
Pillar 1: Spec-First Prompting — The Foundation
What Spec-First Means
Spec-first prompting means writing the prompt like a brief technical specification: crystal-clear intent, defined input/output format, named edge cases. It's not prose; it's a specification that the model executes.
import requests
import json
# Ollama local endpoint
OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL = "llama3.1:8b"
def ollama_generate(prompt: str, temperature: float = 0.3) -> str:
"""Call Ollama on-prem and return generated text."""
response = requests.post(
OLLAMA_URL,
json={
"model": MODEL,
"prompt": prompt,
"stream": False,
"temperature": temperature,
"num_predict": 500
}
)
return response.json()["response"]
# Vague prompt (ANTI-PATTERN)
vague_prompt = """
Write a Python function that validates an email.
"""
# Spec-first prompt (CORRECT PATTERN)
spec_first_prompt = """You are a Python function expert. Generate a Python function with:
INPUT:
- function name: validate_email
- parameter: email (string)
OUTPUT:
- Return: boolean (True if valid, False otherwise)
- Validation rules:
1. Must contain exactly one '@'
2. Local part (before @) must be 1-64 alphanumeric/dots/hyphens
3. Domain (after @) must have at least one dot
4. TLD must be 2-6 characters
- No external libraries (use re module only)
- Include 3 docstring examples
FUNCTION:
"""
result = ollama_generate(spec_first_prompt)
print(result)
The difference is dramatic. The specific prompt generates a function with type hints, docstring, and correct validation. The vague prompt generates pseudo-code or incomplete logic.
Pillar 2: Few-Shot Learning — Teaching via Examples
What Few-Shot Is
Few-shot learning means providing 1-3 complete input→output examples in the prompt before asking for the real task. The model learns from the pattern and replicates it for the new request.
# Few-shot prompt for API endpoint generation
few_shot_prompt = """Generate a FastAPI endpoint from the description. Follow the pattern:
EXAMPLE 1:
Description: Get user by ID
Response Format: JSON with id, name, email
Expected endpoint: GET /users/{user_id}
Code:
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = await db.fetch_one("SELECT * FROM users WHERE id = $1", user_id)
if not user:
raise HTTPException(status_code=404)
return {"id": user.id, "name": user.name, "email": user.email}
---
EXAMPLE 2:
Description: Create a new article with title and content
Response Format: JSON with id, title, created_at
Expected endpoint: POST /articles
Code:
@app.post("/articles")
async def create_article(article: ArticleRequest):
article_id = await db.fetch_val(
"INSERT INTO articles (title, content) VALUES ($1, $2) RETURNING id",
article.title, article.content
)
return {"id": article_id, "title": article.title, "created_at": datetime.now()}
---
NOW:
Description: Delete an article by ID, return success status
Response Format: JSON with status and message
Expected endpoint: DELETE /articles/{article_id}
Code:
"""
result = ollama_generate(few_shot_prompt, temperature=0.2)
print(result)
Few-shot is powerful because the model doesn't infer the pattern from scratch; it sees it in examples and replicates it. Low temperature (0.2-0.3) makes the model deterministic and faithful to the pattern.
Pillar 3: Chain-of-Thought Reasoning
Guide the Model to Reason Step-by-Step
Chain-of-thought prompting asks the model to write its reasoning before generating code. This reduces logical bugs.
# Chain-of-thought prompt for a complex algorithm
cot_prompt = """Task: Generate a Python function to find the longest substring without repeating characters.
Before writing code, think through the approach:
1. Problem statement: Return the length of longest substring without repeating chars
2. Algorithm: Use a sliding window with a set to track chars in current window
3. Data structure: dict to store last seen index of each char
4. Edge cases:
- Empty string → return 0
- All unique chars → return length
- All same chars → return 1
5. Complexity: O(n) time, O(min(n, charset_size)) space
Now generate the code:
def longest_substring_without_repeating(s: str) -> int:
"""
result = ollama_generate(cot_prompt, temperature=0.2)
print(result)
Asking for reasoning "outside the code" makes the model more precise. It's like rubber duck debugging with AI.
Pillar 4: Role Prompting — Assign a Role
Context and Persona
Role prompting assigns a role to the model ("You're a senior Python backend engineer", "You're a SQL optimization expert"). It improves code quality and tone of voice.
# Role prompting for SQL optimization
role_prompt = """You are a senior SQL database architect at Google Cloud.
Your job: optimize slow queries for PostgreSQL 16 with 1M+ rows.
Task: Given a slow query, rewrite it for performance.
Slow query:
SELECT u.id, u.name, COUNT(a.id) as article_count
FROM users u
LEFT JOIN articles a ON u.id = a.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id
ORDER BY article_count DESC
LIMIT 10;
Optimize for:
1. Add missing indexes
2. Avoid full table scans
3. Use EXPLAIN ANALYZE to verify
4. Include index creation DDL
OPTIMIZED VERSION:
"""
result = ollama_generate(role_prompt, temperature=0.2)
print(result)
With role declared, the model generates index DDL, EXPLAIN suggestions, and senior-engineer comments. Without role, it might not think of these.
Pillar 5: Structured Output — Force the Format
JSON, Markdown, Specific Code
Structured output means explicitly saying: "Output valid JSON" or "Output only Python code, no comments". It reduces ambiguous parsing.
# Structured output prompting (JSON format forcing)
structured_prompt = """Generate a JSON schema validation function for a User object.
OUTPUT FORMAT (MUST be valid Python code, no markdown, no comments):
def validate_user(data: dict) -> tuple[bool, list[str]]:
errors = []
# Validation rules:
# - name: required, string, 1-100 chars
# - email: required, valid email format
# - age: optional, integer, 18-120
# - role: optional, must be in ['admin', 'user', 'guest']
# Return: (is_valid: bool, errors: list of error messages)
def validate_user(data: dict) -> tuple[bool, list[str]]:
"""
result = ollama_generate(structured_prompt, temperature=0.2)
print(result)
By declaring "OUTPUT FORMAT: Python code, no markdown", the model adheres to it. If you omit it, it might wrap the code in triple backticks.
Pillar 6: Iteration and Feedback Loop
Refine Based on Results
A prompt is never perfect on first try. Iterate: generate, evaluate, refine.
import re
def validate_prompt_output(code: str) -> tuple[bool, list[str]]:
"""Validate generated code before use."""
issues = []
# Check 1: Contains at least one def
if not re.search(r'^\s*def\s+\w+', code, re.MULTILINE):
issues.append("No function definition found")
# Check 2: Valid syntax
try:
compile(code, '', 'exec')
except SyntaxError as e:
issues.append(f"Syntax error: {e}")
# Check 3: Type hints present
if not re.search(r'->\s*\w+', code):
issues.append("Missing type hints on return")
return len(issues) == 0, issues
# Iteration loop
prompt = """Generate a function to parse CSV rows into dataclass instances."""
max_iterations = 3
for i in range(max_iterations):
result = ollama_generate(prompt, temperature=0.2)
is_valid, issues = validate_prompt_output(result)
if is_valid:
print(f"✓ Valid on iteration {i+1}")
break
else:
print(f"✗ Iteration {i+1} issues: {issues}")
# Refine prompt for next iteration
prompt += f"\nFix: {issues[0]}"
Automatic validation + feedback loop transforms a mediocre prompt into a good one in a few iterations.
Context Window Management: Token Budget
Maximize Token Usage
Ollama with llama3.1:8b has ~8k token context. A long prompt + examples + long output generation = overflow. Strategy: be concise, prioritize examples, truncate marginally useful context.
def estimate_tokens(text: str) -> int:
"""Rough estimate of token count (1 token ≈ 4 chars)."""
return len(text) // 4
def build_optimized_prompt(
spec: str,
examples: list[tuple[str, str]],
task: str,
max_context_tokens: int = 6000
) -> str:
"""Build a prompt within token budget."""
lines = [
"You are a code generation expert.",
"",
"SPECIFICATION:",
spec,
""
]
# Add examples up to budget
current_tokens = estimate_tokens("\n".join(lines))
for i, (input_ex, output_ex) in enumerate(examples):
example_text = f"\nEXAMPLE {i+1}:\nInput: {input_ex}\nOutput: {output_ex}"
if current_tokens + estimate_tokens(example_text) < max_context_tokens * 0.7:
lines.append(example_text)
current_tokens += estimate_tokens(example_text)
lines.append(f"\nTASK:\n{task}")
return "\n".join(lines)
# Usage
spec = "Generate a Python function that validates a credit card number using Luhn algorithm."
examples = [
("4532015112830366", "True"),
("1234567890123456", "False"),
]
task = "def validate_cc(card_number: str) -> bool:"
optimized = build_optimized_prompt(spec, examples, task)
print(f"Prompt tokens: {estimate_tokens(optimized)}")
result = ollama_generate(optimized)
Real-World Case Study: Generate a SQL Parser
End-to-End Application
Imagine generating a simple SQL parser for SELECT queries. We'll apply all pillars.
sql_parser_prompt = """You are a Python expert in parsing SQL queries.
TASK: Generate a simple SQL SELECT query parser class.
SPECIFICATION:
- Class name: SimpleSelectParser
- Input: SELECT query string (e.g., "SELECT id, name FROM users WHERE age > 18")
- Output: dict with keys: columns (list), table (str), where_condition (str or None)
- No external SQL libraries (no sqlparse)
- Must handle: SELECT col1, col2 FROM table WHERE condition
EXAMPLE:
Input: "SELECT id, name FROM users WHERE age > 18"
Output: {"columns": ["id", "name"], "table": "users", "where_condition": "age > 18"}
REASONING:
1. Use regex to extract SELECT clause, FROM clause, WHERE clause
2. Split columns on comma, strip whitespace
3. Extract table name after FROM
4. Extract everything after WHERE
CODE:
"""
result = ollama_generate(sql_parser_prompt, temperature=0.2)
# Validation and testing
test_input = 'SELECT id, email FROM customers WHERE status = "active"'
try:
exec(result)
parser = eval("SimpleSelectParser()")
output = parser.parse(test_input)
print(f"Parsed: {output}")
except Exception as e:
print(f"Error: {e}")
print("Refining prompt...")
Common Mistakes and How to Avoid Them
Frequent Anti-Patterns
1. Prompt too vague: "Write a sorting algorithm". The model generates bubble sort, quicksort, or merge sort at random. Solution: specify which algorithm and what complexity.
2. Incomplete context: Don't give enough background on the project. The model doesn't know if the function must integrate with PostgreSQL, Redis, or an external API. Solution: include brief comments with dependencies and constraints.
3. No output validation: Assume generated code is always correct. Solution: always compile, test with 1-2 simple cases, check type hints.
4. Temperature too high: With temperature > 0.5, the model diverges from examples and spec. Solution: use temperature 0.1-0.3 for code, 0.5-0.7 for creative text.
Production Best Practices
- Version your prompts: Keep prompt_v1.txt, prompt_v2.txt, etc. Document what changed and why.
- Cache prompts: If you use the same prompt 100 times, cache the response. Redis + hash the request.
- Quality monitoring: Track how many generated outputs pass validation. Target: >90%.
- Deterministic fallback: If Ollama is down or output is invalid, return a generic template or ask the user.
- Rate limiting awareness: Ollama on-prem has finite resources. Limit 10 req/min per user, queue the rest.
FAQ
Q: How reliable is code generated by Ollama?
A: It depends on the prompt. With spec-first + few-shot + chain-of-thought + validation, success rate is 85-95% for simple-intermediate tasks (functions <100 lines, classic algorithms). For complex tasks (distributed architectures, numerical optimizations), drop to 50-70%. Always: generate, validate, manually review, test.
Q: Can I use a cloud AI service instead of Ollama?
A: Technically yes, but this article and Federico Calò's backend use ONLY Ollama on-prem for privacy, zero recurring costs, and full control. Metered cloud AI services add costs ($0.01-0.02 per request) and external dependencies. If you already have Ollama, use it.
Q: How do I maintain code style consistency in generated code?
A: In the prompt, cite a snippet of your style (variable names, structure, imports). Few-shot works: if you give 2 examples in your style, the generated code will follow it. Alternatively: post-process with Black (Python), Prettier (JS), or an automatic formatter.
Q: How many tokens do I consume per request?
A: Rough: spec (100-300 tok) + examples (200-500 tok) + task (50-200 tok) + output (100-500 tok) = 500-1500 token per request. Ollama has no token costs (it's on-prem), so iterating is free. With a metered service, you'd calculate prompt + completion token costs and optimize.
Q: The model fails often. What do I do?
A: Iteration + feedback. Track errors, update the prompt with constraints that prevent that error type, and retry. Example: if the model forgets type hints, add "MUST INCLUDE: Python 3.9+ type hints on all function parameters and returns" to the prompt. Version and keep a story of improvements.
Conclusion
Prompt engineering for code isn't abstract science; it's a practical skill with immediate ROI: less boilerplate to write, fewer bugs, faster deploys. The 6 pillars (spec-first, few-shot, chain-of-thought, role prompting, structured output, iteration) are the production-verified playbook. Use Ollama on-prem to avoid cloud costs and dependencies, always validate generated output, and iterate until coverage >90%. Next step: integrate this workflow into your CI/CD—generate code, validate, test automatically, and deploy if all passes.

Commenti
Caricamento commenti...
Sign in to comment