Salta al contenuto principale

05 - Workers AI: Inferenza di LLM e Modelli Vision Direttamente sull'Edge

Edge Computing 15 min Workers AI: Inferenza di LLM e Modelli Vision Direttamente sull'Edge Workers AI ha registrato una crescita del 4000% YoY nelle...

Cosa ne pensi di questo articolo?

05 - Workers AI: Inferenza di LLM e Modelli Vision Direttamente sull'Edge
Edge ComputingFoto:panumas nikhomkhaiPexels

In Breve

Edge Computing 15 min Workers AI: Inferenza di LLM e Modelli Vision Direttamente sull'Edge Workers AI ha registrato una crescita del 4000% YoY nelle...

Edge Computing 15 min

Workers AI: Inferenza di LLM e Modelli Vision Direttamente sull'Edge

Workers AI ha registrato una crescita del 4000% YoY nelle richieste di inferenza a Q1 2026. Scopri come eseguire modelli di testo, visione e speech direttamente nei tuoi Workers senza GPU dedicata, con latenza sotto i 200ms.

Workers AI LLM Edge AI Inference Edge AI Cloudflare AI

L'Inferenza AI Spostata all'Edge

Fino al 2023, eseguire un Large Language Model significava quasi obbligatoriamente comunicare con un API esterna (OpenAI, Anthropic, Google) o deployare GPU costose su infrastruttura dedicata. La latenza della rete verso questi endpoint centralizzati aggiungeva 200-800ms a ogni richiesta, e i costi di GPU erano proibitivi per applicazioni a basso volume.

Workers AI ha cambiato questo scenario. Cloudflare ha distribuito hardware di inferenza AI (GPU specializzate) in decine di datacenter in tutto il mondo. I modelli vengono eseguiti sullo stesso hardware che gestisce il tuo Worker, eliminando il round-trip di rete verso provider esterni. Il risultato è inferenza a latenza ridotta, fatturata al consumo senza gestione di infrastruttura.

Cosa Imparerai

  • Modelli disponibili in Workers AI: LLM, vision, speech, embedding
  • Text generation con Llama 3.1 e streaming della risposta
  • Vision models: analisi di immagini con LLaVA
  • Speech-to-text con Whisper
  • Text embeddings per search semantica
  • AI Gateway: caching, rate limiting e osservabilità delle richieste AI
  • Limiti, costi e strategie di ottimizzazione

Panoramica dei Modelli Disponibili

Workers AI offre una selezione di modelli open-source ottimizzati per l'inferenza su hardware Cloudflare. I modelli vengono indicati con il prefisso @cf/ o @hf/ (Hugging Face hosted):

Categoria Modelli principali Caso d'uso
Text Generation @cf/meta/llama-3.1-8b-instruct, @cf/mistral/mistral-7b-instruct-v0.2 Chatbot, summary, Q&A, code generation
Text Generation (Large) @cf/meta/llama-3.3-70b-instruct-fp8-fast Reasoning complesso, analisi avanzata
Vision @cf/llava-hf/llava-1.5-7b-hf, @cf/unum/uform-gen2-qwen-500m Image captioning, visual Q&A
Speech-to-Text @cf/openai/whisper, @cf/openai/whisper-large-v3-turbo Trascrizione audio
Text Embeddings @cf/baai/bge-base-en-v1.5, @cf/baai/bge-large-en-v1.5 Semantic search, similarity, RAG
Image Classification @cf/microsoft/resnet-50 Classificazione immagini
Translation @cf/meta/m2m100-1.2b Traduzione 100+ lingue
Image Generation @cf/stabilityai/stable-diffusion-xl-base-1.0 Text-to-image

Configurazione: Binding AI nel wrangler.toml

Per usare Workers AI basta aggiungere il binding [ai] nella configurazione:

# wrangler.toml
name = "ai-worker"
main = "src/worker.ts"
compatibility_date = "2024-09-23"

# Binding per Workers AI
[ai]
binding = "AI"

Il tipo TypeScript del binding va dichiarato nell'interfaccia Env:

// types.ts - dichiarazione del binding AI
interface Env {
  AI: Ai; // Tipo fornito da @cloudflare/workers-types
}

Text Generation con Llama 3.1

Il caso d'uso più comune è la generazione di testo con un modello istruzione-risposta. Vediamo come implementare un endpoint di chat:

// src/worker.ts - endpoint di chat con Llama 3.1

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== 'POST' || new URL(request.url).pathname !== '/chat') {
      return new Response('POST /chat required', { status: 400 });
    }

    const { messages, stream = false } = await request.json<ChatRequest>();

    // Valida l'input
    if (!Array.isArray(messages) || messages.length === 0) {
      return Response.json({ error: 'messages array required' }, { status: 400 });
    }

    if (stream) {
      // Streaming response: il modello restituisce token man mano
      const aiStream = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
        messages,
        stream: true,
        max_tokens: 1024,
        temperature: 0.7,
      });

      // Trasforma lo stream AI in Server-Sent Events
      return new Response(aiStream, {
        headers: {
          'Content-Type': 'text/event-stream',
          'Cache-Control': 'no-cache',
          'Connection': 'keep-alive',
        },
      });
    }

    // Risposta sincrona: attende il completion completo
    const result = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
      messages,
      max_tokens: 1024,
      temperature: 0.7,
    });

    return Response.json({
      response: (result as AiTextGenerationOutput).response,
      usage: {
        // Workers AI non espone ancora i token counts nella risposta base
        model: '@cf/meta/llama-3.1-8b-instruct',
      },
    });
  },
};

interface ChatRequest {
  messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;
  stream?: boolean;
}

interface Env {
  AI: Ai;
}

Un esempio più completo che implementa un assistente con system prompt e gestione degli errori robusta:

// src/assistant-worker.ts

const SYSTEM_PROMPT = `Sei un assistente tecnico esperto in cloud computing e edge computing.
Rispondi in modo conciso e tecnico. Se non conosci la risposta, dillo chiaramente.
Non inventare informazioni. Rispondi sempre in italiano a meno che l'utente non scriva in un'altra lingua.`;

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== 'POST') {
      return new Response('Method Not Allowed', { status: 405 });
    }

    let body: AssistantRequest;
    try {
      body = await request.json<AssistantRequest>();
    } catch {
      return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
    }

    if (!body.question?.trim()) {
      return Response.json({ error: 'question field is required' }, { status: 400 });
    }

    try {
      const result = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
        messages: [
          { role: 'system', content: SYSTEM_PROMPT },
          { role: 'user', content: body.question },
        ],
        max_tokens: 2048,
        temperature: 0.3, // Bassa temperatura per risposte piu deterministiche
      }) as AiTextGenerationOutput;

      return Response.json({
        answer: result.response,
        model: '@cf/meta/llama-3.1-8b-instruct',
        timestamp: new Date().toISOString(),
      });
    } catch (err) {
      console.error('AI inference error:', err);
      return Response.json(
        { error: 'AI inference failed', details: (err as Error).message },
        { status: 500 }
      );
    }
  },
};

interface AssistantRequest {
  question: string;
}

interface Env {
  AI: Ai;
}

Vision Models: Analisi di Immagini

I modelli vision permettono di analizzare immagini in input insieme a una domanda testuale. Questo è utile per moderazione di contenuti, estrazione di informazioni da documenti scansionati e accessibility features:

// src/vision-worker.ts - analisi immagini con LLaVA

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== 'POST') {
      return new Response('Method Not Allowed', { status: 405 });
    }

    // Accetta immagine come Base64 o URL
    const body = await request.json<VisionRequest>();

    let imageData: number[];

    if (body.imageUrl) {
      // Scarica l'immagine e converti in array di byte
      const imgResponse = await fetch(body.imageUrl);
      if (!imgResponse.ok) {
        return Response.json({ error: 'Failed to fetch image' }, { status: 400 });
      }
      const buffer = await imgResponse.arrayBuffer();
      imageData = Array.from(new Uint8Array(buffer));
    } else if (body.imageBase64) {
      // Decodifica Base64
      const binaryString = atob(body.imageBase64);
      imageData = Array.from({ length: binaryString.length }, (_, i) =>
        binaryString.charCodeAt(i)
      );
    } else {
      return Response.json({ error: 'imageUrl or imageBase64 required' }, { status: 400 });
    }

    const prompt = body.prompt ?? 'Descrivi questa immagine in dettaglio in italiano.';

    const result = await env.AI.run('@cf/llava-hf/llava-1.5-7b-hf', {
      image: imageData,
      prompt,
      max_tokens: 512,
    }) as AiTextGenerationOutput;

    return Response.json({
      description: result.response,
      prompt,
      model: '@cf/llava-hf/llava-1.5-7b-hf',
    });
  },
};

interface VisionRequest {
  imageUrl?: string;
  imageBase64?: string;
  prompt?: string;
}

interface Env {
  AI: Ai;
}

