Modellizzazione degli argomenti nella produzione

Modellazione degli argomenti nella produzione

Sfruttare LangChain per passare da notebook Jupyter ad-hoc a un servizio modulare di produzione

Immagine di DALL-E 3

Nell’articolo precedente, abbiamo discusso di come fare Topic Modelling utilizzando ChatGPT e abbiamo ottenuto risultati eccellenti. Il compito era quello di analizzare le recensioni dei clienti per le catene alberghiere e definire i principali argomenti menzionati nelle recensioni.

Nella versione precedente, abbiamo utilizzato la standard API di completamento di ChatGPT e abbiamo inviato direttamente noi le richieste. Questo approccio funziona bene quando si fa una ricerca analitica ad-hoc.

Tuttavia, se il tuo team sta usando attivamente e monitorando le recensioni dei clienti, vale la pena considerare l’automazione. Un’automazione efficace ti aiuterà non solo a costruire una pipeline autonoma, ma sarà anche più comoda (anche i membri del team che non sono familiari con i LLM e la codifica potranno accedere a questi dati) e più conveniente (invierai tutti i testi al LLM e pagherai solo una volta).

Supponiamo di voler creare un servizio di produzione sostenibile. In questo caso, vale la pena sfruttare i framework esistenti per ridurre la quantità di codice di collegamento e avere una soluzione più modulare (in modo da poter passare facilmente, ad esempio, da un LLM a un altro).

In questo articolo, vorrei parlarti di uno dei framework più popolari per le applicazioni LLM — LangChain. Inoltre, capiremo nel dettaglio come valutare le prestazioni del tuo modello, poiché è un passaggio cruciale per le applicazioni aziendali.

Sfumature del processo di produzione

Revisione dell’approccio iniziale

Prima di tutto, rivediamo il nostro approccio precedente per il Topic Modelling ad-hoc con ChatGPT.

Passaggio 1: Ottenere un campione rappresentativo.

Vogliamo determinare l’elenco degli argomenti che useremo per il markup. Il modo più semplice è inviare tutte le recensioni e chiedere al LLM di definire l’elenco dei 20-30 argomenti menzionati nelle recensioni. Purtroppo, non saremo in grado di farlo perché non rientrerà nella dimensione del contesto. Potremmo utilizzare un approccio di map-reduce, ma potrebbe essere costoso. Ecco perché vogliamo definire un campione rappresentativo.

A tale scopo, abbiamo costruito un modello di argomento BERTopic e abbiamo ottenuto le recensioni più rappresentative per ciascun argomento.

Passaggio 2: Determinare l’elenco degli argomenti che useremo per il markup.

Il passaggio successivo è inviare tutti i testi selezionati a ChatGPT e chiedere di definire un elenco di argomenti menzionati in queste recensioni. In seguito, possiamo utilizzare questi argomenti per il markup successivo.

Passaggio 3: Effettuare il markup degli argomenti a lotti.

Il passaggio finale è il più semplice: possiamo inviare le recensioni dei clienti a lotti che rientrano nella dimensione del contesto e chiedere al LLM di restituire gli argomenti per ciascuna recensione dei clienti.

Infine, con questi tre passaggi, potremmo determinare l’elenco degli argomenti pertinenti per i nostri testi e classificarli tutti.

Funziona perfettamente per una ricerca occasionale. Tuttavia, mancano alcuni dettagli per una soluzione di produzione eccellente.

Da ad-hoc a produzione

Discutiamo quali miglioramenti possiamo apportare al nostro approccio iniziale ad-hoc.

  • Nell’approccio precedente, abbiamo un elenco statico di argomenti. Ma negli esempi di vita reale, potrebbero emergere nuovi argomenti nel tempo, ad esempio se lanci una nuova funzionalità. Quindi, abbiamo bisogno di un ciclo di feedback per aggiornare l’elenco degli argomenti che stiamo utilizzando. Il modo più semplice per farlo è acquisire l’elenco delle recensioni senza argomenti assegnati e eseguire regolarmente il topic modelling su di esse.
  • Se stiamo conducendo una ricerca una tantum, possiamo convalidare manualmente i risultati degli assegnamenti degli argomenti. Ma per il processo in esecuzione in produzione, dobbiamo pensare a una valutazione continua.
  • Se stiamo creando una pipeline per l’analisi delle recensioni dei clienti, dovremmo considerare altri potenziali casi d’uso e memorizzare altre informazioni correlate di cui potremmo aver bisogno. Ad esempio, è utile memorizzare le versioni tradotte delle recensioni dei clienti in modo che i nostri colleghi non debbano utilizzare continuamente Google Translate. Inoltre, gli attributi come il sentimento e altre caratteristiche (ad esempio, i prodotti menzionati nella recensione del cliente) potrebbero essere preziosi per l’analisi e il filtraggio.
  • Attualmente, l’industria dei LLM sta progredendo molto velocemente e tutto cambia costantemente. Vale la pena considerare un approccio modulare in cui possiamo iterare rapidamente e provare nuovi approcci nel tempo senza dover riscrivere completamente l’intero servizio.
