App generative AI con Amazon Bedrock guida introduttiva per gli sviluppatori Go

Guida introduttiva per gli sviluppatori Go sull'utilizzo dell'app generative AI con Amazon Bedrock

Questo articolo è una guida introduttiva per gli sviluppatori di Go che vogliono iniziare a creare applicazioni di IA generative utilizzando Amazon Bedrock, che è un servizio completamente gestito che rende accessibili modelli di base di Amazon e fornitori di modelli di terze parti tramite un’API.

Utilizzeremo l’AWS Go SDK per Amazon Bedrock e affronteremo i seguenti argomenti lungo il percorso:

  • API Go di Amazon Bedrock e come utilizzarle per compiti come la generazione di contenuti
  • Come creare un’applicazione di chat semplice e gestire l’output in streaming dai modelli di base di Amazon Bedrock
  • Analisi del codice degli esempi

Gli esempi di codice sono disponibili in questo repository GitHub.

Prima di iniziare

Dovrai installare una versione recente di Go, se non l’hai già.

Assicurati di avere configurato e impostato Amazon Bedrock, inclusa la richiesta di accesso ai modelli di base.

Mentre eseguiamo gli esempi, utilizzeremo l’AWS Go SDK per invocare le operazioni dell’API di Amazon Bedrock dal nostro computer locale. Per fare ciò, devi:

  1. Concedere l’accesso programmatico utilizzando un utente/ruolo IAM.
  2. Concedere i seguenti permessi all’identità IAM che stai utilizzando:
{    "Version": "2012-10-17",    "Statement": [        {            "Effect": "Allow",            "Action": "bedrock:*",            "Resource": "*"        }    ]}

Nota sull’autenticazione AWS Go SDK

Se hai già utilizzato l’AWS Go SDK, ti sarà familiare. In caso contrario, tieni presente che negli esempi di codice ho utilizzato quanto segue per caricare la configurazione e specificare le credenziali per l’autenticazione:

cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region))

Quando inizializzi un’istanza di aws.Config utilizzando config.LoadDefaultConfig, l’AWS Go SDK utilizza la propria catena di credenziali predefinita per trovare le credenziali AWS. Puoi leggere i dettagli qui, ma nel mio caso, ho già un file credentials in <USER_HOME>/.aws che viene rilevato e utilizzato dal SDK.

Tipi di client Amazon Bedrock

L’AWS Go SDK per Amazon Bedrock supporta due tipi di client:

  1. Il primo, bedrock.Client, può essere utilizzato per operazioni di tipo piano di controllo come ottenere informazioni sui modelli di base, o modelli personalizzati, creare un job di perfezionamento per personalizzare un modello di base, ecc.
  2. Il bedrockruntime.Client nel pacchetto bedrockruntime viene utilizzato per eseguire inferenze sui modelli di base (questa è la parte interessante!).

Elencazione dei modelli di base di Amazon Bedrock

