Come creare un assistente AI con OpenAI + Python |  di Shaw Talebi |  Febbraio 2024

 | Intelligenza-Artificiale

Prima di immergermi nel codice di esempio, voglio differenziare brevemente un chatbot AI da un assistente. Sebbene questi termini siano spesso usati in modo intercambiabile, qui li uso per significare cose diverse.

UN chatbot È un’intelligenza artificiale con cui puoi conversarementre un Assistente AI È un chatbot che può utilizzare strumenti. Uno strumento può essere qualcosa come la navigazione web, una calcolatrice, un interprete Python o qualsiasi altra cosa che espanda le capacità di un chatbot (1).

Ad esempio, se utilizzi la versione gratuita di ChatGPT, quello è un chatbot perché include solo la funzionalità di chat di base. Tuttavia, se utilizzi la versione premium di ChatGPT, questo è un assistente perché include funzionalità come la navigazione web, il recupero della conoscenza e la generazione di immagini.

Sebbene la creazione di assistenti AI (ovvero agenti AI) non sia un’idea nuova, la nuova API Assistants di OpenAI fornisce un modo semplice per creare questi tipi di AI. In questo caso utilizzerò l’API per creare un risponditore ai commenti di YouTube dotato di recupero della conoscenza (ad esempio RAG) da uno dei miei articoli su Medium. Il seguente codice di esempio è disponibile in questo post Repositorio GitHub.

Assistente alla vaniglia

Iniziamo importando le librerie Python e impostando la comunicazione con l’API OpenAI.

from openai import OpenAI
from sk import my_sk # import secret key from .py file

client = OpenAI(api_key=my_sk)

Tieni presente che per questo passaggio è necessaria una chiave API OpenAI. Se non disponi di una chiave API o non sai come ottenerne una, spiegherò come farlo in un articolo precedente. Qui, ho la mia chiave segreta definita in un file Python separato chiamato sk.py, che è stato importato nel blocco di codice precedente.

Ora possiamo creare un assistente di base (tecnicamente un chatbot poiché non ci sono ancora strumenti). Questo può essere fatto in una riga di codice, ma ne uso qualcuna in più per la leggibilità.

intstructions_string = "ShawGPT, functioning as a virtual data science \
consultant on YouTube, communicates in clear, accessible language, escalating \
to technical depth upon request. \
It reacts to feedback aptly and concludes with its signature '–ShawGPT'. \
ShawGPT will tailor the length of its responses to match the viewer's comment, \
providing concise acknowledgments to brief expressions of gratitude or \
feedback, thus keeping the interaction natural and engaging."

assistant = client.beta.assistants.create(
name="ShawGPT",
description="Data scientist GPT for YouTube comments",
instructions=intstructions_string,
model="gpt-4-0125-preview"
)

Come mostrato sopra, possiamo impostare l’assistente nome, descrizione, IstruzioniE modello. Gli input più rilevanti per la performance dell’assistente sono i Istruzioni E modello. Sviluppare buone istruzioni (es ingegneria tempestiva) è un processo iterativo su cui vale la pena dedicare un po’ di tempo. Inoltre, utilizzo l’ultima versione disponibile di GPT-4. Tuttavia, lo sono anche i modelli più vecchi (e più economici). disponibile (2).

Con la configurazione “assistente”, possiamo inviargli un messaggio per generare una risposta. Questo viene fatto nel blocco di codice seguente.

# create thread (i.e. object that handles conversation between user and assistant)
thread = client.beta.threads.create()

# add a user message to the thread
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="Great content, thank you!"
)

# send message to assistant to generate a response
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
)

Stanno accadendo alcune cose nel blocco di codice sopra. Primocreiamo un oggetto thread. Questo gestisce lo scambio di messaggi tra utente e assistente, evitando così la necessità di scrivere codice boilerplate per farlo. Prossimoaggiungiamo un messaggio utente al thread. Questi sono i commenti di YouTube per il nostro caso d’uso. Poiinfine, inviamo il thread all’assistente per generare una risposta tramite l’oggetto run.

Dopo alcuni secondi, otteniamo la seguente risposta dall’assistente:

You're welcome! I'm glad you found it helpful. If you have any more questions 
or topics you're curious about, feel free to ask. –ShawGPT

