TL;DR

  • Da POC a prodotto più veloce: Come descrive la documentazione di langchain, "LCEL è un modo dichiarativo per comporre facilmente le catene. LCEL è stato progettato fin dal primo giorno per supportare la messa in produzione dei prototipi, senza alcuna modifica del codice".

  • Creazione di catene personalizzate: LCEL semplifica il processo di creazione di catene personalizzate con una nuova sintassi.

  • Streaming e batch pronti all'uso: LCEL offre gratuitamente funzionalità di batch, streaming e async.

  • Interfaccia unificata: Offre la parallelizzazione automatica, le funzionalità di digitazione e qualsiasi funzione futura che LangChain potrebbe sviluppare.

  • LCEL è il futuro di LangChain: LCEL offre una nuova prospettiva sullo sviluppo di applicazioni basate su LLM. Consiglio vivamente di utilizzarlo per il vostro prossimo progetto LLM.

LangChain è diventata una delle librerie Python più utilizzate per interagire con i LLM in meno di un anno, ma LangChain era soprattutto una libreria per i POC, in quanto mancava della capacità di creare applicazioni complesse e scalabili.
Tutto è cambiato nell'agosto 2023, quando è stato rilasciato LangChain Expression Language (LCEL), una nuova sintassi che colma il divario tra POC e produzione. Questo articolo vi guiderà attraverso i dettagli di LCEL, mostrandovi come semplifica la creazione di catene personalizzate e perché dovete impararlo se state costruendo applicazioni LLM!

Prompts, LLM e catene, rinfreschiamo la memoria

Prima di immergersi nella sintassi di LCEL, credo sia utile rinfrescare la memoria sui concetti di LangChain, come LLM e Prompt o anche Catena.

LLM: In langchain, llm è un'astrazione attorno al modello usato per fare i completamenti come openai gpt3.5, claude, ecc...

Prompt: È l'input dell'oggetto LLM, che porrà le domande all'LLM e fornirà i suoi obiettivi.

Catena: Si riferisce a una sequenza di chiamate a un LLM o a qualsiasi fase di elaborazione dei dati.

class="lazyload

Ora che le definizioni sono state tolte di mezzo, supponiamo di voler creare un'azienda! Abbiamo bisogno di un nome molto bello e accattivante e di un modello di business per fare soldi!

Esempio - Nome dell'azienda e modello di business con le vecchie catene

da langchain.chains importare LLMChain
da langchain.prompts import PromptTemplate
da langchain_community.llms importare OpenAI

USER_INPUT = "calzini colorati"
llm = OpenAI(temperatura=0)

prompt_template_product = "Qual è un buon nome per un'azienda che produce {prodotto}?".
nome_azienda_catena = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
nome_azienda_output = nome_azienda_catena(USER_INPUT)

prompt_template_business = “Give me the best business model idea for my company named: {company}”
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
business_model_output = business_model_chain(company_name_output[“text”])

print(nome_azienda_output)
print(modello_aziendale_output)

>>> {‘product’: ‘colorful socks’, ‘text’: ‘Socktastic!’}
>>> {‘company’: ‘Socktastic!’,’text’: “A subscription-based service offering a monthly delivery…”}

È abbastanza facile da seguire, si nota un po' di ridondanza, ma è gestibile.

Aggiungiamo un po' di personalizzazione gestendo i casi in cui l'utente non utilizza la nostra catena come previsto.
Forse l'utente inserisce qualcosa di completamente estraneo all'obiettivo della nostra catena? In questo caso, vogliamo rilevarlo e rispondere in modo appropriato.

Esempio - Personalizzazione e instradamento con le catene precedenti

da langchain.chains importare LLMChain
da langchain.prompts import PromptTemplate
da langchain_community.llms importare OpenAI
importare ast

USER_INPUT = "Harrison Chase"
llm = OpenAI(temperatura=0)

# -- Stesso codice di prima
prompt_template_product = "Qual è un buon nome per un'azienda che produce {prodotto}?"
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))

prompt_template_business = “Give me the best business model idea for my company named: {company}”
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))

# -- Nuovo codice

prompt_template_is_product = (
“Your goal is to find if the input of the user is a plausible product namen”
“Questions, greetings, long sentences, celebrities or other non relevant inputs are not considered productsn”
“input: {product}n”
“Answer only by ‘True’ or ‘False’ and nothing moren”
)

prompt_template_cannot_respond = (
“You cannot respond to the user input: {product}n”
“Ask the user to input the name of a product in order for you to make a company out of it.n”
)

