Il prompt engineering per la generazione di codice è l'arte di formulare richieste precise a un modello LLM (Large Language Model) come llama3.1:8b su Ollama, in modo da ottenere codice affidabile, manutenibile e direttamente utilizzabile. A differenza della generazione di testo libero, il prompt engineering per il codice richiede rigore: specificità dell'intent, format strutturato, esempi concreti (few-shot), e ragionamento passo dopo passo (chain-of-thought). Questa guida copre le tecniche provate in produzione sul backend di Federico Calò—dalle basi del prompt structure fino alla valutazione automatica dell'output e all'iterazione intelligente. Useremo Ollama on-prem locale e Python per integrare il flusso end-to-end, evitando dipendenze da provider cloud.
Introduzione: Perché il Prompt Engineering per il Codice Importa
Nel 2026, la generazione di codice via LLM è un dato di fatto: feature flag implementation, boilerplate reduction, refactoring scaffolding. Ma la qualità del codice generato è direttamente proporzionale alla qualità del prompt. Un prompt vago genera codice vago; un prompt preciso, con esempi e vincoli chiari, genera codice production-ready.
La sfida: i modelli LLM on-prem come llama3.1:8b hanno vincoli di context window (8k token per Ollama standard) e non hanno visto l'intero codebase del tuo progetto. Quindi il prompt deve essere auto-contenuto e sufficientemente specifico da disambiguare il task.
Questa guida insegna i 6 pilastri del prompt engineering per il codice:
- Spec-first prompting: dichiarare intent e output format prima della richiesta
- Few-shot learning: fornire esempi concreti nel prompt stesso
- Chain-of-thought reasoning: guidare il modello a ragionare passo dopo passo
- Role prompting: assegnare un ruolo al modello per contest awareness
- Structured output: forzare il modello a output JSON, codice, o format specifici
- Iterazione e feedback: refine i prompt in base ai risultati reali
Pilastro 1: Spec-First Prompting — Il Foundation
Cosa significa Spec-First
Spec-first prompting significa scrivere il prompt come se fosse una specifica tecnica breve: intent chiarissimo, input/output formato definito, edge case nominati. Non è prosa naturale; è una specifica che il modello esegue.
import requests
import json
# Endpoint Ollama locale
OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL = "llama3.1:8b"
def ollama_generate(prompt: str, temperature: float = 0.3) -> str:
"""Chiama Ollama on-prem e ritorna il testo generato."""
response = requests.post(
OLLAMA_URL,
json={
"model": MODEL,
"prompt": prompt,
"stream": False,
"temperature": temperature,
"num_predict": 500
}
)
return response.json()["response"]
# Prompt vago (ANTI-PATTERN)
vague_prompt = """
Scrivi una funzione Python che valida un email.
"""
# Spec-first prompt (PATTERN CORRETTO)
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)
La differenza è drammatica. Il prompt specifico genera una funzione con type hints, docstring, e validazione corretta. Il prompt vago genera spesso pseudo-codice o logica incompleta.
Pilastro 2: Few-Shot Learning — Insegnare via Esempi
Cos'è Few-Shot
Few-shot learning significa fornire 1-3 esempi completi di input→output nel prompt, prima di chiedere il vero task. Il modello impara dal pattern e lo replica per la nuova richiesta.
# Few-shot prompt per generare API endpoint
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 è potente perché il modello non deve inferire il pattern da zero; lo vede negli esempi e lo replica. Temperature bassa (0.2-0.3) rende il modello deterministico e fedele al pattern.
Pilastro 3: Chain-of-Thought Reasoning
Guidare il Modello a Ragionare Passo dopo Passo
Chain-of-thought prompting chiede al modello di scrivere il suo ragionamento prima di generare il codice. Questo riduce i bug logici.
# Chain-of-thought prompt per un algoritmo complesso
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)
Chiedere il ragionamento "fuori dal codice" rende il modello più preciso. È come se stessi facendo rubber duck debugging con l'AI.
Pilastro 4: Role Prompting — Assegnare un Ruolo
Contesto e Persona
Role prompting assegna un ruolo al modello ("Sei un senior backend engineer di Python", "Sei un expert di query SQL ottimizzate"). Migliora la qualità e la tone of voice del codice.
# Role prompting per 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)
Con il ruolo dichiarato, il modello genera DDL index, suggerimenti di EXPLAIN, e commenti da senior engineer. Senza ruolo, potrebbe non pensarci.
Pilastro 5: Structured Output — Forzare il Format
JSON, Markdown, Codice Specifico
Structured output significa dire esplicitamente: "Output JSON valido" o "Output solo codice Python, nessun commento". Riduce il parsing ambiguo.
# 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)
Dichiarando "OUTPUT FORMAT: Python code, nessun markdown", il modello si attiene a it. Se lo ometti, potrebbe wrappare il codice in triple backticks.
Pilastro 6: Iterazione e Feedback Loop
Refine in Base ai Risultati
Un prompt non è mai perfetto al primo colpo. Iterare: generare, valutare, refine.
import re
def validate_prompt_output(code: str) -> tuple[bool, list[str]]:
"""Valida il codice generato prima di usarlo."""
issues = []
# Check 1: Contiene almeno una def
if not re.search(r'^\s*def\s+\w+', code, re.MULTILINE):
issues.append("No function definition found")
# Check 2: Syntax valido
try:
compile(code, '', 'exec')
except SyntaxError as e:
issues.append(f"Syntax error: {e}")
# Check 3: Type hints presenti
if not re.search(r'->\s*\w+', code):
issues.append("Missing type hints on return")
return len(issues) == 0, issues
# Iterazione 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 per la prossima iterazione
prompt += f"\nFix: {issues[0]}"
La validazione automatica + feedback loop trasforma un prompt mediocre in uno buono in poche iterazioni.
Gestione del Context Window: Token Budget
Massimizzare l'uso dei Token
Ollama con llama3.1:8b ha ~8k token di context. Un prompt lungo + esempi + generazione di output lungo = overflow. Strategia: be concise, prioritize examples, truncate marginally useful context.
def estimate_tokens(text: str) -> int:
"""Stima rozza del numero di token (1 token ≈ 4 char)."""
return len(text) // 4
def build_optimized_prompt(
spec: str,
examples: list[tuple[str, str]],
task: str,
max_context_tokens: int = 6000
) -> str:
"""Costruisce un prompt entro il budget di token."""
lines = [
"You are a code generation expert.",
"",
"SPECIFICATION:",
spec,
""
]
# Aggiungi esempi fino al 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)
# Utilizzo
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)
Case Study Reale: Generare un Parser SQL
Applicazione End-to-End
Immagina di voler generare un parser SQL semplice per SELECT queries. Applicheremo tutti i pilastri.
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)
# Validazione e test
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...")
Errori Comuni e Come Evitarli
Anti-Pattern Frequenti
1. Prompt troppo vago: "Scrivi un algoritmo di sort". Il modello genera bubble sort, quicksort, o merge sort a caso. Soluzione: specificare quale algoritmo e con quale complessità.
2. Contesto incompleto: Non dare abbastanza background sul progetto. Il modello non sa se la funzione deve integrare con PostgreSQL, Redis, o una API esterna. Soluzione: includere brevi commenti con dipendenze e constraint.
3. Nessuna validazione dell'output: Assumere che il codice generato sia sempre corretto. Soluzione: sempre compilare, testare con 1-2 case semplici, controllare type hints.
4. Temperature troppo alta: Con temperature > 0.5, il modello diverge da esempi e spec. Soluzione: usare temperature 0.1-0.3 per codice, 0.5-0.7 per testo creativo.
Best Practice per la Produzione
- Versionare i prompt: Tieni un file prompt_v1.txt, prompt_v2.txt, ecc. Documenta cosa è cambiato e perché.
- Caching dei prompt: Se usi lo stesso prompt 100 volte, cachela risposta. Redis + hash della richiesta.
- Monitoraggio della qualità: Traccia quanti output generati passano la validazione. Target: >90%.
- Fallback deterministico: Se Ollama è down o l'output è invalido, ritorna un template generico o chiedi all'utente.
- Rate limiting consapevole: Ollama on-prem ha risorse finite. Limita 10 req/min per utente, queue il resto.
FAQ
D: Quanto è affidabile il codice generato da Ollama?
R: Dipende dal prompt. Con spec-first + few-shot + chain-of-thought + validazione, il tasso di successo è 85-95% per task semplici-intermedi (funzioni <100 righe, algoritmi classici). Per task complessi (architetture distribuite, ottimizzazioni numeriche), scendi a 50-70%. Sempre: generare, validare, rivedere manualmente, testare.
D: Posso usare un servizio AI cloud al posto di Ollama?
R: Tecnicamente sì, ma questo articolo e il backend di Federico Calò usano SOLO Ollama on-prem per privacy, zero costi ricorrenti, e control totale. I servizi AI cloud a consumo aggiungono costi ($0.01-0.02 per richiesta) e dipendenze esterne. Se hai già Ollama, usalo.
D: Come mantengo la coerenza di stile nel codice generato?
R: Nel prompt, cita uno snippet del tuo stile (nomi di variabili, struttura, import). Few-shot funziona: se dai 2 esempi nel tuo stile, il generato seguirà quello. Alternativa: post-process con Black (Python), Prettier (JS), o uno formatter automatico.
D: Quanti token consumo per richiesta?
R: Rough: spec (100-300 tok) + esempi (200-500 tok) + task (50-200 tok) + output (100-500 tok) = 500-1500 token per richiesta. Ollama non ha costi per token (è on-prem), quindi iterare è free. Con un servizio a consumo, calcoleresti costi di prompt + completion token e ottimizzeresti.
D: Il modello fallisce spesso. Cosa faccio?
R: Iterazione + feedback. Traccia gli errori, aggiorna il prompt con vincoli che prevengono quel tipo di errore, e riprova. Esempio: se il modello dimentica i type hints, aggiungi "MUST INCLUDE: Python 3.9+ type hints on all function parameters and returns" al prompt. Versiona e tieni story di miglioramenti.
Conclusione
Il prompt engineering per il codice non è una scienza astratta; è una skill pratica con ROI immediato: meno boilerplate da scrivere, meno bug, deploy più veloci. I 6 pilastri (spec-first, few-shot, chain-of-thought, role prompting, structured output, iterazione) sono il playbook verificato in produzione. Usa Ollama on-prem per evitare costi e dipendenze cloud, valida sempre l'output generato, e iterare fino a coverage >90%. Il prossimo passo: integrare questo workflow nel tuo CI/CD—genera codice, valida, testa automaticamente, e deploy se tutto passa.

Commenti
Caricamento commenti...
Accedi per commentare