Comprensione del codice sul proprio hardware

'Comprendere il codice sul proprio hardware'

Impostazione di un LLM per parlare del tuo codice – con LangChain e hardware locale

Prometto che il tuo codice non lascerà il tuo hardware locale. Foto di Clément Hélardot su Unsplash

Tra i vari compiti che i Large Language Models (LLM) possono svolgere oggi, la comprensione del codice può essere di particolare interesse per te, se lavori con il codice sorgente come sviluppatore di software o scienziato dei dati. Non sarebbe fantastico avere un chatbot a cui puoi fare domande sul tuo codice? Dove viene implementata la pre-elaborazione dei dati? Esiste già una funzione per verificare l’autenticazione dell’utente? Qual è la differenza tra la funzione calculate_vector_dim e calculate_vector_dimension? Invece di cercare il file corretto da solo, chiedi semplicemente al bot e ti darà una risposta, insieme a un puntatore ai file che contengono i frammenti di codice rilevanti. Questo meccanismo è chiamato ricerca semantica, e puoi immaginare quanto sia utile.

In questo tutorial, ti mostrerò come implementare un bot LangChain che fa esattamente questo. Inoltre, mi concentrerò sul problema specifico correlato alla privacy dei dati di non divulgare il tuo codice a terzi. Il codice che tu o la tua azienda avete prodotto è proprietà privata e potrebbe contenere informazioni sensibili o conoscenze preziose. Potresti non volerlo fare, o le politiche della tua azienda potrebbero non permetterti di inviarlo a un LLM ospitato da un’altra azienda, che potrebbe trovarsi in un paese straniero. Pertanto, in questo tutorial, ti mostrerò come configurare un bot di comprensione del codice che viene eseguito sul tuo hardware locale, in modo che il tuo codice non lasci mai la tua infrastruttura.

Iniziamo già! Prima ti darò una breve introduzione al processo generale della ricerca semantica prima di implementare un bot per la comprensione del codice.

Introduzione alla ricerca semantica

Nella ricerca semantica, si tratta di trovare i documenti rilevanti. Foto di Markus Spiske su Unsplash

Prima di tutto, lascia che ti spieghi brevemente l’idea generale della ricerca semantica. Questo approccio consiste in due fasi principali, che sono il recupero e la generazione della risposta da parte del LLM stesso. Nella fase di recupero, vengono selezionati i documenti che contengono informazioni rilevanti, e questi vengono alimentati al LLM per creare una risposta in linguaggio naturale. Ad esempio, se fai una domanda su una funzione chiamata transform_vectors, il recupero selezionerà quei file che sono rilevanti per rispondere a quella domanda. Ciò potrebbe includere il file in cui è implementata la funzione transform_vectors, ma anche file che la utilizzano o parti della documentazione che la menzionano. Nella seconda fase, il contenuto di quei file viene fornito al LLM in un prompt che potrebbe assomigliare a questo:

"""Rispondi alla domanda di seguito dato il contesto. <documento 1><documento 2>...<documento n>Domanda: <domanda dell'utente>Risposta:"""

Il LLM crea una risposta in linguaggio naturale alla domanda utilizzando le informazioni dai documenti che gli sono stati forniti.

Questa è l’idea principale della ricerca semantica. Ora iniziamo l’implementazione! Prima di tutto, dobbiamo installare i requisiti e leggere i nostri dati.

Installare i requisiti

Prima di poter iniziare, assicurati di aver configurato un ambiente in esecuzione Python e installa i seguenti pacchetti:

pip install langchain==0.0.191pip install transformers

Leggere i documenti

Ora dobbiamo leggere i dati e convertirli in un formato con cui LangChain può lavorare. Per questa dimostrazione, scaricherò il codice di LangChain stesso, ma puoi usare la tua base di codice, ovviamente:

import osfolder_name = "sample_code"os.system(f"git clone https://github.com/hwchase17/langchain {folder_name}")

Carichiamo tutti i file e li convertiamo in un Document ciascuno, ovvero ogni Document conterrà esattamente un file della base di codice.

