LLM e JavaScript:
approcci pratici

Rappresentazione astratta di reti neurali e architettura Transformer per l'intelligenza artificiale

La rivoluzione dei modelli generativi: un caso d'uso con Transformers.js

Negli ultimi anni i modelli generativi basati su reti neurali profonde hanno rivoluzionato il panorama tecnologico. Inizialmente sviluppati per fini accademici, sono diventati parte integrante di applicazioni quotidiane, dalla generazione automatica di contenuti alla traduzione multilingue, fino al supporto decisionale avanzato.

La rapidità con cui queste tecnologie vengono adottate e integrate nei sistemi esistenti è sorprendente. Per accelerare ulteriormente questo processo e rendere accessibili concetti complessi come l'utilizzo, il training e l'implementazione dei modelli linguistici (LLM), è fondamentale adottare approcci pratici che si adattino al nostro modo di progettare e sviluppare soluzioni tecnologiche.

In questo articolo affronteremo l'integrazione di modelli linguistici in architetture client e server, utilizzando la libreria Transformers.js di Xenova. Sfrutteremo funzionalità avanzate di intelligenza artificiale sia direttamente all’interno di una web app standalone - senza necessità di server esterni - sia in un server backend tramite endpoint API, basato sul nostro progetto open source @volcanicminds/backend.


Perché JavaScript?

Sebbene Python sia il linguaggio dominante nel mondo dell'AI, JavaScript offre alcune caratteristiche uniche che lo rendono una scelta vincente in determinati contesti:

  • esecuzione diretta nel browser: consente di eseguire modelli AI lato client, eliminando la necessità di server esterni e riducendo la latenza nelle risposte;
  • flessibilità per il backend: grazie a tecnologie come Node.js, JavaScript permette di creare API efficienti per gestire modelli più complessi su server;
  • integrazione fluida: si adatta facilmente a infrastrutture esistenti, semplificando l'inserimento di modelli generativi nelle web app;
  • rapidità nello sviluppo: favorisce la creazione di prototipi grazie alla compatibilità con browser moderni e librerie come Transformers.js.

Questa versatilità rende JavaScript una scelta strategica per chi desidera integrare modelli generativi in applicazioni web moderne, offrendo sia la possibilità di eseguire l’AI direttamente nel browser, sia di sviluppare soluzioni scalabili su infrastrutture server.


La libreria Transformers.js

Transformers.js è un porting JavaScript della famosa libreria Transformers di HuggingFace. Questa versione consente di:

  • eseguire inferenze direttamente nel browser senza server esterni;
  • integrare modelli pre-addestrati in applicazioni esistenti;
  • sfruttare una community attiva che fornisce modelli, strumenti e supporto per lo sviluppo.

La combinazione di queste caratteristiche rende Transformers.js uno strumento potente per creare applicazioni AI innovative in contesti client-side o server-side.


Caso pratico: web app standalone

La web app dimostrativa utilizza Transformers.js per eseguire inferenze direttamente nel browser. Una volta installata ed eseguita, l'applicazione è accessibile all'indirizzo http://localhost:5173. Il codice principale che dialoga con il modello si trova in src/transformers/worker.js:

import { pipeline, env } from '@xenova/transformers'

env.allowLocalModels = false

/**
 * This class uses the Singleton pattern to ensure that only one instance of the pipeline is loaded.
 */
class SamplePipeline {
  static task = 'text-generation'
  static model = 'Xenova/gpt2'
  static instance = null

  static async getInstance(progress_callback = null) {
    if (this.instance === null) {
      this.instance = pipeline(this.task, this.model, { progress_callback })
    }

    return this.instance
  }
}

// Listen for messages from the main thread
self.addEventListener('message', async (event) => {
  const { model, task, text, ...rest } = event.data

  if (SamplePipeline.model !== model || SamplePipeline.task !== task) {
    // Invalidate model if different
    SamplePipeline.model = model || SamplePipeline.model
    SamplePipeline.task = task || SamplePipeline.task

    if (SamplePipeline.instance !== null) {
      ;(await SamplePipeline.getInstance()).dispose()
      SamplePipeline.instance = null
    }
  }

  // Retrieve the code-completion pipeline. When called for the first time,
  // this will load the pipeline and save it for future use.
  const generator = await SamplePipeline.getInstance((x) => {
    // We also add a progress callback to the pipeline so that we can
    // track model loading.
    self.postMessage(x)
  })

  // Actually perform the code-completion
  let output = await generator(text, {
    ...rest,

    // Allows for partial output
    callback_function: (x) => {
      self.postMessage({
        status: 'update',
        output: generator.tokenizer.decode(x[0].output_token_ids, { skip_special_tokens: true })
      })
    }
  })

  // Send the output back to the main thread
  self.postMessage({
    status: 'complete',
    output: output
  })
})