cannot_respond_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_cannot_respond))
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
is_a_product_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_is_product))

# Se usiamo bool su una stringa non vuota, sarà True, quindi abbiamo bisogno di `literal_eval`.
is_a_product = ast.literal_eval(is_a_product_chain(USER_INPUT)["text"])
se is_a_product:
nome_azienda_output = catena_nome_azienda(USER_INPUT)
business_model_output = business_model_chain(company_name_output["text"])
print(business_model_output)
altrimenti:
print(cannot_respond_chain(USER_INPUT))

Questo diventa un po' più difficile da capire, ma riassumiamo:

  • Abbiamo creato una nuova catena is_real_product_chain() che rileva se l'input dell'utente può essere considerato un prodotto.

  • Implementiamo le condizioni if/else per passare da una catena all'altra.

I problemi che iniziano a sorgere sono molteplici:

  • Il codice è un po' ridondante, in quanto contiene un sacco di boilerplate.

  • È difficile distinguere quale LLMChain è collegata a quale LLMChain, dobbiamo tracciare gli ingressi e le uscite per capirlo.

  • Si possono facilmente commettere errori sui tipi di uscita delle catene, per esempio, l'uscita di is_a_product_chain() è una str che dovrebbe essere successivamente valutata come bool.

Che cos'è il linguaggio di espressione LangChain (LCEL)?

LCEL è un'interfaccia e una sintassi unificate per scrivere catene di produzione composte e pronte per l'uso, ma c'è molto da spacchettare per capire cosa significa.

Cercheremo innanzitutto di capire la nuova sintassi riscrivendo la catena di prima.

Esempio - Nome dell'azienda e modello di business con LCEL

da langchain_core.runnables importare RunnablePassthrough
da langchain.prompts importare PromptTemplate
da langchain_community.llms importare OpenAI

USER_INPUT = "calzini colorati"
llm = OpenAI(temperatura=0)

prompt_template_product = “What is a good name for a company that makes {product}?”
prompt_template_business = “Give me the best business model idea for my company named: {company}”

chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
| {‘company’: RunnablePassthrough()}
| PromptTemplate.from_template(prompt_template_business)
| llm
)

business_model_output = chain.invoke({‘product’: ‘colorful socks’})

Un sacco di codice insolito, in poche righe:

  • C'è uno strano operatore | tra un PromptTemplate, un llm e un dizionario?! L'operatore | serve semplicemente a dire: "prendi il dizionario a sinistra e passalo come input dell'oggetto a destra".

  • Perché passiamo la variabile prodotto all'interno di un dizionario invece che in una stringa come prima? Se avete letto il paragrafo #1, sapete che l'operatore | si aspetta che gli input siano dizionari, quindi diamo l'argomento prodotto all'interno di un dizionario.

  • Perché c'è il nome della funzione RunnablePassthrough() invece del nome della società? RunnablePassthrough() è un segnaposto per dire: "per ora non abbiamo il nome dell'azienda, ma quando lo avremo, inseritelo qui". Spiegherò il significato del termine "Runnable" nelle prossime parti, per ora è bene ignorarlo.

  • Why do we need a specific method .invoke() instead of writing chain({‘product’:’colorful socks’}) ?We will understand this in the next part, but it is a sneak peek to why LCEL makes industrialization easier!

Ma è davvero più composito creare catene in questo modo?
Mettiamolo alla prova aggiungendo la funzione is_a_product_chain() e la ramificazione se l'input dell'utente non è corretto. Possiamo anche digitare la catena con Python Typing, come buona pratica.

Esempio - Personalizzazione e instradamento con LCEL

da typing import Dict
da langchain_core.runnables import RunnablePassthrough, RunnableBranch
da langchain.prompts import PromptTemplate
da langchain_core.output_parsers importare StrOutputParser
da langchain.output_parsers import BooleanOutputParser
da langchain_community.llms import OpenAI

USER_INPUT = "Harrrison Chase"
llm = OpenAI(temperatura=0)

prompt_template_product = “What is a good name for a company that makes {product}?”
prompt_template_cannot_respond = (
“You cannot respond to the user input: {product}n”
“Ask the user to input the name of a product in order for you to make a company out of it.n”
)
prompt_template_business = “Give me the best business model idea for my company named: {company}”
prompt_template_is_product = (
“Your goal is to find if the input of the user is a plausible product namen”
“Questions, greetings, long sentences, celebrities or other non relevant inputs are not considered productsn”
“input: {product}n”
“Answer only by ‘True’ or ‘False’ and nothing moren”
)

