TL;DR
LangChain is in minder dan een jaar tijd uitgegroeid tot een van de meest gebruikte Python-bibliotheken voor interactie met LLM's, maar LangChain was vooral een bibliotheek voor POC's omdat het niet de mogelijkheid had om complexe en schaalbare toepassingen te maken.
Alles veranderde in augustus 2023 toen ze LangChain Expression Language (LCEL) uitbrachten, een nieuwe syntaxis die de kloof van POC naar productie overbrugt. Dit artikel leidt je door de ins en outs van LCEL, laat je zien hoe het het maken van aangepaste ketens vereenvoudigt en waarom je het moet leren als je LLM applicaties bouwt!
Prompts, LLM en kettingen, laten we ons geheugen opfrissen
Voordat we in de LCEL syntax duiken, denk ik dat het goed is om ons geheugen op te frissen over LangChain concepten zoals LLM en Prompt of zelfs een Chain.
LLM: In langchain is llm een abstractie rond het model dat gebruikt wordt om de aanvullingen te maken, zoals openai gpt3.5, claude, etc..
Prompt: Dit is de invoer van het LLM-object, dat de LLM vragen zal stellen en zijn doelstellingen zal geven.
Keten: Dit verwijst naar een opeenvolging van aanroepen naar een LLM, of een willekeurige data verwerkingsstap.