Schema del servizio dell'autore

Abbiamo molte idee su cosa fare con il nostro servizio di modellazione di argomenti. Ma concentriamoci sulle parti principali: approccio modulare anziché chiamate API e valutazione. Il framework LangChain ci aiuterà con entrambi gli argomenti, quindi scopriamone di più.

Cenni preliminari su LangChain

LangChain è un framework per la creazione di applicazioni basate su modelli linguistici. Ecco i principali componenti di LangChain:

  • Schema sono le classi più elementari come Documenti, Messaggi di chat e Testi.
  • Modelli. LangChain fornisce accesso a Modelli di LLM, Modelli di chat e Modelli di embedding di testo che è possibile utilizzare facilmente nelle proprie applicazioni e passare da uno all’altro se necessario. Naturalmente, supporta modelli popolari come ChatGPT, Anthropic e Llama.
  • Prompts è una funzionalità per aiutare a lavorare con i prompt, inclusi modelli di prompt, parser di output e selettori di esempi per prompt di few-shot.
  • Chains sono il nucleo di LangChain (come si può intuire dal nome). Le chains ti aiuteranno a creare una sequenza di blocchi che verranno eseguiti. Puoi apprezzare appieno questa funzionalità se stai creando un’applicazione complessa.
  • Indici: caricatori di documenti, suddivisori di testo, archivi e recuperatori di vettori. Questo modulo fornisce strumenti che aiutano i LLM a interagire con i tuoi documenti. Questa funzionalità sarebbe utile se stai creando un caso di utilizzo Q&A. Non utilizzeremo molto questa funzionalità nel nostro esempio di oggi.
  • LangChain fornisce un set completo di metodi per gestire e limitare la memoria. Questa funzionalità è principalmente necessaria per gli scenari di ChatBot.
  • Una delle ultime e più potenti funzionalità è quella degli agenti. Se sei un utente intensivo di ChatGPT, avrai sicuramente sentito parlare dei plugin. È la stessa idea che puoi potenziare LLM con una serie di strumenti personalizzati o predefiniti (come Ricerca Google o Wikipedia), e poi l’agente può usarli mentre risponde alle tue domande. In questa configurazione, LLM agisce come un agente di ragionamento e decide cosa deve fare per ottenere il risultato e quando ottiene la risposta finale che può condividere. È una funzionalità entusiasmante, quindi merita sicuramente una discussione separata.

Quindi, LangChain può aiutarci a costruire applicazioni modulari e essere in grado di passare da un componente all’altro (ad esempio, da ChatGPT ad Anthropic o da un input di dati CSV a un database Snowflake). LangChain ha più di 190 integrazioni, quindi può risparmiarti parecchio tempo.

Inoltre, potremmo riutilizzare chains predefinite per alcuni casi d’uso anziché partire da zero.

Quando chiami manualmente l’API di ChatGPT, devi gestire parecchio codice di collegamento Python per farlo funzionare. Questo non è un problema quando si lavora su compiti piccoli e semplici, ma può diventare ingestibile quando si deve costruire qualcosa di più complesso e complicato. In questi casi, LangChain può aiutarti a eliminare questo codice di collegamento e creare un codice modulare più facile da mantenere.

Tuttavia, LangChain ha i suoi limiti:

  • È principalmente focalizzato su modelli OpenAI, quindi potrebbe non funzionare così fluidamente con modelli open-source on-premise.
  • Il rovescio della medaglia della comodità è che non è facile capire cosa sta succedendo sotto il cofano e quando e come viene eseguita l’API di ChatGPT per la quale stai pagando. Puoi utilizzare la modalità di debug, ma devi specificarla e leggere i log completi per una visione più chiara.
  • Nonostante una documentazione piuttosto buona, a volte fatico a trovare risposte alle mie domande. Non ci sono così tanti tutorial e risorse su Internet oltre alla documentazione ufficiale, spesso si trovano solo pagine ufficiali su Google.
  • La libreria Langchain sta facendo molti progressi e il team continua a introdurre nuove funzionalità. Quindi, la libreria non è ancora matura e potresti dover passare dalla funzionalità che stai usando. Ad esempio, la classe SequentialChain è considerata obsoleta e potrebbe essere deprecata in futuro visto che hanno introdotto LCEL – ne parleremo in dettaglio in seguito.

Abbiamo ottenuto una panoramica a volo d’uccello della funzionalità di LangChain, ma la pratica rende perfetti. Iniziamo ad utilizzare LangChain.

Migliorare l’assegnazione degli argomenti

Rifattorizziamo l’assegnazione degli argomenti poiché sarà l’operazione più comune nel nostro processo regolare e ci aiuterà a capire come utilizzare LangChain nella pratica.

Prima di tutto, dobbiamo installare il pacchetto.

!pip install --upgrade langchain

Caricamento documenti