Anche se questa potrebbe sembrare una bella risposta, non è qualcosa che direi mai. Vediamo come possiamo migliorare l’assistente tramite il cosiddetto suggerimento a pochi colpi.

Richiesta di pochi colpi

Il suggerimento per pochi colpi è dove includiamo esempi di input-output nelle istruzioni dell’assistente da cui può imparare. Qui aggiungo 3 commenti e risposte (reali) alla stringa di istruzioni precedente.

intstructions_string_few_shot = """ShawGPT, functioning as a virtual data \
science consultant on YouTube, communicates in clear, accessible language, \
escalating to technical depth upon request. \
It reacts to feedback aptly and concludes with its signature '–ShawGPT'. \
ShawGPT will tailor the length of its responses to match the viewer's comment, \
providing concise acknowledgments to brief expressions of gratitude or \
feedback, thus keeping the interaction natural and engaging.

Here are examples of ShawGPT responding to viewer comments.

Viewer comment: This was a very thorough introduction to LLMs and answered many questions I had. Thank you.
ShawGPT: Great to hear, glad it was helpful :) -ShawGPT

Viewer comment: Epic, very useful for my BCI class
ShawGPT: Thanks, glad to hear! -ShawGPT

Viewer comment: Honestly the most straightforward explanation I've ever watched. Super excellent work Shaw. Thank you. It's so rare to find good communicators like you!
ShawGPT: Thanks, glad it was clear -ShawGPT"""

Nota che ho aggiunto la firma “— ShawGPT” ad ogni esempio (anche se quelli non facevano parte del mio effettivo risposta). Questo è importante per ottenere il comportamento desiderato dall’assistente.

Con le istruzioni aggiornate, possiamo creare un nuovo assistente nello stesso modo di prima.

assistant = client.beta.assistants.create(
name="ShawGPT",
description="Data scientist GPT for YouTube comments",
instructions=intstructions_string_few_shot,
model="gpt-4-0125-preview"
)

Superando lo stesso commento di prova (ovvero “Ottimo contenuto, grazie!”) a questo assistente come prima, ora otteniamo la seguente risposta:

You're welcome, happy to hear you found it useful! -ShawGPT

Anche se è (ancora) un po’ più lunga della mia risposta tipica, è molto meglio di quanto abbiamo visto con l’assistente Vanilla.

Proviamo con un’altra domanda (più tecnica). Possiamo usare la stessa sintassi di prima per passare il messaggio dell’utente all’assistente, ma ora chiediamo: “Cos’è la coda grassa?

Ecco la risposta dell’assistente.

Fat-tailedness is a concept from statistics that refers to the properties of a 
particular type of probability distribution. In a fat-tailed distribution, the
tails (the far ends of the distribution, representing extreme values far from
the mean) are heavier or "fatter" than they would be in a normal (Gaussian)
distribution. This means there's a higher likelihood of extreme outcomes
occurring compared to what you would expect if the data followed a normal
distribution.

In practical terms, if a dataset is described as having a fat-tailed
distribution, it indicates that rare, extreme events (the ones in the tails)
happen more frequently than would be predicted by models based on normal
distributions. This concept is extremely important in fields like finance,
economics, and risk management, where underestimating the likelihood of extreme
events can have significant consequences.

Fat-tailed distributions contrast with thin-tailed distributions, like the
normal distribution, where extreme values are less likely, and most of the
probabilities are concentrated around the mean. Understanding whether a dataset
follows a fat-tailed distribution can significantly impact how we model and
predict phenomena, especially when it comes to assessing risks and anticipating
rare but impactful events. -ShawGPT

Sebbene (di nuovo) questa risposta sia ragionevole, sembra più qualcosa di ChatGPT di quello che direi. Vale a dire, utilizza una diversa definizione di code grasse (cioè una distribuzione con code più pesanti rispetto a una distribuzione normale) rispetto a quella che utilizzo in la mia serie sull’argomento (ovvero una distribuzione guidata da eventi estremi).

Per migliorare questo aspetto delle risposte dell’assistente, possiamo dargli la possibilità di accedere alla conoscenza da a Articolo medio Ho scritto sulla coda grassa.

Recupero della conoscenza

Mentre l’impostazione di un sistema RAG consiste in una manciata di passaggi non banali (ad esempio suddivisione di documenti, impostazione di un database vettoriale, creazione di funzioni di ricerca semantica, fusione dei risultati della ricerca in una finestra di contesto), il l’intero processo è ridotto a 2 passaggi nell’API degli assistenti.