from langchain.docstore.document import Document
documents = []
for root, dirs, files in os.walk(folder_name):
    for file in files:
        try:
            with open(os.path.join(root, file), "r", encoding="utf-8") as o:
                code = o.readlines()
                d = Document(page_content="\n".join(code), metadata={"source": os.path.join(root, file)})
                documents.append(d)
        except UnicodeDecodeError:
            # alcuni file non sono codificati in utf-8; li ignoriamo per ora
            pass

Recupero

Quale di questi è rilevante per rispondere alla nostra domanda? È compito del recupero decidere. Foto di Ed Robertson su Unsplash

Ora che abbiamo creato i nostri Documenti, dobbiamo indicizzarli per renderli cercabili. Indicizzare un Documento significa calcolare un vettore numerico che cattura le informazioni più rilevanti del Documento. A differenza del testo semplice, un vettore di numeri può essere utilizzato per eseguire calcoli numerici, e questo significa che possiamo facilmente calcolarne la similarità, che viene poi utilizzata per determinare quali Documenti sono rilevanti per rispondere a una determinata domanda.

Da un punto di vista tecnico, questo indice che creeremo con l’aiuto di un embedding verrà memorizzato in un VectorStore. Ci sono VectorStore disponibili come servizio (ad esempio DeepLake), che presentano alcuni vantaggi interessanti, ma nel nostro scenario non vogliamo consegnare il codice nelle mani di altri, quindi creiamo un VectorStore localmente sulla nostra macchina. Il modo più semplice per farlo è utilizzare Chroma, che crea un VectorStore in memoria e ci consente di persistere i dati.

from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma

hfemb = HuggingFaceEmbeddings(model_name="krlvi/sentence-t5-base-nlpl-code-x-glue")
persist_directory = "db"
db = Chroma.from_documents(documents, hfemb, persist_directory=persist_directory)
db.persist()

All’interno della funzione from_documents, gli indici vengono calcolati e memorizzati nel database Chroma. La prossima volta, invece di chiamare nuovamente la funzione from_documents, possiamo caricare direttamente il database Chroma persistito:

db = Chroma(persist_directory=persist_directory, embedding_function=hfemb)

Come hai visto sopra, come embedding ho utilizzato krlvi/sentence-t5-base-nlpl-code-x-glue, che è un embedding addestrato sul codice delle librerie GitHub open-source. Come puoi immaginare, è fondamentale che l’embedding che utilizziamo sia stato addestrato sul codice (tra gli altri dati), in modo che possa utilizzare i dati che gli forniamo. Un embedding addestrato solo sul linguaggio naturale avrà prestazioni inferiori, molto probabilmente.

Ora che abbiamo il nostro VectorStore e il nostro embedding, possiamo creare il recupero direttamente dal database Chroma:

retriever = db.as_retriever()

LLM

L'LLM deve ragionare sui documenti e trovare una risposta alla domanda dell'utente. Foto di Tingey Injury Law Firm su Unsplash

Il componente finale di cui abbiamo bisogno è un LLM. La soluzione più semplice sarebbe utilizzare un servizio di LLM ospitato, ad esempio utilizzando l’interfaccia di OpenAI. Tuttavia, non vogliamo inviare il nostro codice a un servizio ospitato. Invece, eseguiremo un LLM sul nostro hardware. Per farlo, utilizziamo HuggingFacePipeline, che ci consente di utilizzare un modello di HuggingFace nel framework LangChain.

from langchain import HuggingFacePipeline
import transformers

model_id = "mosaicml/mpt-7b-instruct"
config = transformers.AutoConfig.from_pretrained(model_id, trust_remote_code=True)
tokenizer = transformers.AutoTokenizer.from_pretrained(model_id)
model = transformers.AutoModelForCausalLM.from_pretrained(model_id, config=config, trust_remote_code=True)
pipe = transformers.pipeline("text-generation", model=model, tokenizer=tokenizer, max_new_tokens=100)
llm = HuggingFacePipeline(pipeline=pipe)