Speech-to-Text con Whisper

Workers AI include Whisper per la trascrizione audio. Il modello accetta audio in formato ArrayBuffer e restituisce la trascrizione con timestamp opzionali:

// src/transcribe-worker.ts - Speech-to-text con Whisper

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== 'POST') {
      return new Response('Method Not Allowed', { status: 405 });
    }

    const contentType = request.headers.get('Content-Type') ?? '';

    // Accetta audio come multipart/form-data o application/octet-stream
    let audioBuffer: ArrayBuffer;

    if (contentType.includes('multipart/form-data')) {
      const formData = await request.formData();
      const audioFile = formData.get('audio') as File | null;
      if (!audioFile) {
        return Response.json({ error: 'audio field required in form data' }, { status: 400 });
      }
      audioBuffer = await audioFile.arrayBuffer();
    } else {
      // Raw binary audio
      audioBuffer = await request.arrayBuffer();
    }

    if (audioBuffer.byteLength === 0) {
      return Response.json({ error: 'Empty audio data' }, { status: 400 });
    }

    // Limita a 25MB (limite Whisper)
    if (audioBuffer.byteLength > 25 * 1024 * 1024) {
      return Response.json({ error: 'Audio file too large (max 25MB)' }, { status: 413 });
    }

    const result = await env.AI.run('@cf/openai/whisper', {
      audio: Array.from(new Uint8Array(audioBuffer)),
    }) as AiSpeechRecognitionOutput;

    return Response.json({
      text: result.text,
      wordCount: result.text.split(/\s+/).filter(Boolean).length,
      model: '@cf/openai/whisper',
    });
  },
};

interface Env {
  AI: Ai;
}

Text Embeddings per Semantic Search

Gli embeddings sono vettori numerici che rappresentano il significato semantico di un testo. Workers AI include modelli BGE ottimizzati per la ricerca semantica. Combinati con Vectorize (il vector database di Cloudflare), permettono di costruire pipeline RAG completamente all'edge:

// src/embedding-worker.ts - generazione embeddings + ricerca semantica

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === '/embed' && request.method === 'POST') {
      const { texts } = await request.json<EmbedRequest>();

      if (!Array.isArray(texts) || texts.length === 0) {
        return Response.json({ error: 'texts array required' }, { status: 400 });
      }

      // BGE genera embedding di 768 dimensioni (base) o 1024 (large)
      const result = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
        text: texts,
      }) as AiTextEmbeddingsOutput;

      return Response.json({
        embeddings: result.data,
        dimensions: result.data[0]?.length ?? 0,
        count: result.data.length,
        model: '@cf/baai/bge-base-en-v1.5',
      });
    }

    if (url.pathname === '/search' && request.method === 'POST') {
      const { query, topK = 5 } = await request.json<SearchRequest>();

      // 1. Genera l'embedding per la query
      const queryEmbed = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
        text: [query],
      }) as AiTextEmbeddingsOutput;

      // 2. Ricerca semantica su Vectorize
      const results = await env.VECTORIZE.query(queryEmbed.data[0], {
        topK,
        returnMetadata: 'all',
      });

      return Response.json({
        query,
        results: results.matches.map((match) => ({
          id: match.id,
          score: match.score,
          metadata: match.metadata,
        })),
      });
    }

    return new Response('Not Found', { status: 404 });
  },
};

interface EmbedRequest {
  texts: string[];
}

interface SearchRequest {
  query: string;
  topK?: number;
}

interface Env {
  AI: Ai;
  VECTORIZE: VectorizeIndex;
}

AI Gateway: Osservabilità e Caching

Cloudflare AI Gateway è un proxy trasparente che si posiziona davanti alle chiamate AI (sia Workers AI che provider esterni come OpenAI). Aggiunge caching semantico, rate limiting, logging e fallback automatico:

// src/worker-with-gateway.ts - Workers AI via AI Gateway

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { prompt } = await request.json<{ prompt: string }>();

    // Usa il gateway invece del binding diretto
    // Il gateway aggiunge: caching, retry, logging, rate limiting
    const response = await fetch(
      `https://gateway.ai.cloudflare.com/v1/${env.CF_ACCOUNT_ID}/${env.AI_GATEWAY_ID}/workers-ai/@cf/meta/llama-3.1-8b-instruct`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${env.CF_API_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          messages: [{ role: 'user', content: prompt }],
          max_tokens: 512,
        }),
      }
    );

    if (!response.ok) {
      const error = await response.text();
      return Response.json({ error }, { status: response.status });
    }

    const result = await response.json();
    return Response.json(result);
  },
};

