TL;DR

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

  • Ketting op maat 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.

  • Unified interface: Het biedt automatische parallellisatie, typmogelijkheden en elke toekomstige functie 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 u ten zeerste aan om het te gebruiken voor uw volgende LLM-project.

LangChain is een van de meest gebruikte Python-bibliotheek om te communiceren met LLM's in minder dan een jaar, maar LangChain was vooral een bibliotheek voor POC's omdat het niet de mogelijkheid had om complexe en schaalbare toepassingen.
Alles veranderde in augustus 2023 toen ze LangChain uitdrukkingstaal (LCEL), een nieuwe syntaxis die de kloof overbrugt tussen POC naar productie. Dit artikel leidt u door de ins en outs van LCEL en laat u zien hoe het de creëren van aangepaste kettingen en waarom u het moet leren als u bouwt aan LLM aanvragen!

Prompts, LLM en kettingen, laten we ons geheugen opfrissen

Voordat we in de LCEL syntax duiken, denk ik dat het nuttig 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, enz...

Prompt: Dit is de invoer van het LLM object, dat de LLM vragen zal stellen en de doelstellingen zal geven.

Ketting: Dit verwijst naar een opeenvolging van oproepen naar een LLM, of een willekeurige data verwerkingsstap.

Nu de definities uit de weg zijn, stel dat we een bedrijf willen oprichten! We hebben een coole en pakkende naam nodig en een bedrijfsmodel om geld te verdienen!

Voorbeeld - Bedrijfsnaam & Businessmodel met oude ketens

uit langchain.chains importeert LLMChain
uit 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 bedrijf dat ?" maakt."
bedrijfsnaam_keten = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
bedrijfsnaam_uitvoer = bedrijfsnaam_keten(USER_INPUT)

prompt_template_business = "Geef me het beste idee voor een bedrijfsmodel voor mijn bedrijf genaamd: "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
business_model_output = business_model_chain(company_name_output["text"])

print(bedrijfsnaam_uitvoer)
print(business_model_output)

>>>
>>>

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 ketting 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 dat detecteren en op de juiste manier reageren.

Voorbeeld - Aanpassing & Routing met oude ketens

uit langchain.chains importeert LLMChain
uit langchain.prompts importeer PromptTemplate
van langchain_community.llms importeer OpenAI
import ast

USER_INPUT = "Harrison Chase"."
llm = OpenAI(temperatuur=0)

# -- Dezelfde code als hierboven
prompt_template_product = "Wat is een goede naam voor een bedrijf dat ?" maakt."
bedrijfsnaam_keten = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))

prompt_template_business = "Geef me het beste idee voor een bedrijfsmodel voor mijn bedrijf genaamd: "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))

# -- Nieuwe code

prompt_template_is_product = (
"Uw doel is om uit te zoeken of de invoer van de gebruiker een plausibele productnaam is"
"Vragen, begroetingen, lange zinnen, beroemdheden of andere niet-relevante inputs worden niet als producten beschouwdn"
"invoer: n"
"Antwoord alleen met 'Waar' of 'Onwaar' en niets meer."
)

prompt_template_cannot_respond = (
"U kunt niet reageren op de gebruikersinvoer: n"
"Vraag de gebruiker om de naam van een product in te voeren zodat u er een bedrijf van kunt maken.n"
)

cannot_respond_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_cannot_respond))
bedrijfsnaam_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"])
if_a_product:
bedrijfsnaam_uitvoer = bedrijfsnaam_keten(USER_INPUT)
business_model_output = business_model_chain(company_name_output["text"])
print(business_model_output)
anders:
print(cannot_respond_chain(USER_INPUT))

Dit wordt iets 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.

  • Wij implementeren if/else-voorwaarden om tussen de ketens te schakelen.

Er beginnen zich meerdere problemen voor te doen:

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

  • Het is moeilijk om te onderscheiden welke LLMChain aan welke LLMChain is gekoppeld, we moeten de in- en uitgangen 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 syntaxis om samenstelbare productieklare ketens te schrijven, er moet veel uitgepakt worden om te begrijpen wat het betekent.