Nu de definities uit de weg zijn, stel dat we een organisatie willen maken! We hebben een coole en pakkende naam nodig en een businessmodel om geld te verdienen!
Voorbeeld - organisatie naam & bedrijfsmodel met oude ketens
van langchain.chains importeer LLMChain
van langchain.prompts importeer PromptTemplate
van langchain_community.llms importeer OpenAI
USER_INPUT = "kleurrijke sokken".
llm = OpenAI(temperatuur=0)
prompt_template_product = "Wat is een goede naam voor een organisatie die {product} maakt?"
organisatie_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
organisatie_naam_uitvoer = organisatie_naam_keten(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(organisatie_naam_uitvoer)
afdrukken(bedrijfs_model_uitvoer)
>>> {‘product’: ‘colorful socks’, ‘text’: ‘Socktastic!’}
>>> {‘company’: ‘Socktastic!’,’text’: “A subscription-based service offering a monthly delivery…”}
Dit is vrij gemakkelijk te volgen, we zien een beetje redundantie, maar het is beheersbaar.
Laten we wat maatwerk toevoegen door de gevallen af te handelen waarin de gebruiker onze keten niet gebruikt zoals verwacht.
Misschien voert de gebruiker iets in dat helemaal niets te maken heeft met het doel van onze keten? In dat geval willen we dit detecteren en op de juiste manier reageren.
Voorbeeld - Aanpassing & Routing met oude ketens
van langchain.chains importeer LLMChain
van langchain.prompts importeer PromptTemplate
van langchain_community.llms importeer OpenAI
importeer ast
USER_INPUT = "Harrison Chase".
llm = OpenAI(temperatuur=0)
# Dezelfde code als eerder
prompt_template_product = "Wat is een goede naam voor een organisatie die {product} maakt?"
organisatie_naam_keten = 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))
# Nieuwe code
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))
organisatie_naam_keten = 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))
# Als we bool gebruiken op een niet lege str zal het True zijn, dus hebben we `literal_eval` nodig
is_a_product = ast.literal_eval(is_a_product_chain(USER_INPUT)["text"])
Als is_a_product:
organisatie_naam_uitvoer = organisatie_naam_keten(USER_INPUT)
business_model_output = business_model_chain(organisatie_naam_output["tekst"])
afdrukken(bedrijfs_model_uitvoer)
anders:
print(cannot_respond_chain(USER_INPUT))
Dit wordt wat moeilijker te begrijpen, laten we het samenvatten:
Er doen zich verschillende problemen voor:
Wat is LangChain Expression Language (LCEL)?
LCEL is een uniforme interface en syntax om samenstelbare productieklare ketens te schrijven, er is veel om uit te pakken om te begrijpen wat het betekent.
We zullen eerst proberen de nieuwe syntaxis te begrijpen door de keten van eerder te herschrijven.
Voorbeeld - organisatie naam & bedrijfsmodel met LCEL
uit langchain_core.runnables importeer RunnablePassthrough
uit langchain.prompts importeert PromptTemplate
van langchain_community.llms importeer OpenAI
USER_INPUT = "kleurrijke sokken".
llm = OpenAI(temperatuur=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’})
Veel ongebruikelijke code, in slechts een paar regels :
Maar is het echt beter samen te stellen om op deze manier ketens te maken?
Laten we de proef op de som nemen door de is_a_product_chain() toe te voegen en de vertakking als de invoer van de gebruiker niet correct is. We kunnen de keten zelfs typen met Python Typing, laten we dit doen als een goede oefening.
Voorbeeld - Aanpassing & Routing met LCEL
van typing importeer Dict
uit langchain_core.runnables importeer RunnablePassthrough, RunnableBranch
uit langchain.prompts importeer PromptTemplate
uit langchain_core.output_parsers importeert StrOutputParser
van langchain.output_parsers import BooleanOutputParser
van langchain_community.llms importeer OpenAI
USER_INPUT = "Harrrison Chase".
llm = OpenAI(temperatuur=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)
kan niet_reageren_keten = (
PromptTemplate.from_template(prompt_template_cannot_respond) | llm
).with_types(input_type=Dict[str, str], output_type=str)
volledige_keten = RunnableBranch(
(is_product_chain, antwoord_gebruiker_chain),
kan_niet_reageren_keten
).with_types(input_type=Dict[str, str], output_type=str)
print(full_chain.invoke({‘product’: USER_INPUT}))
Laten we de verschillen eens op een rijtje zetten:
Waarom is LCEL beter voor industrialisatie?
Als ik dit artikel tot op dit punt aan het lezen was en iemand zou me vragen of ik overtuigd was van LCEL, zou ik waarschijnlijk nee zeggen. De syntax is te anders en ik kan mijn code waarschijnlijk in functies indelen om bijna exact dezelfde code te krijgen. Maar ik ben hier en schrijf dit artikel, dus er moet iets meer zijn.
Out of the box aanroepen, streamen en batchen
Door LCEL te gebruiken heeft je ketting automatisch:
mijn_keten = prompt | llm
# ---invoke-- #
resultaat_met_invoke = mijn_keten.invoke("hallo wereld!")
# ---batch--- #
resultaat_met_batch = mijn_keten.batch(["hallo", "wereld", "!"])
# -stroom #
voor chunk in my_chain.stream("hello world!"):
print(chunk, flush=True, end="")
Wanneer je itereert, kun je de invoke methode gebruiken om het ontwikkelproces te vergemakkelijken. Maar als je de uitvoer van je keten in een UI toont, wil je het antwoord streamen. Je kunt nu de stream methode gebruiken zonder iets te herschrijven.
Out of the box async methoden
Meestal zullen de frontend en backend van je applicatie gescheiden zijn, wat betekent dat de frontend een verzoek doet aan de backend. Als je meerdere gebruikers hebt, moet je misschien meerdere verzoeken tegelijkertijd afhandelen op je backend.
Aangezien het grootste deel van de code in LangChain alleen maar wacht tussen API-aanroepen, kunnen we asynchrone code gebruiken om de schaalbaarheid van de API te verbeteren. Als je wilt begrijpen waarom dit belangrijk is, raad ik je aan om het concurrent burgers verhaal van de FastAPI documentatie te lezen.
Je hoeft je geen zorgen te maken over de implementatie, want async methodes zijn al beschikbaar als je LCEL gebruikt:
.ainvoke() / .abatch() / .astream: asynchrone versies van invoke, batch en stream.
Ik raad ook aan om de Waarom LCEL gebruiken pagina van LangChain documentatie te lezen met voorbeelden voor elke sync / async methode.
Langchain heeft deze "out of the box" functies bereikt door een uniforme interface te maken met de naam "Runnable". Om LCEL volledig te benutten, moeten we ons verdiepen in deze nieuwe Runnable interface.
De Runnable-interface
Elk object dat we tot nu toe hebben gebruikt in de LCEL syntaxis is een Runnable. Het is een python object gemaakt door LangChain, dit object erft automatisch elke eigenschap waar we het eerder over hebben gehad en nog veel meer. Door de LCEL syntaxis te gebruiken, stellen we bij elke stap een nieuwe Runnable samen, wat betekent dat het uiteindelijke object ook een Runnable zal zijn. Je kunt meer leren over de interface in de officiële documentatie.
Alle objecten in de onderstaande code zijn ofwel Runnable of woordenboeken die automatisch worden geconverteerd naar een Runnable :
uit langchain_core.runnables importeer RunnablePassthrough, RunnableParallel
van langchain.prompts importeer PromptTemplate
van langchain_community.llms importeer 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(keten_nummer_één == keten_nummer_twee)
>>>waar
Waarom gebruiken we RunnableParallel() en niet gewoon Runnable()?
Omdat elke Runnable in een RunnableParallel parallel wordt uitgevoerd. Dit betekent dat als je 3 onafhankelijke stappen in je Runnable hebt, ze tegelijkertijd op verschillende threads van je machine worden uitgevoerd, waardoor de snelheid van je keten gratis wordt verbeterd!
Nadelen van LCEL
Ondanks de voordelen heeft LCEL ook enkele potentiële nadelen:
Conclusie
Concluderend kan ik zeggen dat LangChain Expression Language (LCEL) een krachtige tool is die een frisse kijk geeft op het bouwen van Python applicaties. Ondanks de onconventionele syntaxis, raad ik het gebruik van LCEL ten zeerste aan om de volgende redenen :
Verder gaan...
De runnable abstractie
In sommige gevallen denk ik dat het belangrijk is om de abstractie te begrijpen die LangChain heeft geïmplementeerd om de LCEL syntax te laten werken.
Je kunt de basisfunctionaliteiten van Runnable eenvoudig als volgt herimplementeren:
klasse Runnable:
def __init__(self, func):
self.func = func
def __or__(self, other):
def chained_func(*args, **kwargs):
# self.func staat links, other staat rechts
return other(self.func(*args, **kwargs))
return Runnable(chained_func)
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def add_ten(x):
return x + 10
def deel_bij_twee(x):
return x / 2
runnable_add_ten = Runnable(add_ten)
runnable_divide_by_two = Runnable(divide_by_two)
keten = runnable_add_ten | runnable_divide_by_two
resultaat = keten(8) # (8+10) / 2 = 9,0 zou het antwoord moeten zijn
print(resultaat)
>>> 9.0
Een Runnable is simpelweg een python object waarin de .__or__() methode is overschreven.
In de praktijk heeft LangChain veel functionaliteiten toegevoegd, zoals het converteren van woordenboeken naar Runnable, typingsmogelijkheden, configureerbaarheidsmogelijkheden en invoke-, batch-, stream- en async-methodes!
Dus waarom LCEL niet eens proberen in je volgende project?
Als je meer wilt weten, raad ik je ten zeerste aan om het LangChain kookboek op LCEL door te bladeren.