Per lavorare con le recensioni dei clienti, dobbiamo prima caricarle. A tal scopo, potremmo utilizzare Caricatori di documenti. Nel nostro caso, le recensioni dei clienti sono memorizzate come un insieme di file .txt in una directory, ma è possibile caricare facilmente documenti da strumenti di terze parti. Ad esempio, c’è un’integrazione con Snowflake.

Useremo DirectoryLoader per caricare tutti i file nella directory poiché abbiamo file separati per gli hotel. Per ogni file, specificheremo TextLoader come caricatore (per default, viene utilizzato un caricatore per documenti non strutturati). I nostri file sono codificati in ISO-8859–1, quindi la chiamata di default restituisce un errore. Tuttavia, LangChain può rilevare automaticamente la codifica utilizzata. Con una tale configurazione, funziona bene.

from langchain.document_loaders import TextLoader, DirectoryLoadertext_loader_kwargs={'autodetect_encoding': True}loader = DirectoryLoader('./hotels/london', show_progress=True,     loader_cls=TextLoader, loader_kwargs=text_loader_kwargs)docs = loader.load()len(docs)82

Divisione dei documenti

Ora, vorremmo dividere i nostri documenti. Sappiamo che ogni file è composto da un insieme di commenti dei clienti delimitati da \n. Dal momento che il nostro caso è molto semplice, utilizzeremo il più elementare CharacterTextSplitter che divide i documenti per carattere. Quando si lavora con documenti reali (interi testi lunghi anziché brevi commenti indipendenti), è meglio utilizzare Divisione ricorsiva per carattere in quanto consente di suddividere i documenti in chunk in modo più intelligente.

Tuttavia, LangChain è più adatto per la divisione sfumata dei testi. Quindi, ho dovuto fare una piccola modifica per farlo funzionare nel modo desiderato.

Come funziona:

  • Specificate chunk_size e chunk_overlap, e cerca di creare il minor numero possibile di divisioni in modo che ogni chunk sia più piccolo di chunk_size. Se non riesce a creare un chunk sufficientemente piccolo, viene visualizzato un messaggio nell’output del notebook Jupyter.
  • Se specificate un chunk_size troppo grande, non tutti i commenti verranno separati.
  • Se specificate un chunk_size troppo piccolo, avrete istruzioni di stampa per ogni commento nel vostro output, con conseguente ricaricamento del notebook. Purtroppo, non sono riuscito a trovare parametri per disattivarlo.

Per risolvere questo problema, ho specificato length_function come una costante uguale a chunk_size. Poi otteniamo una divisione standard per carattere. LangChain fornisce sufficiente flessibilità per fare ciò che si desidera, ma solo in modo un po’ improvvisato.

