Kort gezegd
LangChain is in minder dan een jaar 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 de mogelijkheid ontbrak om complexe en schaalbare applicaties te bouwen.
Alles veranderde in augustus 2023 toen ze LangChain Expression Language (LCEL) uitbrachten, een nieuwe syntaxis die de kloof tussen POC en productie overbrugt. Dit artikel leidt je door de ins en outs van LCEL, laat zien hoe het het maken van aangepaste chains vereenvoudigt en waarom je het moet leren als je LLM-applicaties bouwt!
Prompts, LLM’s en ketenmodellen: even een opfrisser
Voordat we ons verdiepen in de LCEL-syntaxis, lijkt het me nuttig om onze kennis van LangChain-begrippen zoals LLM, Prompt en zelfs een Chain even op te frissen.
LLM: In LangChain is een LLM een abstractie van het model dat wordt gebruikt om aanvullingen te genereren, zoals OpenAI GPT-3.5, Claude, enz.
Opdracht: Dit is de invoer voor het LLM-object, dat vragen aan de LLM zal stellen en de doelstellingen zal aangeven.
Keten: Dit verwijst naar een reeks aanroepen van een LLM, of een willekeurige stap in data .

Nu we de definities hebben gehad, laten we eens aannemen dat we een organisatie willen oprichten! We hebben een hele coole en pakkende naam nodig, en een bedrijfsmodel om wat geld te verdienen!
Voorbeeld — organisatie & bedrijfsmodel met Old Chains
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI
USER_INPUT = "kleurrijke sokken"
llm = OpenAI(temperature=0)
prompt_template_product = "Wat is een goede naam voor een organisatie ... maakt?"
organisatie= LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
organisatie= organisatie(USER_INPUT)
prompt_template_business = "Geef me het beste idee voor een bedrijfsmodel voor mijn organisatie : "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
business_model_output = business_model_chain(organisatie["text"])
print(organisatie)
print(bedrijfsmodel_uitvoer)
>>>
>>>
Dit is vrij eenvoudig te volgen; er is wel wat herhaling, maar dat valt wel mee.
Laten we wat maatwerk toevoegen door rekening te houden met situaties waarin de gebruiker onze keten niet gebruikt zoals bedoeld.
Misschien voert de gebruiker iets in dat helemaal niets te maken heeft met het doel van onze keten? In dat geval willen we dat detecteren en daarop adequaat reageren.
Voorbeeld — Aanpassing en routering met oude ketens
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI
import ast
USER_INPUT = "Harrison Chase"
llm = OpenAI(temperature=0)
# —- Dezelfde code als eerder
prompt_template_product = "Wat is een goede naam voor een organisatie ? maakt?"
organisatie= LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
prompt_template_business = "Geef me het beste idee voor een bedrijfsmodel voor mijn organisatie : "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
# —- Nieuwe code
prompt_template_is_product = (
"Je doel is om te bepalen of de invoer van de gebruiker een aannemelijke productnaam is"
"Vragen, begroetingen, lange zinnen, beroemdheden of andere niet-relevante invoer worden niet als productnamen beschouwd"
"invoer: n"
"Geef alleen 'True' of 'False' als antwoord, en verder niets"
)
prompt_template_cannot_respond = (
"Je kunt niet reageren op de invoer van de gebruiker: n"
"Vraag de gebruiker om de naam van een product in te voeren, zodat je er een organisatie kunt maken.n"
)
cannot_respond_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_cannot_respond))
organisatie= 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` toepassen op een niet-lege tekenreeks, wordt dit `True`, dus hebben we `literal_eval` nodig
is_a_product = ast.literal_eval(is_a_product_chain(USER_INPUT)["text"])
if is_a_product:
organisatie= organisatie(USER_INPUT)
business_model_output = business_model_chain(organisatie["text"])
print(business_model_output)
else:
print(cannot_respond_chain(USER_INPUT))
Dit wordt een beetje lastiger te begrijpen, dus laten we het even samenvatten:
Er doen zich verschillende problemen voor:
Wat is LangChain Expression Language (LCEL)?
LCEL is een uniforme interface en syntaxis voor het schrijven van samenstelbare, productieklare ketens; er komt heel wat bij kijken om te begrijpen wat dit precies inhoudt.
We gaan eerst proberen de nieuwe syntaxis te begrijpen door de keten van daarnet te herschrijven.
Voorbeeld — organisatie & bedrijfsmodel met LCEL
from langchain_core.runnables import RunnablePassthrough
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI
USER_INPUT = "kleurrijke sokken"
llm = OpenAI(temperature=0)
prompt_template_product = "Wat is een goede naam voor een organisatie ... maakt?"
prompt_template_business = "Geef me het beste idee voor een bedrijfsmodel voor mijn organisatie : "
chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
|
| PromptTemplate.from_template(prompt_template_business)
| llm
)
business_model_output = chain.invoke()
Heel wat ongebruikelijke code, in slechts een paar regels:
Maar is het echt beter te combineren om op deze manier ketens te maken?
Laten we het eens testen door de functie `is_a_product_chain()` toe te voegen en een vertakking in te bouwen voor het geval de gebruikersinvoer niet klopt. We kunnen de keten zelfs typen met Python Typing; laten we dit doen als een goede gewoonte.
Voorbeeld — Aanpassing en routering met LCEL
from typing import Dict
from langchain_core.runnables import RunnablePassthrough, RunnableBranch
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.output_parsers import BooleanOutputParser
from langchain_community.llms import OpenAI
USER_INPUT = "Harrrison Chase"
llm = OpenAI(temperature=0)
prompt_template_product = "Wat is een goede naam voor een organisatie ? maakt?"
prompt_template_cannot_respond = (
"Je kunt niet reageren op de gebruikersinvoer: n"
"Vraag de gebruiker om de naam van een product in te voeren, zodat je er een organisatie kunt maken.n"
)
prompt_template_business = "Geef me het beste idee voor een bedrijfsmodel organisatie mijn organisatie : "
prompt_template_is_product = (
"Je doel is om te bepalen of de invoer van de gebruiker een plausibele productnaam is"
"Vragen, begroetingen, lange zinnen, beroemdheden of andere niet-relevante invoer worden niet als producten beschouwd"
"invoer: n"
"Antwoord alleen met 'Waar' of 'Onwaar' en niets meer"
)
answer_user_chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
|
| 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),
cannot_respond_chain
).with_types(input_type=Dict[str, str], output_type=str)
print(full_chain.invoke())
Laten we de verschillen eens op een rijtje zetten:
Waarom is LCEL beter geschikt voor industrialisatie?
Als ik dit artikel tot precies dit punt had gelezen en iemand mij zou vragen of ik overtuigd ben van LCEL, zou ik waarschijnlijk nee zeggen. De syntaxis is te anders, en ik kan mijn code waarschijnlijk in functies indelen om zo bijna precies dezelfde code te krijgen. Maar ik zit hier dit artikel te schrijven, dus er moet meer aan de hand zijn.
Direct aan de slag met invoke, stream en batch
Door gebruik te maken van LCEL beschikt uw keten automatisch over:
my_chain = prompt | llm
# ———invoke——— #
result_with_invoke = my_chain.invoke(“hello world!”)
# ———batch——— #
result_with_batch = my_chain.batch([“hello”, “world”, “!”])
# ———stream——— #
for chunk in my_chain.stream(“hello world!”):
print(chunk, flush=True, end=””)
Bij het doorlopen van een iteratie kun je de methode `invoke` gebruiken om het ontwikkelingsproces te vereenvoudigen. Maar als je de uitvoer van je keten in een gebruikersinterface wilt weergeven, wil je het antwoord streamen. Je kunt nu de methode `stream` gebruiken zonder iets te hoeven herschrijven.
Kant-en-klare asynchrone methoden
Meestal zijn de frontend en de backend van je applicatie gescheiden, wat betekent dat de frontend een verzoek naar de backend stuurt. Als je meerdere gebruikers hebt, moet je mogelijk meerdere verzoeken tegelijkertijd in je backend verwerken.
Aangezien het grootste deel van de code in LangChain bestaat uit wachten tussen API-aanroepen, kunnen we gebruikmaken van asynchrone code om de schaalbaarheid van de API te verbeteren. Als je wilt begrijpen waarom dit belangrijk is, raad ik je aan het verhaal over ‘concurrent burgers’ in de FastAPI-documentatie te lezen.
Je hoeft je geen zorgen te maken over de implementatie, want als je LCEL gebruikt, zijn asynchrone methoden al beschikbaar:
.ainvoke() / .abatch() / .astream: asynchrone versies van invoke, batch en stream.
Ik raad ook aan om de pagina „Waarom LCEL gebruiken?“ uit de LangChain-documentatie te lezen, met voorbeelden voor elke synchrone en asynchrone methode.
Langchain heeft deze ‘kant-en-klare’ functies gerealiseerd door een uniforme interface te ontwikkelen met de naam ‘Runnable’. Om LCEL ten volle te benutten, moeten we nu eens nader bekijken wat deze nieuwe Runnable-interface precies inhoudt.
De Runnable-interface
Alle objecten die we tot nu toe in de LCEL-syntaxis hebben gebruikt, zijn Runnables. Dit is een Python-object dat door LangChain wordt aangemaakt; dit object neemt automatisch alle functies over die we eerder hebben besproken, 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 dat wordt aangemaakt ook een Runnable is. Meer informatie over de interface vind je in de officiële documentatie.
Alle objecten uit de onderstaande code zijn ofwel `Runnable` ofwel woordenboeken die automatisch worden omgezet naar een `Runnable`:
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI
chain_number_one = (
PromptTemplate.from_template(prompt_template_product)
| llm
| # <— 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(chain_number_one == chain_number_two)
>>> True
Waarom gebruiken we RunnableParallel() en niet gewoon Runnable()?
Omdat elke Runnable binnen een RunnableParallel parallel wordt uitgevoerd. Dit betekent dat als je drie onafhankelijke stappen in je Runnable hebt, deze tegelijkertijd op verschillende threads van je computer worden uitgevoerd, waardoor de snelheid van je keten vanzelf toeneemt!
Nadelen van LCEL
Ondanks de voordelen kent LCEL wel enkele mogelijke nadelen:
Conclusie
Kortom, LangChain Expression Language (LCEL) is een krachtig hulpmiddel dat een frisse kijk biedt op het bouwen van Python-toepassingen. Ondanks de onconventionele syntaxis raad ik het gebruik van LCEL ten zeerste aan om de volgende redenen:
Voor meer informatie…
De uitvoerbare abstractie
In sommige gevallen vind ik het belangrijk om te begrijpen welke abstractie LangChain heeft geïmplementeerd om de LCEL-syntaxis te laten werken.
Je kunt de basisfuncties van `Runnable` eenvoudig als volgt opnieuw implementeren:
class 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 divide_by_two(x):
return x / 2
runnable_add_ten = Runnable(add_ten)
runnable_divide_by_two = Runnable(divide_by_two)
chain = runnable_add_ten | runnable_divide_by_two
result = chain(8) # (8+10) / 2 = 9.0 zou het antwoord moeten zijn
print(result)
>>> 9.0
Een Runnable is simpelweg een Python-object waarbij de methode .__or__() is overschreven.
In de praktijk heeft LangChain tal van functionaliteiten toegevoegd, zoals het omzetten van woordenboeken naar Runnable, typedefinities, configuratiemogelijkheden en methoden voor het aanroepen, verwerken in batches, streamen en asynchroon verwerken!
Waarom zou u LCEL dan niet eens uitproberen bij uw volgende project?
Als je meer wilt weten, raad ik je ten zeerste aan om het LangChain-cookbook op LCEL eens door te nemen.

BLOG