Flusso di lavoro

  1. l'utente invia un messaggio attraverso l'interfaccia della web app;
  2. il messaggio viene passato a un web worker;
  3. il web worker elabora la richiesta utilizzando un modello LLM predefinito (in questo caso, Xenova/distilgpt2);
  4. il modello genera una risposta che viene visualizzata nell'interfaccia.

Preview di risposta LLM chatbot Volcanic Minds RAG

Preview di risposta LLM

Questo approccio è ideale per applicazioni che richiedono risposte rapide senza dipendere da server esterni. Tuttavia, l'utilizzo di un modello leggero come distilgpt2 implica alcune limitazioni in termini di qualità dell'output.


Caso pratico: backend API

Il progetto backend utilizza un'architettura basata su Fastify e TypeORM per fornire un endpoint API che sfrutta Transformers.js. Il codice relativo alla generazione del testo si trova in src/api/pipeline/controller/TextGenerationPipeline.ts:

export class TextGenerationPipeline {
  static task = 'text-generation'
  static model = 'Xenova/distilgpt2'
  static instance = null

  static async getInstance(progress_callback = null) {
    if (this.instance === null) {
      const { pipeline } = await Function('return import("@xenova/transformers")')()
      this.instance = pipeline(this.task, this.model, { progress_callback })
    }

    return this.instance
  }

  static async execute({ text }) {
    const executor: any = await this.getInstance()
    let answer = await executor(text, {
      temperature: 2,
      max_new_tokens: 50,
      repetition_penalty: 5,
      no_repeat_ngram_size: 2,
      num_beams: 2,
      num_return_sequences: 1
    })
    answer = answer?.length > 0 ? answer[0] : answer
    return answer?.generated_text || answer
  }
}

Flusso di lavoro

  1. una richiesta viene inviata all'endpoint API con un input di testo;
  2. l'API elabora l'input utilizzando un modello predefinito (anche in questo caso, Xenova/distilgpt2 per semplicità);
  3. la risposta generata viene restituita al client.

Preview di risposta chatbot agente LLM postman Volcanic Minds RAG

Risposta Postman

Questo approccio è ideale per applicazioni centralizzate, dove il carico di elaborazione può essere gestito su server più potenti. Ad esempio, modelli più complessi e performanti possono essere integrati per migliorare la qualità delle risposte.


Confronto tra web app e backend

I due approcci offrono vantaggi complementari:

  • web app standalone: eliminando la necessità di un server, consente una maggiore autonomia del client. Tuttavia, le risorse limitate del browser possono rappresentare un ostacolo per modelli complessi;
  • backend API: centralizza l'elaborazione, consentendo di utilizzare modelli più avanzati. È particolarmente adatto per scenari in cui le risorse hardware non sono un vincolo.

Una soluzione ibrida, in cui i task vengono distribuiti tra client e server, potrebbe rappresentare un equilibrio ottimale, ma richiede uno studio approfondito per gestire la comunicazione e il carico di lavoro.


Conclusione

I progetti dimostrativi illustrano come integrare funzionalità generative utilizzando Transformers.js sia in un contesto completamente client-side sia in un'architettura server-side. Anche se non pronti per la produzione, questi esempi rappresentano un punto di partenza per sviluppatori interessati a esplorare l'uso di modelli generativi in applicazioni reali.

Per ulteriori informazioni si possono consultare le seguenti risorse:

Con la continua evoluzione dei modelli generativi e delle librerie che li supportano, le possibilità di innovazione sono pressoché infinite. Che si tratti di una web app leggera o di un backend robusto, l'importante è iniziare a sperimentare e contribuire all'ecosistema in crescita dell'intelligenza artificiale.

Condividi l'articolo

Tag: TecnologiaAI

Data di pubblicazione: 15 gennaio 2025

Ultima revisione: 16 gennaio 2025