Primocarichiamo i nostri documenti per il recupero. Secondoabbiamo aggiunto la funzionalità di recupero al nostro assistente.

Nel blocco di codice seguente, carico il mio articolo su quantificare le code grasse al mio account OpenAI. Per fare ciò, dobbiamo specificare il percorso/nome del file e lo scopo del file. Qui, stabiliamo lo scopo come “assistenti”. L’altra opzione è “sintonizzare”, che vedremo presto.

file = client.files.create(
file=open("articles/4 Ways to Quantify Fat Tails with Python _ by Shaw Talebi _ Towards Data Science.pdf", "rb"),
purpose="assistants"
)

Successivamente, possiamo creare un nuovo assistente simile a quello precedente, ma che ora consente il recupero e specifica i file disponibili per l’assistente. Anche se qui utilizzo solo un documento per il recupero, possiamo specificare fino a 20 file includendo gli ID file associati nel file file_id elenco.

assistant = client.beta.assistants.create(
name="ShawGPT",
description="Data scientist GPT for YouTube comments",
instructions=intstructions_string_few_shot,
tools=({"type": "retrieval"}),
file_ids=(file.id),
model="gpt-4-0125-preview"
)

Passando la stessa domanda tecnica all’assistente, otteniamo la seguente risposta.

Fat-tailedness describes the degree to which rare events significantly 
contribute to the aggregate statistics of a distribution. This concept
highlights that in certain distributions, a small number of observations can
disproportionately influence the overall picture. A classic example is the
Pareto distribution, often referenced in the context of wealth distribution,
where a small proportion of individuals can hold the majority of wealth. In
statistical terms, distributions range from thin-tailed (e.g., Gaussian, where
extreme events are highly improbable) to very fat-tailed (e.g., Pareto, where
extreme events are less rare than in Gaussian distributions).

Fat-tailedness is not about a binary categorization of distributions as either
"Power Law" or not but recognizes a continuum or spectrum of "tail heaviness."
This understanding offers a more nuanced view, allowing for more accurate and
precise categorization of data distributions. Identifying and quantifying the
fat-tailed nature of a dataset is important in fields like finance, insurance,
risk management, and social sciences, where understanding the likelihood and
impact of extreme events is crucial.

In practical terms, fat-tailedness can be quantified using different heuristics,
which may involve mathematical computations such as the Power Law Tail Index,
Kurtosis, Log-normal's σ, and Taleb's κ metric. Each of these measures offers
a different lens through which to assess the fat-tailed nature of a
distribution, thereby providing useful insights into the behavior of extreme
events within the dataset -ShawGPT

Questa risposta è molto più vicina al modo in cui penso (e spiego) coda grassa. L’assistente ha svolto un lavoro impeccabile incorporando i concetti chiave da l’articolo nella sua risposta. Ad esempio, definendo la coda grassa in termini di eventi rari, la coda grassa che vive su uno spettro e quattro euristiche per misurarli.

Fino a questo punto, siamo andati abbastanza lontano utilizzando ingegneria tempestiva E recupero della conoscenza per creare il nostro assistente. Tuttavia, le risposte non sembrano ancora del tutto come qualcosa che scriverei. Per migliorare ulteriormente questo aspetto dell’assistente, possiamo ricorrere alla messa a punto.

Mentre ingegneria tempestiva può essere un modo semplice per programmare un assistente, lo è non sempre ovvio come istruire al meglio il modello per dimostrare il comportamento desiderato. In queste situazioni può essere vantaggioso mettere a punto il modello.

Ritocchi è quando noi addestrare un modello preesistente con esempi aggiuntivi per un compito particolare. Nell’API di ottimizzazione di OpenAI ciò consiste nel fornire esempi di coppie di messaggi utente-assistente (3).

Per il caso d’uso del risponditore dei commenti di YouTube, ciò significa raccogliere coppie di commenti degli spettatori (ad esempio, messaggio dell’utente) e le relative risposte associate (ad esempio, messaggio dell’assistente).

Sebbene questo ulteriore processo di raccolta dei dati richieda più lavoro di messa a punto in anticipo, esso può portare a miglioramenti significativi nella prestazione del modello (3). Qui, esaminerò il processo di messa a punto per questo particolare caso d’uso.