Per iniziare, diamo un’occhiata a un esempio semplice del client piano di controllo per elencare i modelli di base in Amazon Bedrock (omessa la gestione degli errori e la registrazione):

    region := os.Getenv("AWS_REGION")    if region == "" {        region = defaultRegion    }    cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region))    bc := bedrock.NewFromConfig(cfg)    fms, err := bc.ListFoundationModels(context.Background(), &bedrock.ListFoundationModelsInput{        //ByProvider: aws.String("Amazon"),        //ByOutputModality: types.ModelModalityText,    })    for _, fm := range fms.ModelSummaries {        info := fmt.Sprintf("Nome: %s | Provider: %s | Id: %s", *fm.ModelName, *fm.ProviderName, *fm.ModelId)        fmt.Println(info)    }

Criamo un’istanza bedrock.Client e la usiamo per ottenere i modelli Foundation supportati in Amazon Bedrock utilizzando l’API ListFoundationModels.

Clonare la repository di GitHub e cambiare la directory corretta:

git clone https://github.com/build-on-aws/amazon-bedrock-go-sdk-examplescd amazon-bedrock-go-sdk-examplesgo mod tidy

Eseguire questo esempio:

go run bedrock-basic/main.go

Dovresti vedere l’elenco dei modelli Foundation supportati.

Nota che puoi anche filtrare per provider, modalità (input/output), ecc. specificandolo in ListFoundationModelsInput.

Chiamare il modello per l’inferenza (con API bedrockruntime)

Iniziamo usando il modello Anthropic Claude (v2). Ecco un esempio di una semplice sceneggiatura di generazione di contenuti con il seguente prompt:

<paragraph> "Nel 1758, il botanico e zoologo svedese Carl Linneo pubblicò nel suo Systema Naturae, la denominazione delle specie a due parole (Nomenclatura binomiale). Canis è la parola latina che significa "cane", e sotto questo genere, elencò il cane domestico, il lupo e lo sciacallo dorato."</paragraph>Si prega di riadattare il paragrafo sopra in modo che risulti comprensibile per un alunno di quinta elementare.Prego inserire la riformulazione all'interno dei tag <rewrite></rewrite>.

Per eseguire il programma:

go run claude-content-generation/main.go

L’output potrebbe differire leggermente nel tuo caso, ma dovrebbe essere in qualche modo simile a questo:

<rewrite>Carl Linneo era uno scienziato svedese che studiava piante e animali. Nel 1758, pubblicò un libro chiamato Systema Naturae in cui dava a tutte le specie nomi composti da due parole. Ad esempio, chiamava i cani Canis familiaris. Canis è la parola latina per cane. Sotto il nome di Canis, Linneo elencava il cane domestico, il lupo e lo sciacallo dorato. Quindi usava la prima parola Canis per raggruppare animali strettamente correlati come cani, lupi e sciacalli. Questo modo di denominare le specie con due parole è chiamato nomenclatura binomiale e viene ancora utilizzato dagli scienziati oggi.</rewrite>

Ecco uno snippet di codice (senza la gestione degli errori, ecc.).

    //...    brc := bedrockruntime.NewFromConfig(cfg)    payload := {        Prompt:            fmt.Sprintf(claudePromptFormat, prompt),        MaxTokensToSample: 2048,        Temperature:       0.5,        TopK:              250,        TopP:              1,    }    payloadBytes, err := json.Marshal(payload)    output, err := brc.InvokeModel(context.Background(), &bedrockruntime.InvokeModelInput{        Body:        payloadBytes,        ModelId:     aws.String(claudeV2ModelID),        ContentType: aws.String("application/json"),    })    var resp Response    err = json.Unmarshal(output.Body, &resp)    //.....

Ottieniamo l’istanza bedrockruntime.Client e creiamo il payload contenente la richiesta che dobbiamo inviare ad Amazon Bedrock (che include anche il prompt). Il payload è formattato in JSON e i suoi dettagli sono ben documentati qui: Parametri di inferenza per i modelli foundation.

Quindi includiamo il payload nella chiamata InvokeModel. Nota il ModelId nella chiamata che puoi ottenere dall’elenco degli ID dei modelli di base. La risposta JSON viene quindi convertita in una struttura Response.

Si noti che questo “workflow” (preparazione del payload con prompt, marshaling del payload, invocazione del modello e unmarshaling) sarà comune in tutti i nostri esempi (e molto probabilmente nelle tuoi applicazioni) con piccoli cambiamenti in base al modello/caso d’uso.

Puoi anche provare uno scenario di estrazione di informazioni utilizzando questo prompt:

<directory>Rubrica telefonica: John Latrabe, 800-232-1995, [email protected] Josie Lana, 800-759-2905, [email protected] Keven Stevens, 800-980-7000, [email protected] La rubrica telefonica verrà aggiornata dal responsabile HR."<directory>Si prega di restituire gli indirizzi email all'interno della rubrica, uno per riga, nell'ordine in cui appaiono nel testo. Se non ci sono indirizzi email nel testo, restituire "N/A".

Per eseguire il programma:

go run claude-information-extraction/main.go

Chat: un esempio canonico di GenAI

Non possiamo avere un articolo su GenAI senza un’applicazione di chat, giusto?

Continuando con il modello Claude, vediamo un esempio di conversazione. Mentre è possibile scambiarsi messaggi isolati, questo esempio mostra come scambiare più messaggi (chat) e conservare la cronologia della conversazione.

Dato che si tratta di una semplice implementazione, lo stato viene mantenuto in memoria.

Per eseguire l’applicazione:

go run claude-chat/main.go # Se desideri registrare i messaggi scambiati con LLM, # esegui il programma in modalità verbose go run claude-chat/main.go --verbose

Ecco un esempio di conversazione che ho fatto. Nota come l’ultima risposta viene generata in base alle risposte precedenti, grazie alla conservazione della cronologia della chat:

Usare l’API di streaming

Nell’esempio precedente di chat, avresti dovuto aspettare alcuni secondi per ottenere l’intera risposta. Questo perché il processo è completamente sincrono: invoca il modello e attendi la risposta completa.

L’API InvokeModelWithResponseStream ci consente di adottare un approccio asincrono, anche chiamato Streaming. Questo è utile se si desidera visualizzare la risposta all’utente o elaborare la risposta mentre viene generata; ciò fornisce un’esperienza “responsiva” all’applicazione.

Per provarlo, utilizziamo lo stesso prompt dell’esempio di generazione di contenuti in quanto genera una risposta abbastanza lunga da poter vedere lo streaming in azione.

  <rewrite>Carl Linnaeus era uno scienziato svedese che studiava piante e animali. Nel 1758, pubblicò un libro chiamato Systema Naturae in cui dava a tutte le specie dei nomi composti da due parole. Ad esempio, chiamava i cani Canis familiaris. Canis è la parola latina per cane. Sotto il nome Canis, Linneo elencava il cane domestico, il lupo e lo sciacallo dorato. In questo modo usava la prima parola Canis per raggruppare animali strettamente correlati come cani, lupi e sciacalli. Questo modo di nominare le specie con due parole è chiamato nomenclatura binomiale e viene ancora utilizzato dagli scienziati oggi.</rewrite>

Esegui l’applicazione:

go run streaming-claude-basic/main.go

Dovresti vedere l’output scritto sulla console mentre le parti vengono generate da Amazon Bedrock.

Diamo un’occhiata al codice.

Ecco la prima parte: business as usual. Creiamo un payload con il prompt (e i parametri) e chiamiamo l’API InvokeModelWithResponseStream, che restituisce un’istanza di bedrockruntime.InvokeModelWithResponseStreamOutput.

    //...    brc := bedrockruntime.NewFromConfig(cfg)    payload := Request{        Prompt:            fmt.Sprintf(claudePromptFormat, prompt),        MaxTokensToSample: 2048,        Temperature:       0.5,        TopK:              250,        TopP:              1,    }    payloadBytes, err := json.Marshal(payload)    output, err := brc.InvokeModelWithResponseStream(context.Background(), &bedrockruntime.InvokeModelWithResponseStreamInput{        Body:        payloadBytes,        ModelId:     aws.String(claudeV2ModelID),        ContentType: aws.String("application/json"),    })    //....

La parte successiva è diversa rispetto all’approccio sincrono con l’API InvokeModel. Poiché l’istanza InvokeModelWithResponseStreamOutput non ha la risposta completa (ancora), non possiamo (o non dovremmo) semplicemente restituirla al chiamante. Invece, optiamo per elaborare questo output pezzo per pezzo con la funzione processStreamingOutput.

La funzione passata a essa è del tipo type StreamingOutputHandler func(ctx context.Context, part []byte) error che è un tipo personalizzato che ho definito per fornire un modo per specificare all’applicazione chiamante come gestire i frammenti di output, in questo caso, semplicemente stampiamo sulla console (stdout).

    //...    _, err = processStreamingOutput(output, func(ctx context.Context, part []byte) error {        fmt.Print(string(part))        return nil    })    //...

Dai un’occhiata a cosa fa la funzione processStreamingOutput (alcune parti del codice sono omesse per brevità). InvokeModelWithResponseStreamOutput ci fornisce accesso a un canale di eventi (di tipo types.ResponseStream) che contiene il payload dell’evento. Questo non è altro che una stringa formattata JSON con la risposta parzialmente generata dall’LLM; la convertiamo in una struttura Response.

Invochiamo la funzione handler (stampa la risposta parziale sulla console) e ci assicuriamo di continuare a costruire anche la risposta completa aggiungendo i frammenti parziali. La risposta completa viene infine restituita dalla funzione.

func processStreamingOutput(output *bedrockruntime.InvokeModelWithResponseStreamOutput, handler StreamingOutputHandler) (Response, error) {    var combinedResult string    resp := Response{}    for event := range output.GetStream().Events() {        switch v := event.(type) {        case *types.ResponseStreamMemberChunk:            var resp Response            err := json.NewDecoder(bytes.NewReader(v.Value.Bytes)).Decode(&resp)            if err != nil {                return resp, err            }            handler(context.Background(), []byte(resp.Completion))            combinedResult += resp.Completion            //....    }    resp.Completion = combinedResult    return resp, nil}

Responsive Chat Application, Grazie all’API in Streaming

Ora che hai capito come e perché gestire le risposte in streaming, la nostra semplice app di chat è il candidato perfetto per utilizzare questo!

Non ripercorrerò ancora una volta il codice. Ho aggiornato l’applicazione di chat per utilizzare l’API InvokeModelWithResponseStream e gestire le risposte come nell’esempio precedente.

Per eseguire la nuova versione dell’app:

go run claude-chat-streaming/main.go

Fino ad ora abbiamo usato il modello Anthropic Claude v2. Puoi anche provare l’esempio del modello Cohere per la generazione di testo. Per eseguirlo: go run cohere-text-generation/main.go

Generazione di immagini

La generazione di immagini è un altro caso d’uso fondamentale dell’IA Generativa! Questo esempio utilizza il modello Stable Diffusion XL in Amazon Bedrock per generare un’immagine data una frase di input e altri parametri.

Per provarlo:

go run stablediffusion-image-gen/main.go "<la tua frase di input>"# per esempio go run stablediffusion-image-gen/main.go "Piantagione di tè in Sri Lanka"go run stablediffusion-image-gen/main.go "Razzo che si lancia dalla foresta con giardino di fiori sotto un cielo blu, maestoso, ghibli"

Dovresti vedere un file JPG di output generato.

Ecco una veloce spiegazione del codice (senza la gestione degli errori, ecc.).

Il payload di output dalla chiamata a InvokeModel viene convertito in una struttura Response che viene ulteriormente scomposta per estrarre l’immagine in formato base64 (codificata come []byte) e decodificata utilizzando encoding/base64 e scrivere il risultato finale come []byte in un file di output (formato output-<timestamp>.jpg).

    //...    brc := bedrockruntime.NewFromConfig(cfg)    prompt := os.Args[1]    payload := Request{        TextPrompts: []TextPrompt{{Text: prompt}},        CfgScale:    10,        Seed:        0,        Steps:       50,    }    payloadBytes, err := json.Marshal(payload)    output, err := brc.InvokeModel(context.Background(), &bedrockruntime.InvokeModelInput{        Body:        payloadBytes,        ModelId:     aws.String(stableDiffusionXLModelID),        ContentType: aws.String("application/json"),    })    var resp Response    err = json.Unmarshal(output.Body, &resp)    decoded, err := resp.Artifacts[0].DecodeImage()    outputFile := fmt.Sprintf("output-%d.jpg", time.Now().Unix())    err = os.WriteFile(outputFile, decoded, 0644)    //...

Nota i parametri del modello (CfgScale, Seed e Steps); i loro valori dipendono dal caso d’uso. Ad esempio, CfgScale determina quanto l’immagine finale rispecchia la frase di input: utilizzare un numero più basso per aumentare la casualità nella generazione. Consulta la documentazione di Amazon Bedrock sugli Inference Parameters per ulteriori dettagli.

Creare Embeddings da Testo

Gli embeddings di testo rappresentano rappresentazioni vettoriali significative di testo non strutturato come documenti, paragrafi e frasi. Al momento, Amazon Bedrock supporta il modello Titan Embeddings G1 - Text per gli embeddings di testo. Supporta il recupero del testo, la similarità semantica e il clustering. Il testo di input massimo è di 8K token e la lunghezza massima del vettore di output è di 1536.

Per eseguire l’esempio:

go run titan-text-embedding/main.go "<il tuo input>"# per esempio, go run titan-text-embedding/main.go "gatto"go run titan-text-embedding/main.go "cane"go run titan-text-embedding/main.go "trex"

Probabilmente questo è il risultato meno eccitante che vedrai! La verità è che è difficile capire qualcosa guardando una serie di numeri float64.

Diventa più rilevante quando combinato con altri componenti come un Vector Database (per memorizzare questi embeddings) e casi d’uso come la ricerca semantica (per sfruttare questi embeddings). Questi argomenti saranno trattati in futuri articoli del blog. Per ora, accontentati del fatto che “funziona”.

Considerazioni finali

Spero che questo sia utile per gli sviluppatori Go come punto di partenza su come utilizzare i modelli Foundation su Amazon Bedrock per alimentare applicazioni di IA Generativa.

State attenti a ulteriori articoli che coprono argomenti di IA generativa per sviluppatori Go. Nel frattempo, buona costruzione!