TL;DR

  • Sneller van POC naar prod: Zoals de documentatie van langchain het omschrijft: "LCEL is een declaratieve manier om eenvoudig ketens samen te stellen. LCEL is vanaf dag 1 ontworpen om prototypes in productie te nemen, zonder code te veranderen".

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

  • Out of the box streaming en batch: LCEL geeft u gratis batch, streaming en async mogelijkheden.

  • Uniforme interface: Het services automatische parallellisatie, typmogelijkheden en alle toekomstige functies die LangChain zou kunnen ontwikkelen.

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

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.

class="img-responsive

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:

  • 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 implementeren if/else-voorwaarden om tussen de ketens te schakelen.

Er doen zich verschillende problemen voor:

  • De code is een beetje overbodig omdat er veel boilerplate is.

  • Het is moeilijk om te onderscheiden welke LLMChain is gekoppeld aan welke LLMChain, we moeten de inputs en outputs traceren om het te begrijpen.

  • We kunnen gemakkelijk fouten maken in de uitvoertypes van de ketens, bijvoorbeeld, de uitvoer van is_a_product_chain() is een str die later geëvalueerd zou moeten worden als bool.

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 :

  • Er is een rare | operator tussen een PromptTemplate, een llm en een woordenboek! De | operator is er simpelweg om te zeggen: "neem het linker woordenboek en geef het door als invoer van het rechter object".

  • Waarom geven we de variabele product door in een dictionnaire in plaats van in een string zoals voorheen? Als je #1 hebt gelezen, weet je dat de | operator de invoer als woordenboek verwacht, daarom geven we het argument product door in een woordenboek.

  • Waarom is er een functienaam RunnablePassthrough() in plaats van de naam organisatie ? De RunnablePassthrough() is een placeholder om te zeggen: "we hebben nu nog geen organisatie naam, maar als we die hebben, plaats hem dan hier". Ik zal in de volgende delen uitleggen wat de term "Runnable" betekent, voor nu is het goed om het te negeren.

  • 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!

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:

  • De syntaxis is anders, ok dat is een gegeven.

  • Er zijn tussenliggende ketens gedefinieerd en aangeroepen in een grotere keten, bijna zoals functies.

  • In- en uitgangen worden getypt, bijna zoals functies.

  • Dit voelt niet als python.

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:

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

  • .batch(): Je wilt meerdere ingangen doorgeven om meerdere uitgangen te verkrijgen, de parallellisatie wordt voor je afgehandeld (sneller dan 3 keer invoke aanroepen).

  • .stream(): Hiermee kun je beginnen met het afdrukken van het begin van de voltooiing voordat de volledige voltooiing is voltooid.

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:

  • Niet volledig PEP-compliant: LCEL voldoet niet volledig aan PEP20, de Zen van Python, die stelt dat "expliciet beter is dan impliciet". (Om PEP20 te controleren kun je dit importeren in Python.) Daarnaast wordt LCEL's syntax niet als "Pythonisch" beschouwd omdat het aanvoelt als een andere taal. Dit kan LCEL minder intuïtief maken voor sommige Python ontwikkelaars die het misschien 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 syntax efficiënt te kunnen gebruiken.

  • Afhankelijkheden tussen invoer en uitvoer: Tussenliggende inputs en finale outputs moeten worden doorgegeven van het begin tot het einde. Als je bijvoorbeeld de uitvoer van een tussenstap wilt gebruiken als de uiteindelijke uitvoer, moet je deze door alle volgende stappen meenemen. Dit kan leiden tot extra argumenten in de meeste van je ketens, die misschien niet worden gebruikt maar wel nodig zijn als je ze wilt doorvoeren via de uitvoer.

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 :

  • Unified Interface: Biedt een consistente interface voor alle ketens, waardoor het eenvoudiger wordt om je code te industrialiseren met out of the box stream, async, fallback modellen, typing, run time configuraties, enz...

  • Automatische parallellisatie: Voer automatisch meerdere taken parallel uit, waardoor je ketens sneller worden uitgevoerd en de gebruikerservaring wordt verbeterd.

  • Samenstelbaarheid: Hiermee kun je eenvoudig ketens samenstellen en wijzigen, waardoor je code flexibeler en aanpasbaarder wordt.

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.

class="img-responsive

Medium Blog door Artefact.

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