Preparazione dei dati

Per generare le coppie di messaggi utente-assistente, ho esaminato manualmente i commenti precedenti di YouTube e li ho copiati e incollati in un foglio di calcolo. Ho quindi esportato questo foglio di calcolo come file .csv (disponibile all’indirizzo Deposito GitHub).

Sebbene questo file .csv contenga tutti i dati critici necessari per la messa a punto, non può essere utilizzato direttamente. Dobbiamo prima trasformarlo in un formato particolare per passarlo all’API OpenAI.

Più specificamente, dobbiamo generare un file file .jsonlun file di testo in cui ogni riga corrisponde a un esempio di training in formato JSON. Se sei un utente Python che non ha familiarità con JSON, puoi considerarlo come un dizionario (ovvero una struttura dati composta da coppie chiave-valore) (4).

Per ottenere il nostro .csv nel formato .jsonl necessario, creo innanzitutto elenchi Python per ogni tipo di commento. Questo viene fatto leggendo il file raw .csv riga per riga e memorizzando ciascun messaggio nell’elenco appropriato.

import csv
import json
import random

comment_list = ()
response_list = ()

with open('data/YT-comments.csv', mode ='r') as file:
file = csv.reader(file)

# read file line by line
for line in file:
# skip first line
if line(0)=='Comment':
continue

# append comments and responses to respective lists
comment_list.append(line(0))
response_list.append(line(1) + " -ShawGPT")

Successivamente, per creare il file .jsonl, dobbiamo creare un elenco di dizionari in cui ogni elemento corrisponde a un esempio di training. IL chiave per ciascuno di questi dizionari è “messaggi“, e il valore è (ancora un altro) elenco dei dizionari corrispondenti rispettivamente ai messaggi di sistema, utente e assistente. Di seguito viene fornita una panoramica visiva di questa struttura dati.

Panoramica della regolazione fine del formato dei dati di addestramento (4). Immagine dell’autore.

Il codice Python per prendere our lista_commenti E lista_risposte oggetti e la creazione dell’elenco di esempi è riportata di seguito. Questo viene fatto passando attraverso lista_commenti E lista_risposteelemento per elemento e creando tre dizionari ad ogni passaggio.

Corrispondono rispettivamente ai messaggi di sistema, utente e assistente, dove il messaggio di sistema è le stesse istruzioni che abbiamo utilizzato per creare il nostro assistente tramite prompt di pochi passaggi e i messaggi utente/assistente provengono dai rispettivi elenchi. Questi dizionari vengono quindi archiviati in un elenco che funge da valore per quel particolare esempio di formazione.

example_list = ()

for i in range(len(comment_list)):
# create dictionaries for each role/message
system_dict = {"role": "system", "content": intstructions_string_few_shot}
user_dict = {"role": "user", "content": comment_list(i)}
assistant_dict = {"role": "assistant", "content": response_list(i)}

# store dictionaries into list
messages_list = (system_dict, user_dict, assistant_dict)

# create dictionary for ith example and add it to example_list
example_list.append({"messages": messages_list})

Alla fine di questo processo, abbiamo una lista con 59 elementi corrispondenti a 59 coppie di esempio utente-assistente. Un altro passaggio che aiuta a valutare le prestazioni del modello è dividere questi 59 esempi in due set di dati, uno per addestrare il modello e l’altro per valutandone le prestazioni.

Questo viene fatto nel blocco di codice seguente, da cui campione casualmente 9 esempi su 59 lista_esempio e memorizzarli in un nuovo elenco chiamato validation_data_list. Questi esempi vengono quindi rimossi da lista_esempioche servirà come set di dati di addestramento.

# create train/validation split
validation_index_list = random.sample(range(0, len(example_list)-1), 9)

validation_data_list = (example_list(index) for index in validation_index_list)

for example in validation_data_list:
example_list.remove(example)

Infine, con i nostri set di dati di formazione e convalida preparati, possiamo scriverli su file .jsonl. Questo può essere fatto nel modo seguente.

# write examples to file
with open('data/training-data.jsonl', 'w') as training_file:
for example in example_list:
json.dump(example, training_file)
training_file.write('\n')

