TL;DR
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.

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:
I problemi che iniziano a sorgere sono molteplici:
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:
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:
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:
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:
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:
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.