Prerequisiti e installazione
Servono tre cose: Python in una versione fra la 3.10 e la 3.13 (la 3.14 non è supportata, il pacchetto dichiara esplicitamente il limite <3.14), un gestore di pacchetti e una chiave API di un fornitore di modelli. Un fornitore qualsiasi: OpenAI, Anthropic, Google, oppure nessuna chiave se userai un modello locale.
La documentazione ufficiale adotta uv come gestore di dipendenze e consiglia uv tool install crewai per la CLI. Se preferisci pip, per la libreria funziona altrettanto bene:
python -m venv .venv # ambiente isolato
source .venv/bin/activate
pip install crewai # il framework
pip install 'crewai[tools]' # con gli strumenti pronti
crewai version # conferma l'installazioneLe virgolette attorno a crewai[tools] non sono decorative: senza, alcune shell interpretano le parentesi quadre e il comando fallisce. L'extra tools aggiunge il pacchetto degli strumenti pronti; su PyPI sono elencati anche gli altri extra, fra cui embeddings e quelli per i singoli fornitori cloud.
Due strade: scaffolding o API diretta
Dalla serie 1.x ci sono due modi di lavorare con CrewAI, e quasi tutti i tutorial in circolazione ne mostrano uno solo, spesso nella forma vecchia.
La prima strada è lo scaffolding: crewai create crew nome genera un progetto completo con la configurazione degli agenti in file separati dal codice. Nella forma attuale i file sono JSONC, un crew.jsonc più un file per agente nella cartella agents/; con il flag --classic ottieni la struttura precedente, con crew.py e le configurazioni YAML in config/agents.yaml e config/tasks.yaml. Il progetto si avvia con crewai run, che capisce dal pyproject.toml cosa deve lanciare.
crewai create crew analisi_mercato
# progetto JSON-first: crew.jsonc + agents/*.jsonc
crewai create crew analisi_mercato --classic
# scaffold classico: crew.py + config/agents.yaml + config/tasks.yaml
cd analisi_mercato
crewai install # risolve le dipendenze
crewai run # esegue il crewLa seconda strada è l'API Python diretta: importi le classi e scrivi tutto in un file. Resta pienamente supportata ed è quella che troverai in quasi ogni guida online, perché esiste dalla prima versione. Il problema è che molte di quelle guide sono ferme alla serie 0.x, con import e parametri che nel frattempo sono cambiati. Qui usiamo l'API diretta per una ragione didattica, si vede ogni pezzo del meccanismo, ma sulla versione attuale: quando il progetto cresce, passare allo scaffolding conviene.
Il primo crew funzionante
L'esempio classico con due ruoli: uno cerca, l'altro scrive. Due agenti, due task, processo sequenziale. È il codice completo, non un frammento: con una chiave configurata gira così com'è.
from crewai import Agent, Task, Crew, Process
# I segnaposto {settore} vengono riempiti da kickoff(inputs=...)
ricercatore = Agent(
role="Ricercatore di mercato",
goal="Raccogliere dati verificabili sul mercato {settore} in Italia",
backstory="Distingue le fonti primarie dai comunicati stampa.",
llm="gpt-5.4-mini",
verbose=True, # mostra il ragionamento passo passo
)
redattore = Agent(
role="Redattore tecnico",
goal="Trasformare i dati raccolti in una sintesi onesta",
backstory="Frasi brevi. Non aggiunge nulla che non sia nei dati.",
llm="gpt-5.4-mini",
)
ricerca = Task(
description="Raccogli 5 dati recenti sul mercato {settore} in Italia.",
expected_output="Elenco numerato: dato, anno, fonte.",
agent=ricercatore,
)
sintesi = Task(
description="Scrivi una sintesi di 200 parole dai dati raccolti.",
expected_output="Testo di circa 200 parole che cita i 5 dati.",
agent=redattore,
context=[ricerca], # riceve l'output del task precedente
)
crew = Crew(
agents=[ricercatore, redattore],
tasks=[ricerca, sintesi],
process=Process.sequential,
)
esito = crew.kickoff(inputs={"settore": "fotovoltaico"})
print(esito.raw)Tre dettagli che i tutorial saltano. Primo: i valori passati a kickoff(inputs=...) sostituiscono i segnaposto fra graffe in ruoli, obiettivi e descrizioni, quindi lo stesso crew serve per settori diversi senza toccare il codice. Secondo: il campo expected_output non è un commento, è la specifica del risultato, e la qualità dell'output dipende più da lì che dal modello. Terzo: context=[ricerca] dichiara esplicitamente da quale task arriva il materiale, invece di affidarsi solo all'ordine di esecuzione.
Configurare il modello
Il parametro llm dell'agente accetta una stringa o un oggetto. Se lo ometti, il framework usa il modello indicato nella variabile d'ambiente OPENAI_MODEL_NAME e, in mancanza, ripiega su un modello OpenAI: in entrambi i casi serve una chiave OpenAI valida.
errore
Chiede la chiave OpenAI anche se non la volevi usare
llm, esecuzione avviata, e il crew si ferma chiedendo OPENAI_API_KEY. Non è un difetto della tua installazione, è il comportamento predefinito. Dichiara sempre il modello su ogni agente, anche quando usi OpenAI: il codice resta leggibile e il giorno in cui cambi fornitore modifichi una riga.Per puntare a un fornitore diverso si usa la notazione fornitore/modello. CrewAI ha integrazioni native per OpenAI, Anthropic, Google, Azure e Bedrock; tutti gli altri fornitori passano da LiteLLM, con le stesse stringhe. L'oggetto LLM serve quando vuoi controllare temperatura, limiti di token o l'indirizzo di un server locale:
from crewai import Agent, LLM
# stringa "fornitore/modello": la via rapida
redattore = Agent(
role="Redattore tecnico",
goal="...",
backstory="...",
llm="anthropic/claude-sonnet-4-6",
)
# oggetto LLM: controllo fine, endpoint locali
locale = LLM(
model="ollama/llama3:70b",
base_url="http://localhost:11434", # indirizzo standard di Ollama
temperature=0.2,
)
analista = Agent(
role="Analista",
goal="...",
backstory="...",
llm=locale,
)Far girare tutto in locale, senza chiavi e senza mandare dati fuori, merita un discorso a parte: la guida per il locale copre installazione di Ollama, scelta del modello e i limiti reali dei modelli piccoli con gli strumenti.
Aggiungere strumenti
Senza strumenti un agente può solo scrivere. Il pacchetto crewai_tools, installato con l'extra visto sopra, ne porta decine già pronti: SerperDevTool fa ricerche Google tramite serper.dev e richiede la variabile SERPER_API_KEY, ScrapeWebsiteTool legge il contenuto di una pagina, FileReadTool legge file locali.
Quando serve qualcosa di tuo, la via più corta è il decoratore @tool su una funzione. Per strumenti con più argomenti e validazione si estende la classe BaseTool, definendo name, description, uno schema Pydantic in args_schema e il metodo _run. In entrambi i casi la descrizione è la parte che conta: è il testo che il modello legge per decidere se e quando usare lo strumento.
from crewai import Agent
from crewai.tools import tool
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
# strumento personalizzato: una funzione con docstring
@tool("Cambio euro dollaro")
def cambio_eur_usd() -> str:
"""Restituisce il cambio euro dollaro corrente."""
return leggi_cambio_da_api() # la tua logica
ricercatore = Agent(
role="Ricercatore di mercato",
goal="Raccogliere dati verificabili su {settore}",
backstory="Cita la fonte di ogni numero che riporta.",
tools=[SerperDevTool(), ScrapeWebsiteTool(), cambio_eur_usd],
llm="gpt-5.4-mini",
)Un avvertimento pratico: ogni strumento in più allarga il prompt e dà al modello un'occasione in più di sbagliare chiamata, un problema frequente con i modelli non OpenAI. Dai a ogni agente solo gli strumenti che il suo compito richiede davvero.
Output strutturati
Finché il risultato lo legge una persona, il testo libero di esito.raw basta. Appena il risultato entra in un altro programma servono campi, non prosa. I Task hanno tre parametri per questo: output_pydantic valida l'output contro un modello Pydantic e restituisce un'istanza tipizzata, output_json usa lo stesso schema ma restituisce un dizionario, output_file scrive il risultato su un percorso.
from pydantic import BaseModel
from crewai import Task
class SchedaMercato(BaseModel):
settore: str
dati: list[str]
fonti: list[str]
sintesi = Task(
description="Componi la scheda del mercato {settore}.",
expected_output="Scheda con settore, 5 dati e le fonti.",
agent=redattore,
output_pydantic=SchedaMercato,
)
esito = crew.kickoff(inputs={"settore": "fotovoltaico"})
scheda = esito.pydantic # istanza di SchedaMercato
print(scheda.dati)Lo schema Pydantic fa due lavori insieme: dice al modello che forma deve avere la risposta e blocca l'esecuzione se la forma non torna. Per la variante JSON il dizionario si legge da esito.json_dict. Esiste anche un parametro guardrail sui Task, una funzione che valida l'output prima di passare al task successivo: utile quando la validazione è logica e non solo di forma.
Da sequenziale a gerarchico
Con process=Process.hierarchical l'ordine dei task non lo decidi più tu: CrewAI crea un agente manager che assegna i compiti, valuta i risultati e può rimandarli indietro. Il processo gerarchico richiede un modello per il manager, indicato in manager_llm, oppure un agente tuo in manager_agent: senza uno dei due l'esecuzione non parte. Al manager conviene dare un modello capace, perché coordinare è il lavoro più delicato del crew.
attenzione
Il gerarchico moltiplica i costi
token_usage prima e dopo: se la qualità non sale in proporzione, torna indietro.Verso la produzione
Un crew che gira sul portatile e un crew che gira cento volte al giorno sono due problemi diversi. I parametri che separano i due mondi sono già nel framework, e conviene conoscerli prima che servano.
max_iter, sull'agente, limita i cicli di ragionamento (il default è 20). È la difesa contro l'errore "Maximum iterations reached" e contro gli agenti che girano a vuoto accumulando token.max_rpm, sull'agente o sull'intero crew, limita le richieste al minuto. È la mitigazione concreta degli errori 429 dei fornitori.verbosestampa il ragionamento: prezioso in sviluppo, rumore in produzione. Per i log persistenti c'èoutput_log_file: conTruescrivelogs.txtnella cartella corrente, con un percorso che finisce in.jsonscrive log strutturati.esito.token_usageriporta i token della singola esecuzione,crew.usage_metricsle metriche aggregate. È il numero da guardare prima di decidere quante esecuzioni al giorno puoi permetterti.
crew = Crew(
agents=[ricercatore, redattore],
tasks=[ricerca, sintesi],
process=Process.sequential,
max_rpm=30, # tetto alle richieste al minuto
output_log_file="crew_log.json", # log strutturati su file
)
esito = crew.kickoff(inputs={"settore": "fotovoltaico"})
print(esito.token_usage) # token di questa esecuzione
print(crew.usage_metrics) # metriche aggregate del crewE qui va detta la cosa che i tutorial di solito omettono: nessun tutorial, questo compreso, rende un sistema pronto per la produzione. Quello che hai a fine pagina è un crew che funziona e che sai misurare. Mancano ancora la gestione dei fallimenti a metà esecuzione, una valutazione sistematica della qualità degli output, un tetto di spesa vero e la struttura deterministica che un processo ripetibile richiede: per quest'ultima il framework offre i Flows, che governano i passaggi a codice e chiamano i crew solo dove l'autonomia serve. E quando qualcosa si rompe, perché succederà, la pagina sugli errori comuni raccoglie i casi che incontrano quasi tutti, con le cause verificate.