with open('data/validation-data.jsonl', 'w') as validation_file:
for example in validation_data_list:
json.dump(example, validation_file)
validation_file.write('\n')

Lavoro di messa a punto

Una volta completata la preparazione dei dati, possiamo eseguire il lavoro di messa a punto in 2 passaggi. Primocarichiamo i file di formazione e convalida sul nostro account OpenAI. Secondoeseguiamo il processo di formazione (3).

Carichiamo i file come abbiamo fatto quando abbiamo configurato il recupero dei documenti per un assistente, ma ora impostiamo lo scopo del file come “sintonizzare”. Questa operazione viene eseguita sia per i set di dati di training che per quelli di convalida riportati di seguito.

# upload fine-tuning files
training_file = client.files.create(
file = open("data/training-data.jsonl", "rb"),
purpose = "fine-tune"
)

validation_file = client.files.create(
file = open("data/validation-data.jsonl", "rb"),
purpose = "fine-tune"
)

Ora possiamo eseguire il lavoro di messa a punto. Per questo, dobbiamo specificare i file di formazione e il modello che desideriamo mettere a punto. Il modello più avanzato disponibile per la messa a punto è gpt-3.5-turboche uso qui. Ho anche impostato un suffisso che aggiunge una stringa definita dall’utente all’ID del modello ottimizzato. Il codice per questo è mostrato di seguito.

client.fine_tuning.jobs.create(
training_file = training_file.id,
validation_file = validation_file.id,
suffix = "ShawGPT",
model = "gpt-3.5-turbo"
)

Il processo di formazione durerà circa 15 minuti. Una volta completato, possiamo utilizzare il modello ottimizzato tramite l’API di completamento, come mostrato di seguito.

test_comment = "Great content, thank you!"

response = client.chat.completions.create(
model="ft:gpt-3.5-turbo-0613:personal:shawgpt:8mUeVreo",
messages=(
{"role": "system", "content": intstructions_string_few_shot},
{"role": "user", "content": test_comment}
)
)

Nota che il modo in cui abbiamo generato la risposta nel blocco di codice sopra è diverso da come lo abbiamo fatto in precedenza. Questo perché i modelli perfezionati lo sono non attualmente supportato nell’API Assistants.

Lo svantaggio più grande è che se vogliamo aumentare il modello ottimizzato con strumenti (ad esempio renderlo un assistente), dobbiamo sviluppare tali funzionalità tramite librerie come LangChain o LlamaIndex (ad esempio).

Sebbene ciò renda più impegnativo lo sviluppo di un assistente ottimizzato, le risposte che vediamo sono pronte all’uso sono più vicini a qualcosa che direi. Ecco alcune risposte al commento del test.

Thanks, I appreciate it! -ShawGPT
Thanks, glad you liked it! -ShawGPT
Glad it was helpful! -ShawGPT

Vediamo come gestisce la stessa questione tecnica di prima, ovvero “Cos’è la coda grassa?

Good question! The fat tail phenomenon represents the size of outlier (extreme) 
events relative to a normal (Gaussian) distribution. In other words, there's a
greater probability of extreme events occurring compared to a normal
distribution. -ShawGPT

Sebbene il modello definisca le code spesse in termini diversi da quelli che farei io, la lunghezza e lo stile della risposta sono molto migliori di quelli che abbiamo visto con l’API Assistants pre-RAG. Ciò suggerisce che se aggiungessimo RAG a questo modello messo a punto, genererebbe risposte significativamente migliori rispetto a quanto visto prima.

Costruire un assistente AI personalizzato è più semplice che mai. Qui abbiamo visto un modo semplice per creare un assistente AI tramite l’API dell’Assistente di OpenAI e come ottimizzare un modello tramite l’API di ottimizzazione.

Sebbene OpenAI disponga attualmente dei modelli più avanzati per lo sviluppo del tipo di assistente AI discusso qui, questi modelli sono bloccati dietro la loro API, il che limita cosa/come possiamo costruire con essi.

Una domanda naturale, quindi, è come potremmo sviluppare sistemi simili utilizzando soluzioni open source. Questo sarà trattato nei prossimi articoli di questa serie, in cui discuterò di come mettere a punto un modello utilizzando QLoRA e potenziare un chatbot tramite RAG.

Altro sui LLM 👇

Shaw talebi

Modelli linguistici di grandi dimensioni (LLM)

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *