Kort gezegd

  • Snellere overgang van proof of concept naar productie: Zoals in de Langchain-documentatie wordt beschreven: „LCEL is een declaratieve manier om keten eenvoudig aan elkaar te koppelen. LCEL is vanaf het begin ontworpen om prototypes zonder aanpassingen aan de code in productie te kunnen nemen”.

  • Het maken van aangepaste ketens: LCEL vereenvoudigt het proces van het maken van aangepaste ketens met een nieuwe syntaxis.

  • Direct klaar voor gebruik: streaming en batchverwerking: LCEL biedt u gratis mogelijkheden voor batchverwerking, streaming en asynchrone verwerking.

  • Uniforme interface: deze services parallellisatie, typemogelijkheden en alle toekomstige functies die LangChain mogelijk nog zal ontwikkelen.

  • LCEL is de toekomst van LangChain: LCEL biedt een frisse kijk op de ontwikkeling van op LLM’s gebaseerde applicaties. Ik raad het ten zeerste aan voor je volgende LLM-project.

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 .

class="lazyload

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:

  • We hebben een nieuwe keten is_real_product_chain() gemaakt die detecteert of de invoer van de gebruiker als een product kan worden beschouwd.

  • We gebruiken if/else-voorwaarden om tussen de ketens te schakelen.

Er doen zich verschillende problemen voor:

  • De code is een beetje overbodig omdat er veel standaardtekst in zit.

  • Het is moeilijk te achterhalen welke LLMChain met welke LLMChain is verbonden; we moeten de invoer en uitvoer traceren om dit te begrijpen.

  • We kunnen gemakkelijk fouten maken met betrekking tot de uitvoertypes van de ketens; de uitvoer van `is_a_product_chain()` is bijvoorbeeld een `str` die later als `bool` moet worden geëvalueerd.

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:

  • Er staat een vreemde |-operator tussen een PromptTemplate, een llm en een woordenboek?! De |-operator is er simpelweg om aan te geven: „neem het woordenboek aan de linkerkant en geef het door als invoer voor het object aan de rechterkant”.

  • Waarom geven we de variabele `product` nu door in een woordenboek in plaats van als een tekenreeks, zoals eerder? Als je punt 1 hebt gelezen, weet je dat de |-operator de invoer als woordenboek verwacht; daarom geven we het argument `product` door in een woordenboek.

  • Waarom heet de functie RunnablePassthrough() in plaats van de organisatie ? RunnablePassthrough() is een tijdelijke aanduiding die betekent: „we hebben de organisatie op dit moment nog niet, maar zodra we die hebben, komt die hier te staan“. Ik zal in de volgende delen uitleggen wat de term „Runnable“ inhoudt; voorlopig kun je die gerust negeren.

  • Waarom hebben we een specifieke methode .invoke() nodig in plaats van chain() te schrijven?Dat zullen we in het volgende deel zien, maar dit is alvast een voorproefje van waarom LCEL industrialisatie eenvoudiger maakt!

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:

  • De syntaxis is anders, oké, dat spreekt voor zich.

  • Er zijn tussenliggende ketens gedefinieerd die binnen een grotere keten worden aangeroepen, bijna zoals functies.

  • Invoer en uitvoer hebben een type, net als functies.

  • Dit voelt niet als Python.

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:

  • .invoke(): Je wilt je invoer doorgeven en de uitvoer ontvangen, niet meer en niet minder.

  • .batch(): Als je meerdere invoergegevens wilt doorgeven om meerdere uitvoerresultaten te verkrijgen, wordt de parallellisatie automatisch voor je afgehandeld (sneller dan wanneer je de methode `invoke` drie keer zou aanroepen).

  • .stream(): Hiermee kun je het begin van de aanvulling weergeven voordat de volledige aanvulling is voltooid.

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:

  • Niet volledig PEP-conform: LCEL voldoet niet volledig aan PEP 20, de Zen van Python, waarin staat dat „expliciet beter is dan impliciet“. (Om PEP 20 te controleren, kun je in Python het commando `import this` uitvoeren.) Bovendien wordt de syntaxis van LCEL niet als „Pythonic“ beschouwd, omdat het aanvoelt als een andere taal; dit zou LCEL voor sommige Python-ontwikkelaars minder intuïtief kunnen maken, waardoor zij het wellicht niet willen gebruiken.

  • LCEL is een domeinspecifieke taal (DSL): van gebruikers wordt verwacht dat ze enige kennis hebben van prompts, ketens of LLM’s om de syntaxis efficiënt te kunnen gebruiken.

  • Invoer-/uitvoerrelaties: Tussentijdse invoer en einduitvoer moeten van het begin tot het einde worden doorgegeven. Als je bijvoorbeeld de uitvoer van een tussenstap als einduitvoer wilt gebruiken, moet je deze door alle volgende stappen heen doorgeven. Dit kan in de meeste van je ketens leiden tot extra argumenten, die misschien niet worden gebruikt, maar wel nodig zijn als je er via de uitvoer toegang toe wilt hebben.

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:

  • Unified Interface: biedt een consistente interface voor alle chains, waardoor het eenvoudiger wordt om je code te industrialiseren met kant-en-klare modellen voor streams, asynchrone verwerking en fallbacks, typecontrole, runtime-configuraties, enzovoort…

  • Automatische parallellisatie: voer automatisch meerdere taken tegelijkertijd uit, waardoor de uitvoersnelheid van uw ketens toeneemt en de gebruikerservaring wordt verbeterd.

  • Samenstelbaarheid: hiermee kun je eenvoudig ketens samenstellen en aanpassen, waardoor je code flexibeler en beter aanpasbaar wordt.

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.

class="lazyload

Medium-blog van Artefact.

Dit artikel is oorspronkelijk gepubliceerd op Medium.com.
Volg ons op onze Medium-blog!