answer_user_chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
| {‘company’: RunnablePassthrough()}
| PromptTemplate.from_template(prompt_template_business)
| llm
).with_types(input_type=Dict[str, str], output_type=str)

is_product_chain = (
PromptTemplate.from_template(prompt_template_is_product)
| llm
| BooleanOutputParser(true_val='True', false_val='False')
).with_types(input_type=Dict[str, str], output_type=bool)

cannot_respond_chain = (
PromptTemplate.from_template(prompt_template_cannot_respond) | llm
).with_types(input_type=Dict[str, str], output_type=str)

full_chain = RunnableBranch(
(is_product_chain, answer_user_chain),
catena_non_possibile_rispondere
).with_types(input_type=Dict[str, str], output_type=str)

print(full_chain.invoke({‘product’: USER_INPUT}))

Elenchiamo le differenze:

  • La sintassi è diversa, questo è un dato di fatto.

  • Ci sono catene intermedie definite e chiamate in una catena più grande, quasi come le funzioni.

  • Gli ingressi e le uscite sono tipizzati, quasi come le funzioni.

  • Non sembra di essere in python.

Perché LCEL è migliore per l'industrializzazione?

Se stessi leggendo questo articolo fino a questo preciso punto e qualcuno mi chiedesse se sono convinto di LCEL, probabilmente risponderei di no. La sintassi è troppo diversa e probabilmente potrei organizzare il mio codice in funzioni per ottenere quasi lo stesso codice. Ma sono qui, a scrivere questo articolo, quindi deve esserci qualcosa di più.

Invocazione, stream e batch

Utilizzando LCEL la vostra catena ha automaticamente:

  • .invoke(): Si vuole passare l'input e ottenere l'output, niente di più e niente di meno.

  • .batch(): Se si vogliono passare più input per ottenere più output, la parallelizzazione viene gestita per l'utente (più veloce che chiamare invoke 3 volte).

  • .stream(): Consente di iniziare a stampare l'inizio del completamento prima che il completamento completo sia terminato.

my_chain = prompt | llm

# ---invocare--- #
result_with_invoke = my_chain.invoke("hello world!")

# ---batch--- #
result_with_batch = my_chain.batch(["hello", "world", "!"])

# ---stream--- #
per chunk in my_chain.stream("hello world!"):
print(chunk, flush=True, end="")

Quando si itera, si può usare il metodo invoke per facilitare il processo di sviluppo. Ma quando si mostra l'output della catena in un'interfaccia utente, si desidera eseguire lo streaming della risposta. Ora è possibile utilizzare il metodo stream senza riscrivere nulla.

Metodi asincroni pronti per l'uso

Nella maggior parte dei casi, il frontend e il backend dell'applicazione saranno separati, il che significa che il frontend farà una richiesta al backend. Se si hanno più utenti, potrebbe essere necessario gestire più richieste contemporaneamente sul backend.

Dato che la maggior parte del codice di LangChain non fa altro che aspettare tra una chiamata all'API e l'altra, possiamo sfruttare il codice asincrono per migliorare la scalabilità dell'API; se volete capire perché è importante, vi consiglio di leggere la storia degli hamburger concorrenti nella documentazione di FastAPI.
Non c'è bisogno di preoccuparsi dell'implementazione, perché i metodi asincroni sono già disponibili se si usa LCEL:

.ainvoke() / .abatch() / .astream: versioni asincrone di invoke, batch e stream.

Consiglio anche di leggere la pagina Perché usare LCEL dalla documentazione di LangChain, con esempi per ogni metodo sync / async.

Langchain ha ottenuto queste caratteristiche "out of the box" creando un'interfaccia unificata chiamata "Runnable". Ora, per sfruttare appieno LCEL, è necessario approfondire il significato di questa nuova interfaccia Runnable.

L'interfaccia Runnable

Tutti gli oggetti che abbiamo usato finora nella sintassi di LCEL sono Runnables. Si tratta di un oggetto python creato da LangChain, che eredita automaticamente tutte le caratteristiche di cui abbiamo parlato prima e molte altre. Usando la sintassi LCEL, componiamo un nuovo Runnable a ogni passo, il che significa che l'oggetto finale creato sarà anch'esso un Runnable. Per saperne di più sull'interfaccia, consultare la documentazione ufficiale.

Tutti gli oggetti del codice sottostante sono Runnable o dizionari che vengono automaticamente convertiti in Runnable:

da langchain_core.runnables import RunnablePassthrough, RunnableParallel
da langchain.prompts importare PromptTemplate
da langchain_community.llms importare OpenAI

chain_number_one = (
PromptTemplate.from_template(prompt_template_product)
| llm
| {‘company’: RunnablePassthrough()} # <— THIS WILL CHANGE
| PromptTemplate.from_template(prompt_template_business)
| llm
)

chain_number_two = (
PromptTemplate.from_template(prompt_template_product)
| llm
| RunnableParallel(company=RunnablePassthrough()) # <— THIS CHANGED
| PromptTemplate.from_template(prompt_template_business)
| llm
)

print(catena_numero_uno == catena_numero_due)
>>> Vero

Perché usare RunnableParallel() e non semplicemente Runnable()?

Perché ogni Runnable all'interno di un RunnableParallel viene eseguito in parallelo. Ciò significa che se avete 3 passi indipendenti nel vostro Runnable, essi verranno eseguiti contemporaneamente su thread diversi della vostra macchina, migliorando gratuitamente la velocità della vostra catena!

Svantaggi di LCEL

Nonostante i suoi vantaggi, LCEL presenta alcuni potenziali svantaggi:

  • Non completamente conforme alla PEP: LCEL non rispetta pienamente il PEP20, lo Zen di Python, che afferma che "l'esplicito è meglio dell'implicito". (Inoltre, la sintassi di LCEL non è considerata "pitonica", in quanto sembra un linguaggio diverso, il che potrebbe rendere LCEL meno intuitivo per alcuni sviluppatori Python che potrebbero rifiutarsi di usarlo.

  • LCEL è un linguaggio specifico per il dominio (DSL): Ci si aspetta che gli utenti abbiano una certa conoscenza di prompt, catene o LLM per poter sfruttare la sintassi in modo efficiente.

  • Dipendenze tra input e output: Gli input intermedi e gli output finali devono essere trasmessi dall'inizio alla fine. Per esempio, se si vuole usare l'output di un passo intermedio come output finale, bisogna farlo passare attraverso tutti i passi successivi. Questo può portare ad avere argomenti in più nella maggior parte delle catene, che potrebbero non essere utilizzati ma che sono necessari se si vuole accedervi attraverso l'output.

Conclusione

In conclusione, LangChain Expression Language (LCEL) è uno strumento potente che porta una nuova prospettiva nella costruzione di applicazioni Python. Nonostante la sua sintassi non convenzionale, consiglio vivamente di utilizzare LCEL per i seguenti motivi:

  • Interfaccia unificata: Fornisce un'interfaccia coerente per tutte le catene, rendendo più facile l'industrializzazione del codice con stream, async, modelli di fallback, tipizzazione, configurazioni in tempo di esecuzione, ecc...

  • Parallelizzazione automatica: Esegue automaticamente più attività in parallelo, migliorando la velocità di esecuzione delle catene e l'esperienza dell'utente.

  • Compositività: Permette di comporre e modificare facilmente le catene, rendendo il codice più flessibile e adattabile.

Per andare oltre...

L'astrazione runnable

In alcuni casi, credo sia importante capire l'astrazione che LangChain ha implementato per far funzionare la sintassi LCEL.

È possibile reimplementare facilmente le funzionalità di base di Runnable come segue:

classe Runnable:
def __init__(self, func):
self.func = func

def __or__(self, other):
def chained_func(*args, **kwargs):
# self.func è a sinistra, other è a destra
return altro(self.func(*args, **kwargs))
return Runnable(chained_func)

def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)

def add_ten(x):
restituisce x + 10

def divide_per_due(x):
restituisce x / 2

runnable_add_ten = Runnable(add_ten)
runnable_divide_per_due = Runnable(divide_per_due)
catena = runnable_add_ten | runnable_divide_by_two
risultato = catena(8) # (8+10) / 2 = 9,0 dovrebbe essere la risposta
print(risultato)
>>> 9.0

Un Runnable è semplicemente un oggetto python in cui il metodo .__or__() è stato sovrascritto.
In pratica, LangChain ha aggiunto molte funzionalità, come la conversione di dizionari in Runnable, capacità di tipizzazione, capacità di configurazione e metodi invoke, batch, stream e async!

Quindi, perché non provare LCEL nel vostro prossimo progetto?

Se volete saperne di più, vi consiglio di consultare il ricettario LangChain su LCEL.

class="lazyload

Medium Blog di Artefact.

Questo articolo è stato pubblicato inizialmente su Medium.com.
Seguiteci sul nostro blog Medium!