from langchain.text_splitter import CharacterTextSplittertext_splitter = CharacterTextSplitter(    separator = "\n",    chunk_size = 1,    chunk_overlap  = 0,    length_function = lambda x: 1, # di solito si utilizza len     is_separator_regex = False)split_docs = text_splitter.split_documents(docs)len(split_docs) 12890

Inoltre, aggiungiamo l’ID del documento ai metadati, lo useremo in seguito.

for i in range(len(split_docs)):    split_docs[i].metadata['id'] = i

Il vantaggio dell’utilizzo dei documenti è che ora abbiamo origini dati automatiche e possiamo filtrare i dati in base ad esse. Ad esempio, possiamo filtrare solo i commenti relativi all’Hotel Travelodge.

list(filter(    lambda x: 'travelodge' in x.metadata['source'],    split_docs))

Successivamente, abbiamo bisogno di un modello. Come abbiamo discusso in precedenza in LangChain, ci sono LLM e modelli di chat. La differenza principale è che gli LLM prendono testi e restituiscono testi, mentre i modelli di chat sono più adatti per casi d’uso conversazionali e possono ricevere un insieme di messaggi in input. Nel nostro caso, utilizzeremo il ChatModel per OpenAI poiché desideriamo trasmettere anche messaggi di sistema.

from langchain.chat_models import ChatOpenAIchat = ChatOpenAI(temperature=0.0, model="gpt-3.5-turbo",   openai_api_key = "your_key")

Prompts

Passiamo alla parte più importante – il nostro prompt. In LangChain, c’è un concetto di Template dei Prompt. Aiutano a riutilizzare prompt parametrizzati da variabili. È utile poiché, nelle applicazioni reali, i prompt potrebbero essere molto dettagliati e sofisticati. Quindi, i template di prompt possono essere un’utile astrazione di alto livello che ti aiuterà a gestire efficacemente il tuo codice.

Siccome useremo il Modello di Chat, avremo bisogno di ChatPromptTemplate.

Ma prima di passare ai prompt, discutiamo brevemente una funzionalità utile: un parser di output. Sorprendentemente, possono aiutarci a creare un prompt efficace. Possiamo definire l’output desiderato, generare un parser di output e poi utilizzare il parser per creare istruzioni per il prompt.

Definiamo cosa vorremmo vedere nell’output. Inizialmente, vorremmo essere in grado di passare una lista di recensioni dei clienti al prompt per elaborarle a lotti, quindi nell’output vorremmo ottenere una lista con i seguenti parametri:

  • id per identificare i documenti,
  • lista di argomenti dall’elenco predefinito (utilizzeremo l’elenco della nostra iterazione precedente),
  • sentimento (negativo, neutro o positivo).

Specifichiamo il nostro parser di output. Poiché abbiamo bisogno di una struttura JSON piuttosto complessa, utilizzeremo Pydantic Output Parser invece del più comunemente usato Structured Output Parser.

Per fare ciò, dobbiamo creare una classe ereditata da BaseModel e specificare tutti i campi di cui abbiamo bisogno con nomi e descrizioni (in modo che LLM possa capire cosa aspettarsi nella risposta).

from langchain.output_parsers import PydanticOutputParserfrom langchain.pydantic_v1 import BaseModel, Fieldfrom typing import Listclass CustomerCommentData(BaseModel):    doc_id: int = Field(description="doc_id dai parametri di input")    topics: List[str] = Field(description="Lista degli argomenti rilevanti \        per la recensione del cliente. Si prega di includere solo argomenti \        dall'elenco fornito.")    sentiment: str = Field(description="sentimento del commento (positivo, neutro o negativo")output_parser = PydanticOutputParser(pydantic_object=CustomerCommentData)

Ora, possiamo utilizzare questo parser per generare istruzioni di formattazione per il nostro prompt. È un caso fantastico in cui puoi utilizzare le migliori pratiche di prompting e spendere meno tempo nell’ingegneria dei prompt.

format_instructions = output_parser.get_format_instructions()print(format_instructions)

Quindi, è ora di passare al nostro prompt. Abbiamo preso un lotto di commenti e li abbiamo formattati nel formato previsto. Poi, abbiamo creato un messaggio di prompt con un insieme di variabili: topics_descr_list, format_instructions e input_data. Dopo di ciò, abbiamo creato messaggi di prompt di chat composti da un messaggio di sistema costante e un messaggio di prompt. L’ultimo passaggio è formattare i messaggi di prompt di chat con i valori effettivi.

from langchain.prompts import ChatPromptTemplatedocs_batch_data = []for rec in docs_batch:    docs_batch_data.append(        {            'id': rec.metadata['id'],            'review': rec.page_content        }    )topic_assignment_msg = '''Di seguito è riportato un elenco di recensioni dei clienti in formato JSON con le seguenti chiavi:1. doc_id - identificatore per la recensione2. review - testo della recensione del clienteSi prega di analizzare le recensioni fornite e identificare gli argomenti principali e il sentimento. Includi solo argomenti dall'elenco fornito di seguito.Elenco degli argomenti con descrizioni (delimitate da ":"):{topics_descr_list}Formato di output:{format_instructions}Recensioni dei clienti:```{input_data}```'''topic_assignment_template = ChatPromptTemplate.from_messages([    ("system", "Sei un assistente d'aiuto. Il tuo compito è analizzare le recensioni degli hotel."),    ("human", topic_assignment_msg)])topics_list = '\n'.join(    map(lambda x: '%s: %s' % (x['topic_name'], x['topic_description']),       topics))messages = topic_assignment_template.format_messages(    topics_descr_list = topics_list,    format_instructions = format_instructions,    input_data = json.dumps(docs_batch_data))

Ora possiamo passare questi messaggi formattati a LLM e vedere una risposta.

response = chat(messaggi)type(response.content)strprint(response.content)

Abbiamo ottenuto la risposta come un oggetto di tipo stringa, ma potremmo sfruttare il nostro parser e ottenere la lista degli oggetti di classe CustomerCommentData come risultato.

response_dict = list(map(lambda x: output_parser.parse(x),   response.content.split('\n')))response_dict

Abbiamo quindi sfruttato LangChain e alcune delle sue funzionalità e abbiamo già costruito una soluzione un po’ più intelligente che potrebbe assegnare argomenti ai commenti in batch (risparmiandoci qualche costo) e ha iniziato a definire non solo gli argomenti ma anche il sentimento.

Aggiunta di altra logica

Fino ad ora, abbiamo costruito solo chiamate LLM singole senza alcuna relazione o sequenza. Tuttavia, nella vita reale, spesso vogliamo suddividere i nostri compiti in più passaggi. Per questo, possiamo usare le Chains. La Chain è il blocco fondamentale per LangChain.

LLMChain

Il tipo di chain più basilare è un LLMChain. È una combinazione di LLM e prompt.

Quindi possiamo riscrivere la nostra logica in una chain. Questo codice ci darà assolutamente lo stesso risultato di prima, ma è abbastanza comodo avere un metodo che lo definisce tutto.

from langchain.chains import LLMChaintopic_assignment_chain = LLMChain(llm=chat, prompt=topic_assignment_template)response = topic_assignment_chain.run(    topics_descr_list = topics_list,    format_instructions = format_instructions,    input_data = json.dumps(docs_batch_data))

Chains sequenziali

L’LLM chain è molto basilare. Il potere delle chains sta nella costruzione di logiche più complesse. Prova a creare qualcosa di più avanzato.

L’idea delle chains sequenziali è utilizzare l’output di una chain come input per un’altra.

Per definire le chains, utilizzeremo LCEL (LangChain Expression Language). Questo nuovo linguaggio è stato introdotto solo un paio di mesi fa e ora tutti gli approcci precedenti con SimpleSequentialChain o SequentialChain sono considerati obsoleti. Quindi, vale la pena dedicare del tempo a capire il concetto di LCEL.

Riscriviamo la chain precedente in LCEL.

chain = topic_assignment_template | chatresponse = chain.invoke(    {        'topics_descr_list': topics_list,        'format_instructions': format_instructions,        'input_data': json.dumps(docs_batch_data)    })

Se vuoi saperne di più, ti consiglio di guardare questo video sul LCEL del team di LangChain.

Utilizzo delle chains sequenziali

In alcuni casi, potrebbe essere utile avere diverse chiamate sequenziali in modo che l’output di una chain venga utilizzato nelle altre.

Nel nostro caso, possiamo prima tradurre le recensioni in inglese e quindi eseguire la modellazione degli argomenti e l’analisi del sentimento.

from langchain.schema import StrOutputParserfrom operator import itemgetter# traduzionetranslate_msg = '''Di seguito è riportato un elenco di recensioni dei clienti in formato JSON con le seguenti chiavi:1. doc_id - identificatore per la recensione2. review - testo della recensione del clientePer favore, traduci la recensione in inglese e restituisci lo stesso JSON. Restituisci SOLO JSON valido nella risposta senza altre informazioni.Recensioni dei clienti:```{input_data}```'''translate_template = ChatPromptTemplate.from_messages([    ("system", "Sei un'API, quindi restituisci solo JSON valido senza commenti."),    ("human", translate_msg)])# assegnazione degli argomenti e analisi del sentimentotopic_assignment_msg = '''Di seguito è riportato un elenco di recensioni dei clienti in formato JSON con le seguenti chiavi:1. doc_id - identificatore per la recensione2. review - testo della recensione del clientePer favore, analizza le recensioni fornite e identifica gli argomenti principali e il sentiment. Includi solo gli argomenti dall'elenco fornito di seguito.Elenco degli argomenti con descrizioni (delimitate da ":"):{topics_descr_list}Formato di output:{format_instructions}Recensioni dei clienti:```{translated_data}```'''topic_assignment_template = ChatPromptTemplate.from_messages([    ("system", "Sei un assistente utile. Il tuo compito è analizzare le recensioni degli hotel."),    ("human", topic_assignment_msg)])# definizione delle chainstranslate_chain = translate_template | chat | StrOutputParser()topic_assignment_chain = {'topics_descr_list': itemgetter('topics_descr_list'),                           'translated_data': translate_chain,                           'format_instructions': itemgetter('format_instructions')}                         | topic_assignment_template | chat # esecuzioneresponse = topic_assignment_chain.invoke(    {        'topics_descr_list': topics_list,        'format_instructions': format_instructions,        'input_data': json.dumps(docs_batch_data)    })

Abbiamo definito in modo simile modelli di prompt per la traduzione e l’assegnazione degli argomenti. Quindi, abbiamo determinato la catena di traduzione. L’unico nuovo elemento qui è l’utilizzo di StrOutputParser(), che converte gli oggetti di risposta in stringhe (niente di complicato).

Poi, abbiamo definito l’intera catena, specificando i parametri di input, il modello di prompt e l’LLM. Per i parametri di input, abbiamo preso translated_data dall’output di translate_chain, mentre gli altri parametri dall’input dell’invocazione utilizzando la funzione itemgetter.

Tuttavia, nel nostro caso, un approccio del genere con una catena combinata potrebbe non essere così conveniente, poiché vorremmo anche salvare l’output della prima catena per avere valori tradotti.

Con le catene, tutto diventa un po’ più complicato, quindi potremmo avere bisogno di alcune capacità di debug. Ci sono due opzioni per il debug. La prima è quella di attivare il debug localmente.

import langchain
langchain.debug = True

L’altra opzione è utilizzare la piattaforma LangChain – LangSmith. Tuttavia, è ancora in modalità beta-tester, quindi potrebbe essere necessario attendere per ottenere l’accesso.

Routing

Uno dei casi più complessi delle catene è il routing quando si utilizzano prompt diversi per casi d’uso diversi. Ad esempio, potremmo salvare diversi parametri di recensione dei clienti a seconda del sentimento:

  • Se il commento è negativo, memorizzeremo l’elenco dei problemi segnalati dal cliente.
  • Altrimenti, otterremo l’elenco dei punti positivi dalla recensione.

Per utilizzare una catena di routing, dovremo passare i commenti uno per uno anziché raggrupparli come abbiamo fatto prima.

Quindi la nostra catena a un alto livello avrà questo aspetto.

Prima, dobbiamo definire la catena principale che determina il sentimento. Questa catena è composta da prompt, LLM e già familiareStrOutputParser().

sentiment_msg = '''Dato il commento del cliente di seguito, classifica se è negativo. Se è negativo, restituisci "negativo", altrimenti restituisci "positivo". Non rispondere con più di una parola.Commento del cliente:```{input_data}```'''sentiment_template = ChatPromptTemplate.from_messages([    ("system", "Sei un assistente. Il tuo compito è stilare il sentimento per le recensioni degli hotel."),    ("human", sentiment_msg)])sentiment_chain = sentiment_template | chat | StrOutputParser()

Per le recensioni positive, chiederemo al modello di estrarre i punti positivi, mentre per quelle negative – i problemi. Quindi, avremo bisogno di due catene diverse. Utilizzeremo gli stessi output parser Pydantic come prima per specificare il formato di output desiderato e generare istruzioni.

Abbiamo utilizzato partial_variables in cima al messaggio generale di assegnazione degli argomenti per specificare diverse istruzioni di formattazione per i casi positivi e negativi.

from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate# definizione della struttura per i casi positivi e negativiclass PositiveCustomerCommentData(BaseModel):    topics: List[str] = Field(description="Elenco degli argomenti rilevanti per la recensione del cliente. Includi solo argomenti dall'elenco fornito.")        advantages: List[str] = Field(description = "Elenco dei punti positivi menzionati dal cliente")    sentiment: str = Field(description="sentimento del commento (positivo, neutrale o negativo)")class NegativeCustomerCommentData(BaseModel):    topics: List[str] = Field(description="Elenco degli argomenti rilevanti per la recensione del cliente. Includi solo argomenti dall'elenco fornito.")        problems: List[str] = Field(description = "Elenco dei problemi menzionati dal cliente")    sentiment: str = Field(description="sentimento del commento (positivo, neutrale o negativo)")# definizione degli output parser e generazione delle istruzionipositive_output_parser = PydanticOutputParser(pydantic_object=PositiveCustomerCommentData)positive_format_instructions = positive_output_parser.get_format_instructions()negative_output_parser = PydanticOutputParser(pydantic_object=NegativeCustomerCommentData)negative_format_instructions = negative_output_parser.get_format_instructions()general_topic_assignment_msg = '''Di seguito è riportata una recensione del cliente delimitata da ```.Per favore, analizza la recensione fornita e identifica gli argomenti principali e il sentimento. Includi solo argomenti dall'elenco fornito.Di seguito l'elenco degli argomenti con le relative descrizioni (delimitate da ":"):{topics_descr_list}Formato di output:{format_instructions}Recensioni dei clienti:```{input_data}```'''# definizione dei modelli di promptpositive_topic_assignment_template = ChatPromptTemplate(     messages=[         SystemMessagePromptTemplate.from_template("Sei un assistente disponibile. Il tuo compito è analizzare le recensioni degli hotel."),        HumanMessagePromptTemplate.from_template(general_topic_assignment_msg)     ],     input_variables=["topics_descr_list", "input_data"],     partial_variables={"format_instructions": positive_format_instructions} )negative_topic_assignment_template = ChatPromptTemplate(     messages=[         SystemMessagePromptTemplate.from_template("Sei un assistente disponibile. Il tuo compito è analizzare le recensioni degli hotel."),        HumanMessagePromptTemplate.from_template(general_topic_assignment_msg)     ],     input_variables=["topics_descr_list", "input_data"],     partial_variables={"format_instructions": negative_format_instructions} )

Quindi, ora abbiamo solo bisogno di costruire la catena completa. La logica principale è definita utilizzando RunnableBranch e una condizione basata sul sentiment, un output di sentiment_chain.

from langchain.schema.runnable import RunnableBranchbranch = RunnableBranch(  (lambda x: "negative" in x["sentiment"].lower(), negative_chain),  positive_chain)full_route_chain = {    "sentiment": sentiment_chain,    "input_data": lambda x: x["input_data"],    "topics_descr_list": lambda x: x["topics_descr_list"]} | branchfull_route_chain.invoke({'input_data': review,   'topics_descr_list': topics_list})

Ecco un paio di esempi. Funziona molto bene e restituisce oggetti diversi a seconda del sentiment.

Abbiamo analizzato in dettaglio l’approccio modulare per eseguire la modellazione degli argomenti utilizzando LangChain e presentato logiche più complesse. Ora è il momento di passare alla seconda parte e discutere come potremmo valutare le prestazioni del modello.

Valutazione

La parte cruciale di ogni sistema in produzione è l’valutazione. Quando abbiamo un modello LLM in produzione, vogliamo garantire la qualità e tenere d’occhio nel tempo.

In molti casi, è possibile utilizzare non solo la presenza umana (quando le persone controllano i risultati del modello per un piccolo campione nel tempo per controllare le prestazioni), ma anche il LLM per questa attività. Potrebbe essere una buona idea utilizzare un modello più complesso per i controlli in tempo reale. Ad esempio, abbiamo utilizzato ChatGPT 3.5 per l’assegnazione degli argomenti, ma potremmo utilizzare GPT 4 per la valutazione (simile al concetto di supervisione nella vita reale quando si chiede a colleghi più esperti di effettuare una revisione del codice).

Langchain può aiutarci anche in questa attività, poiché fornisce alcuni strumenti per valutare i risultati:

  • String Evaluators aiutano a valutare i risultati del modello. Ci sono molti strumenti disponibili, dalla validazione del formato alla valutazione della correttezza basata sul contesto o sul riferimento fornito. Ne parleremo in dettaglio di seguito.
  • L’altro tipo di valutatori sono i Comparison evaluators. Sono utili se si desidera valutare le prestazioni di 2 diversi modelli LLM (come l’A/B testing). Non entreremo nei dettagli oggi.

Corrispondenza esatta

L’approccio più diretto è confrontare l’output del modello con la risposta corretta (ad esempio, da esperti o da un set di allenamento) utilizzando una corrispondenza esatta. Per questo, potremmo utilizzare ExactMatchStringEvaluator, ad esempio, per valutare le prestazioni della nostra analisi del sentiment. In questo caso, non abbiamo bisogno di LLM.

from langchain.evaluation import ExactMatchStringEvaluatorevaluator = ExactMatchStringEvaluator(    ignore_case=True,    ignore_numbers=True,    ignore_punctuation=True,)evaluator.evaluate_strings(    prediction="positive.",    reference="Positive")# {'score': 1}evaluator.evaluate_strings(    prediction="negative",    reference="Positive")# {'score': 0}

Puoi creare il tuo String Evaluator personalizzato o confrontare l’output con un’espressione regolare.

Inoltre, ci sono strumenti utili per convalidare l’output strutturato, verificare se l’output è un JSON valido, ha la struttura prevista ed è vicino al riferimento in base alla distanza. Puoi trovare maggiori dettagli a riguardo nella documentazione.

Valutazione della distanza degli embeddings

Un altro approccio utile è osservare la distanza tra gli embeddings. Otterrai uno score nel risultato: minore è lo score, meglio è, poiché le risposte sono più vicine tra loro. Ad esempio, possiamo confrontare i punti buoni trovati mediante la distanza euclidea.

from langchain.evaluation import load_evaluator
from langchain.evaluation import EmbeddingDistance

evaluator = load_evaluator("embedding_distance", distance_metric=EmbeddingDistance.EUCLIDEAN)
evaluator.evaluate_strings(
    prediction="stanze ben progettate, pulite, ottima posizione",
    reference="stanze ben progettate, pulite, ottima posizione, buona atmosfera"
)['score']: 0.20732719121627757

Otteniamo una distanza di 0,2. Tuttavia, i risultati di questa valutazione potrebbero essere più difficili da interpretare poiché sarà necessario prendere in considerazione le distribuzioni dei dati e definire delle soglie. Passiamo ad approcci basati su LLM, poiché saremo in grado di interpretare i loro risultati senza sforzo.

Valutazione dei criteri

Puoi utilizzare LangChain per validare la risposta di LLM rispetto a qualche criterio o rubrica. È possibile utilizzare una lista di criteri predefiniti o crearne uno personalizzato.

from langchain.evaluation import Criterialist

evaluator = load_evaluator("criteria", criteria=Criterialist([, , , , , , , , , , , , , ])
eval_result = evaluator.evaluate_strings(
    prediction="stanze ben progettate, pulite, ottima posizione",
    input="Elenca i punti positivi menzionati dal cliente",
)

Come risultato, otteniamo la risposta (se i risultati si adattano al criterio specificato) e un ragionamento logico che ci permette di comprendere la logica dietro al risultato e potenzialmente modificare la richiesta.

Se sei interessato a come funziona, puoi attivare langchain.debug = True e vedere la richiesta inviata a LLM.

Passiamo al criterio di correttezza. Per valutarlo, è necessario fornire un riferimento (la risposta corretta).

evaluator = load_evaluator("labeled_criteria", criteria="correctness")
eval_result = evaluator.evaluate_strings(
    prediction="stanze ben progettate, pulite, ottima posizione",
    input="Elenca i punti positivi menzionati dal cliente",
    reference="stanze ben progettate, pulite, ottima posizione, buona atmosfera",
)

È anche possibile creare propri criteri personalizzati, ad esempio, verificando se la risposta contiene più punti.

custom_criterion = {"multiple": "La risposta contiene più punti?"}
evaluator = load_evaluator("criteria", criteria=custom_criterion)
eval_result = evaluator.evaluate_strings(
    prediction="stanze ben progettate, pulite, ottima posizione",
    input="Elenca i punti positivi menzionati dal cliente",
)

Valutazione del punteggio

Con la valutazione dei criteri, otteniamo solo una risposta Sì o No, ma in molti casi non è sufficiente. Ad esempio, nel nostro esempio, la previsione ha 3 su 4 punti menzionati, il che è un buon risultato, ma abbiamo ottenuto un N nella valutazione della correttezza. Quindi, utilizzando questo approccio, le risposte “stanze ben progettate, pulite, ottima posizione” e “internet veloce” saranno equivalenti in termini di nostre metriche, il che non ci fornirà informazioni sufficienti per capire le prestazioni del modello.

C’è un’altra tecnica abbastanza simile di valutazione del punteggio quando si chiede a LLM di fornire il punteggio in output, che potrebbe aiutare a ottenere risultati più dettagliati. Proviamola.

from langchain.chat_models import ChatOpenAIaccuracy_criteria = {    "accuracy": """Punteggio 1: La risposta non menziona punti rilevanti.Punteggio 3: La risposta menziona solo alcuni punti rilevanti ma presenta inesattezze importanti o includi diverse opzioni non rilevanti.Punteggio 5: La risposta ha una quantità moderata di opzioni rilevanti ma potrebbe contenere inesattezze o punti sbagliati.Punteggio 7: La risposta si allinea con il riferimento e mostra la maggior parte dei punti rilevanti e non contiene opzioni completamente sbagliate.Punteggio 10: La risposta è completamente accurata e si allinea perfettamente con il riferimento."""}evaluator = load_evaluator(    "labeled_score_string",     criteria=accuracy_criteria,     llm=ChatOpenAI(model="gpt-4"),)eval_result = evaluator.evaluate_strings(    prediction="stanze ben progettate, pulite, ottima posizione",    input="""Di seguito è riportata una recensione del cliente delimitata da ```. Fornisci l'elenco dei punti positivi menzionati nella recensione del cliente. Recensione del cliente: ```Stanze piccole ma ben progettate, pulite, ottima posizione, buona atmosfera. Ci tornerei. La colazione continentale è debole ma ok.```""",    reference="stanze ben progettate, pulite, ottima posizione, buona atmosfera")

Abbiamo ottenuto sette come punteggio, che sembra abbastanza valido. Diamo un’occhiata al prompt effettivo utilizzato.

Tuttavia, trattare i punteggi da LLM con cautela. Ricorda, non è una funzione di regressione e i punteggi potrebbero essere molto soggettivi.

Abbiamo utilizzato il modello di valutazione con il riferimento. Ma in molti casi potremmo non avere le risposte corrette o potrebbe essere costoso ottenerle. Puoi utilizzare l’evaluatore del punteggio anche senza punteggi di riferimento chiedendo al modello di valutare la risposta. Vale la pena utilizzare GPT-4 per avere risultati più affidabili.

accuracy_criteria = {    "recall": "La risposta dell'assistente deve includere tutto ciò che viene menzionato nella domanda. Se mancano informazioni, assegna un punteggio inferiore.",    "precision": "La risposta dell'assistente non deve contenere punti non presenti nella domanda."}evaluator = load_evaluator("score_string", criteria=accuracy_criteria,   llm=ChatOpenAI(model="gpt-4"))eval_result = evaluator.evaluate_strings(    prediction="stanze ben progettate, pulite, ottima posizione",    input="""Di seguito è riportata una recensione del cliente delimitata da ```. Fornisci l'elenco dei punti positivi menzionati nella recensione del cliente. Recensione del cliente: ```Stanze piccole ma ben progettate, pulite, ottima posizione, buona atmosfera. Ci tornerei. La colazione continentale è debole ma ok.```""")

Abbiamo ottenuto un punteggio abbastanza simile al precedente.

Abbiamo esaminato diverse possibili modalità per convalidare l’output, quindi spero che tu sia ora pronto per testare i risultati dei tuoi modelli.

Sommario

In questo articolo, abbiamo discusso alcune sfumature che dobbiamo tenere in considerazione se vogliamo utilizzare LLM per i processi produttivi.

  • Abbiamo esaminato l’uso del framework LangChain per rendere la nostra soluzione più modulare in modo da poter iterare facilmente e utilizzare nuovi approcci (ad esempio, passare da un LLM a un altro). Inoltre, i framework di solito aiutano a rendere il nostro codice più facile da mantenere.
  • L’altro grande argomento che abbiamo discusso riguarda gli strumenti diversi che abbiamo per valutare le prestazioni del modello. Se stiamo utilizzando LLM in produzione, è necessario avere un monitoraggio costante in atto per garantire la qualità del nostro servizio, ed è utile dedicare del tempo per creare una pipeline di valutazione basata su LLM o su un approccio umano-in-loop.

Grazie mille per aver letto questo articolo. Spero che sia stato illuminante per te. Se hai domande o commenti, ti prego di lasciarli nella sezione dei commenti.

Dataset

Ganesan, Kavita e Zhai, ChengXiang. (2011). Dataset di recensioni OpinRank. Repository di Apprendimento Automatico dell’UCI. https://doi.org/10.24432/C5QW4W

Riferimento

Questo articolo si basa su informazioni del corso “LangChain per lo sviluppo di applicazioni LLM” di DeepLearning.AI e LangChain.