We zullen eerst proberen de nieuwe syntaxis te begrijpen door de ketting van eerder te herschrijven.

Voorbeeld - Bedrijfsnaam & Bedrijfsmodel met LCEL

uit langchain_core.runnables importeer RunnablePassthrough
uit 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 bedrijf dat ?" maakt."
prompt_template_business = "Geef me het beste idee voor een bedrijfsmodel voor mijn bedrijf genaamd: "

ketting = (
PromptTemplate.from_template(prompt_template_product)
| llm
|
| PromptTemplate.from_template(prompt_template_business)
| llm
)

business_model_output = keten.invoke()

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 de woordenlijst links en geef deze door als invoer van het object rechts”.

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

  • Waarom is er een functienaam RunnablePassthrough() in plaats van de bedrijfsnaam? De RunnablePassthrough() is een placeholder om te zeggen: “we hebben de bedrijfsnaam nu nog niet, maar als we hem 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.

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

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() en de vertakking toe te voegen als de invoer van de gebruiker niet correct is. We kunnen zelfs de keten 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 importeert BooleanOutputParser
van langchain_community.llms importeer OpenAI

USER_INPUT = "Harrrison Chase"."
llm = OpenAI(temperatuur=0)

prompt_template_product = "Wat is een goede naam voor een bedrijf dat ?" maakt."
prompt_template_cannot_respond = (
"U kunt niet reageren op de gebruikersinvoer: n"
"Vraag de gebruiker om de naam van een product in te voeren zodat u er een bedrijf van kunt maken.n"
)
prompt_template_business = "Geef me het beste idee voor een bedrijfsmodel voor mijn bedrijf genaamd: "
prompt_template_is_product = (
"Uw doel is om uit te zoeken of de invoer van de gebruiker een plausibele productnaam is"
"Vragen, begroetingen, lange zinnen, beroemdheden of andere niet-relevante inputs worden niet als producten beschouwdn"
"invoer: n"
"Antwoord alleen met 'Waar' of 'Onwaar' en niets meer."
)

antwoord_gebruiker_keten = (
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)

volledige_keten = RunnableBranch(
(is_product_chain, answer_user_chain),
kan_niet_reageren_keten
).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 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 mij vragen of ik overtuigd was van LCEL, dan zou ik waarschijnlijk nee zeggen. De syntaxis is te verschillend, 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 uw ketting automatisch:

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

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

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

mijn_keten = prompt | llm

# ---oproepen-- #
resultaat_met_invoke = mijn_keten.invoke(“hallo wereld!”)

# ---partij-- #
resultaat_met_batch = mijn_keten.batch([“hallo”, “wereld”, “!”])

# ---stroom-- #
voor chunk in my_chain.stream(“hello world!”):
print(chunk, flush=True, end=””)

Wanneer u itereert, kunt u de invoke methode gebruiken om het ontwikkelingsproces te vergemakkelijken. Maar wanneer u de uitvoer van uw keten in een UI toont, wilt u het antwoord streamen. U kunt nu de stream-methode gebruiken zonder iets te herschrijven.

Out of the box async methoden

Meestal zullen de frontend en backend van uw applicatie gescheiden zijn, wat betekent dat de frontend een verzoek doet aan de backend. Als u meerdere gebruikers hebt, moet u misschien meerdere verzoeken tegelijkertijd op uw backend afhandelen.

Aangezien het grootste deel van de code in LangChain gewoon wacht tussen API-aanroepen, kunnen we asynchrone code gebruiken om de schaalbaarheid van de API te verbeteren. Als u wilt begrijpen waarom dit belangrijk is, raad ik u aan om de gelijktijdig hamburgers verhaal van de FastAPI documentatie.
U hoeft zich geen zorgen te maken over de implementatie, omdat async methoden al beschikbaar zijn als u LCEL gebruikt:

.ainvoke() / .abatch() / .astreamAsynchrone versies van invoke, batch en stream.

Ik raad ook aan om de Waarom LCEL pagina uit LangChain documentatie gebruiken met voorbeelden voor elke sync / async methode.

Langchain bereikte deze “out of the box” functies door het creëren van een uniforme interface genaamd “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 zijn Runnables. Het is een python-object dat wordt gemaakt door LangChain, dit object erft automatisch elke functie 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 uiteindelijk gemaakte object ook een Runnable zal zijn. U kunt meer te weten komen 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
uit langchain.prompts importeer PromptTemplate
van langchain_community.llms importeer OpenAI

chain_number_one = (
PromptTemplate.from_template(prompt_template_product)
| llm
| # <- DIT ZAL VERANDEREN
| PromptTemplate.from_template(prompt_template_business)
| llm
)

keten_nummer_twee = (
PromptTemplate.from_template(prompt_template_product)
| llm
| RunnableParallel(company=RunnablePassthrough()) # <- DIT VERANDERDE
| 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 binnen een RunnableParallel parallel wordt uitgevoerd. Dit betekent dat als u 3 onafhankelijke stappen in uw Runnable hebt, deze tegelijkertijd op verschillende threads van uw machine worden uitgevoerd, waardoor de snelheid van uw keten gratis wordt verbeterd!

Nadelen van LCEL

Ondanks de voordelen heeft LCEL ook enkele potentiële nadelen:

  • Niet volledig PEP-conform: LCEL respecteert PEP20, de Zen van Python, niet volledig, waarin staat dat “expliciet beter is dan impliciet”. (Om PEP20 te controleren kunt u dit importeren in Python.) Daarnaast wordt de syntaxis van LCEL 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 syntaxis efficiënt te kunnen gebruiken.

  • Invoer-/uitvoerafhankelijkheden: Tussenliggende inputs en finale outputs moeten van het begin naar het einde worden doorgegeven. Als u bijvoorbeeld de uitvoer van een tussenstap als de uiteindelijke uitvoer wilt gebruiken, moet u deze door alle volgende stappen meenemen. Dit kan leiden tot extra argumenten in de meeste van uw ketens, die misschien niet gebruikt worden, maar wel nodig zijn als u ze door de uitvoer wilt halen.

Conclusie

Concluderend kan ik zeggen dat LangChain Expression Language (LCEL) een krachtig hulpmiddel is dat een nieuw perspectief biedt voor het bouwen van Python toepassingen. Ondanks de onconventionele syntaxis, raad ik het gebruik van LCEL om de volgende redenen ten zeerste aan:

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

  • Automatisch parallelliseren: Voer automatisch meerdere taken parallel uit, waardoor uw ketens sneller worden uitgevoerd en de gebruikerservaring wordt verbeterd.

  • Samenstelbaarheid: Hiermee kunt u eenvoudig ketens samenstellen en wijzigen, waardoor uw 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 syntaxis te laten werken.

U kunt de basisfunctionaliteiten van Runnable eenvoudig als volgt opnieuw implementeren:

klasse Runnable:
def __init__(self, func):
zelf.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):
keer x + 10 terug

def deel_bij_twee(x):
keer x / 2 terug

runnable_add_ten = Runnable(add_ten)
runnable_divide_by_two = Runnable(divide_by_two)
keten = runnable_add_ten | runnable_divide_by_two
resultaat = ketting(8) # (8+10) / 2 = 9,0 zou het antwoord moeten zijn
print(resultaat)
>>> 9.0

Een Runnable is eenvoudigweg 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 zou u LCEL niet eens proberen in uw volgende project?

Als u meer wilt weten, raad ik u ten zeerste aan om eens rond te kijken LangChain kookboek op LCEL.

Medium Blog bij Artefact.

Dit artikel werd oorspronkelijk gepubliceerd op Medium.com.
Volg ons op ons medium Blog !