Come puoi vedere, ho utilizzato il modello mosaic mpt-7b, che richiede solo ~16GB di memoria su una GPU. Ho creato un AutoModelForCausalLM, che viene passato alla transformers.pipeline, che alla fine viene trasformato in un HuggingFacePipeline. L’HuggingFacePipeline implementa la stessa interfaccia degli oggetti LLM tipici in LangChain. Quindi, puoi utilizzarlo nello stesso modo in cui utilizzeresti l’interfaccia LLM di OpenAI, ad esempio.

Se hai più GPU sul tuo computer, devi specificare quale utilizzare. In questo caso, voglio utilizzare la GPU con indice 0:

config.init_device="cuda:0"model.to(device='cuda:0')pipe = transformers.pipeline("text-generation", model=model, tokenizer=tokenizer, max_new_tokens=100, device=0)

Alcuni parametri aggiuntivi che ho impostato possono essere spiegati come segue:

  • trust_remote_code: Questo deve essere impostato su true per consentire l’esecuzione di un modello proveniente da LangChain esterno.
  • max_new_tokens: Questo definisce il numero massimo di token che il modello può produrre nella sua risposta. Se questo valore è troppo basso, la risposta del modello potrebbe essere tagliata prima che abbia avuto la possibilità di rispondere alla domanda.

Collegare tutto insieme

Abbiamo tutti i componenti di cui abbiamo bisogno. Dobbiamo solo collegarli insieme. Foto di John Barkiple su Unsplash

Ora abbiamo tutti i componenti di cui abbiamo bisogno e possiamo combinarli in una ConversationalRetrievalChain.

from langchain.chains import ConversationalRetrievalChainqa_chain = ConversationalRetrievalChain.from_llm(llm=llm, retriever=retriever, return_source_documents=True)

Infine, possiamo interrogare la catena per ottenere risposte alle nostre domande. L’oggetto risultato includerà una risposta in linguaggio naturale e una lista di source_documents che sono stati consultati per arrivare a quella risposta.

result = qa_chain({"question":"Qual è il tipo di ritorno della funzione create_index nel KNNRetriever?", "chat_history":[]})print(f"Risposta: {result['answer']}")print(f"Fonti: {[x.metadata['source'] for x in result['source_documents']]}")

Ecco la risposta:

Risposta: Il tipo di ritorno della funzione create_index nel KNNRetriever è np.ndarray.Fonti: ['sample_code/langchain/retrievers/knn.py', 'sample_code/langchain/vectorstores/elastic_vector_search.py', 'sample_code/langchain/vectorstores/elastic_vector_search.py', 'sample_code/langchain/vectorstores/opensearch_vector_search.py']

Sommario

Siamo finiti! Beh, quasi. Con il codice sopra siamo ora in grado di fare domande riguardanti il codice sorgente. Tuttavia, ci sono alcuni passaggi che potresti voler modificare in base alle tue esigenze:

  • Utilizza il tuo codice sorgente come documenti invece del codice di LangChain.
  • Prova un diverso embedding. Se l’embedding non è adatto, il recuperatore non può trovare i documenti corretti e alla fine le domande non possono essere risposte in modo preciso.
  • Prova un modello diverso. Ci sono modelli più grandi e più potenti là fuori, ma alcuni potrebbero essere troppo grandi per essere eseguiti sul tuo hardware. Devi trovare il punto ottimale in cui hai prestazioni decenti ma sei comunque in grado di eseguire il modello in modo soddisfacente.
  • Prova modi diversi di preprocessare i documenti per facilitare la fase di recupero. Un esempio comune sarebbe suddividerli in chunk di lunghezza uguale.

Sono sicuro che ci sono molte altre cose da provare per ottenere prestazioni migliori. Gioca e adatta il bot alle tue esigenze.

Ulteriori letture

Per ulteriori esempi di comprensione del codice con LangChain, dai un’occhiata alla loro documentazione qui:

  • https://python.langchain.com/docs/use_cases/code/

Su HuggingFace puoi trovare modelli e embedding che puoi utilizzare facilmente in LangChain:

  • https://huggingface.co/models

Ti è piaciuto questo articolo? Seguimi per essere informato dei miei futuri post.