interface Env {
  AI: Ai;
  CF_ACCOUNT_ID: string;
  CF_API_TOKEN: string;
  AI_GATEWAY_ID: string;
}

In alternativa, puoi configurare il gateway direttamente nel binding AI del wrangler.toml:

# wrangler.toml con AI Gateway
[ai]
binding = "AI"
# Il gateway viene usato automaticamente per tutte le chiamate
# Configurato nella dashboard Cloudflare

Limiti e Considerazioni sui Costi

Modello Free (Neuron Units) Paid ($ per 1K neurons) Latenza tipica
Llama 3.1 8B 10K NU/giorno gratis $0.011 / 1K NU ~500ms-2s (dipende da tokens)
Llama 3.3 70B FP8 Incluso nel piano paid $0.050 / 1K NU ~1-5s
Whisper 10K NU/giorno gratis $0.011 / 1K NU ~1-3s per minuto di audio
BGE embeddings 10K NU/giorno gratis $0.011 / 1K NU ~50-200ms
Stable Diffusion XL 10K NU/giorno gratis $0.020 / immagine ~3-10s

Timeout e CPU Limits

Workers AI opera al di fuori del normale budget CPU del Worker (30s paid plan). Tuttavia, i modelli grandi come Llama 70B possono richiedere 5-15 secondi per rispondere. In questi casi, assicurati di usare lo streaming per restituire token man mano e non superare il timeout HTTP del client. Per inferenze lunghe considera di usare una coda (Workers Queues) e notificare il client al termine.

Pattern di Produzione: RAG all'Edge

Un pattern sempre più comune è il RAG (Retrieval-Augmented Generation) interamente all'edge: Vectorize per il retrieval, Workers AI per l'embedding e la generazione.

// src/rag-worker.ts - RAG completo all'edge

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== 'POST') return new Response('POST only', { status: 405 });

    const { question } = await request.json<{ question: string }>();

    // Step 1: Genera l'embedding della domanda
    const queryEmbedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
      text: [question],
    }) as AiTextEmbeddingsOutput;

    // Step 2: Recupera i chunk rilevanti dal vector store
    const relevant = await env.DOCS.query(queryEmbedding.data[0], {
      topK: 3,
      returnMetadata: 'all',
    });

    // Step 3: Costruisce il contesto dai chunk recuperati
    const context = relevant.matches
      .map((m) => m.metadata?.['text'] as string ?? '')
      .filter(Boolean)
      .join('\n\n---\n\n');

    // Step 4: Genera la risposta con il contesto
    const answer = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
      messages: [
        {
          role: 'system',
          content: `Rispondi alla domanda basandoti SOLO sul contesto fornito.
Se il contesto non contiene informazioni sufficienti, dillo esplicitamente.
Contesto:
${context}`,
        },
        { role: 'user', content: question },
      ],
      max_tokens: 1024,
      temperature: 0.1,
    }) as AiTextGenerationOutput;

    return Response.json({
      question,
      answer: answer.response,
      sources: relevant.matches.map((m) => ({
        id: m.id,
        score: m.score,
        title: m.metadata?.['title'],
      })),
    });
  },
};

interface Env {
  AI: Ai;
  DOCS: VectorizeIndex;
}

Conclusioni e Prossimi Passi

Workers AI rappresenta un cambio di paradigma nell'accesso all'inferenza AI: nessuna GPU da gestire, nessun provider esterno da integrare, fatturazione al consumo con free tier generoso. La crescita del 4000% YoY a Q1 2026 riflette l'adozione rapida da parte di sviluppatori che cercano un percorso più semplice verso l'AI nei propri prodotti.

La combinazione Workers AI + Vectorize + Durable Objects (per la gestione dello storico delle conversazioni) permette di costruire assistenti AI completi interamente sulla piattaforma Cloudflare, senza dipendenze esterne.

Prossimi Articoli della Serie

  • Articolo 6: Vercel Edge Runtime - Middleware Avanzato, Geolocalizzazione e A/B Testing: come Vercel usa l'edge runtime con Next.js per personalizzazione e feature flags.
  • Articolo 7: Routing Geografico all'Edge - Personalizzazione Contenuti e Compliance GDPR: costruire logica geo-based senza toccare il server principale.
  • Articolo 8: Cache API e Strategie di Invalidation in Cloudflare Workers: CDN programmabile con TTL, stale-while-revalidate e purge per chiave.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti