TL;DR

  • Schneller von POC zu Prod: In der Langchain-Dokumentation heißt es: "LCEL ist ein deklarativer Weg, um Ketten einfach zusammenzustellen. LCEL wurde vom ersten Tag an so konzipiert, dass Prototypen ohne Codeänderung in die Produktion überführt werden können".

  • Erstellung benutzerdefinierter Ketten: LCEL vereinfacht die Erstellung benutzerdefinierter Ketten durch eine neue Syntax.

  • Streaming und Batch sind sofort einsatzbereit: LCEL bietet Ihnen kostenlos Batch-, Streaming- und Async-Funktionen.

  • Einheitliche Schnittstelle: Sie bietet automatische Parallelisierung, Typisierungsfunktionen und alle zukünftigen Funktionen, die LangChain entwickeln könnte.

  • LCEL ist die Zukunft von LangChain: LCEL bietet eine neue Perspektive für die LLM-basierte Anwendungsentwicklung. Ich empfehle dringend, es für Ihr nächstes LLM-Projekt zu verwenden.

LangChain hat sich in weniger als einem Jahr zu einer der meistgenutzten Python-Bibliotheken für die Interaktion mit LLMs entwickelt, aber LangChain war hauptsächlich eine Bibliothek für POCs, da ihr die Fähigkeit fehlte, komplexe und skalierbare Anwendungen zu erstellen.
Das änderte sich im August 2023 mit der Veröffentlichung der LangChain Expression Language (LCEL), einer neuen Syntax, die die Lücke zwischen POC und Produktion schließt. Dieser Artikel führt Sie durch die Besonderheiten von LCEL und zeigt Ihnen, wie es die Erstellung von benutzerdefinierten Ketten vereinfacht und warum Sie es unbedingt lernen müssen, wenn Sie LLM-Anwendungen erstellen!

Prompts, LLM und Ketten, zur Auffrischung unseres Gedächtnisses

Bevor wir in die LCEL-Syntax eintauchen, sollten wir unser Gedächtnis über LangChain-Konzepte wie LLM und Prompt oder sogar eine Kette auffrischen.

LLM: In langchain, llm ist eine Abstraktion um das Modell verwendet, um die Vervollständigungen wie openai gpt3.5, Claude, etc.

Aufforderung: Dies ist die Eingabe des LLM-Objekts, das dem LLM Fragen stellt und seine Ziele angibt.

Kette: Dies bezieht sich auf eine Folge von Aufrufen eines LLM oder eines beliebigen data Verarbeitungsschritts.

class="img-responsive

Nun, da die Definitionen aus dem Weg geräumt sind, nehmen wir an, wir wollen ein Unternehmen gründen! Wir brauchen einen wirklich coolen und einprägsamen Namen und ein Geschäftsmodell, um Geld zu verdienen!

Beispiel - Firmenname & Geschäftsmodell mit alten Ketten

from langchain.chains import LLMChain
from langchain.prompts importiere PromptTemplate
from langchain_community.llms importieren OpenAI

USER_INPUT = "bunte Socken"
llm = OpenAI(Temperatur=0)

prompt_template_product = "Was ist ein guter Name für ein Unternehmen, das {Produkt} herstellt?"
firmen_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
firmenname_output = firmenname_chain(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(Firmenname_Output)
print(business_model_output)

>>> {‘product’: ‘colorful socks’, ‘text’: ‘Socktastic!’}
>>> {‘company’: ‘Socktastic!’,’text’: “A subscription-based service offering a monthly delivery…”}

Das ist recht einfach nachzuvollziehen, es gibt zwar ein wenig Redundanz, aber es ist überschaubar.

Fügen wir einige Anpassungen hinzu, indem wir die Fälle behandeln, in denen der Benutzer unsere Kette nicht wie erwartet verwendet.
Vielleicht gibt der Benutzer etwas ein, das nichts mit dem Ziel unserer Kette zu tun hat? In diesem Fall wollen wir dies erkennen und entsprechend reagieren.

Beispiel - Anpassung & Routing mit alten Ketten

from langchain.chains import LLMChain
from langchain.prompts importiere PromptTemplate
von langchain_community.llms importieren OpenAI
importieren ast

USER_INPUT = "Harrison Chase"
llm = OpenAI(Temperatur=0)

# -- Gleicher Code wie zuvor
prompt_template_product = "Was ist ein guter Name für ein Unternehmen, das {Produkt} herstellt?"
company_name_chain = 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))

# -- Neuer 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))
firmen_name_chain = 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))

# Wenn wir bool auf einen nicht leeren str anwenden, wird es True sein, also brauchen wir `literal_eval`.
is_a_product = ast.literal_eval(is_a_product_chain(USER_INPUT)["text"])
if is_a_product:
firmenname_ausgabe = firmenname_kette(BENUTZER_EINGABE)
business_model_output = business_model_chain(firmen_name_output["text"])
print(business_model_output)
sonst:
print(cannot_respond_chain(USER_INPUT))

Das wird etwas schwieriger zu verstehen, fassen wir es zusammen:

  • Wir haben eine neue Kette is_real_product_chain() erstellt, die erkennt, ob die Benutzereingabe als Produkt betrachtet werden kann.

  • Wir implementieren if/else-Bedingungen, um zwischen den Ketten zu verzweigen.

Es gibt eine Reihe von Problemen, die sich daraus ergeben:

  • Der Code ist ein wenig überflüssig, da es viele Standardformulierungen gibt.

  • Es ist schwer zu unterscheiden, welcher LLMChain mit welchem LLMChain verknüpft ist, wir müssen die Ein- und Ausgänge zurückverfolgen, um es zu verstehen.

  • Bei den Ausgabetypen der Ketten können leicht Fehler auftreten, z. B. ist die Ausgabe von is_a_product_chain() ein str, der später als bool ausgewertet werden sollte.

Was ist die LangChain Expression Language (LCEL)?

LCEL ist eine einheitliche Schnittstelle und Syntax zum Schreiben von zusammensetzbaren, produktionsbereiten Ketten. Es gibt viel zu entpacken, um zu verstehen, was das bedeutet.

Wir werden zunächst versuchen, die neue Syntax zu verstehen, indem wir die Kette von vorhin umschreiben.

Beispiel - Firmenname & Geschäftsmodell mit LCEL

from langchain_core.runnables import RunnablePassthrough
from langchain.prompts importieren PromptTemplate
from langchain_community.llms importieren OpenAI

USER_INPUT = "bunte Socken"
llm = OpenAI(Temperatur=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’})

Eine Menge ungewöhnlicher Code, in nur wenigen Zeilen:

  • Es gibt einen seltsamen | Operator zwischen einem PromptTemplate, einem llm und einem dictionnary?! Der | Operator ist einfach da, um zu sagen: "Nimm das Wörterbuch auf der linken Seite und gib es als Eingabe für das Objekt auf der rechten Seite ein".

  • Warum übergeben wir die Variable product in einem Dictionnary und nicht wie bisher in einer Zeichenkette? Wenn Sie #1 gelesen haben, wissen Sie, dass der |-Operator die Eingaben als Wörterbuch erwartet, daher geben wir das Produktargument product in einem Wörterbuch an.

  • Warum gibt es einen Funktionsnamen RunnablePassthrough() anstelle des Firmennamens? RunnablePassthrough() ist ein Platzhalter, um zu sagen: "Wir haben den Firmennamen noch nicht, aber wenn wir ihn haben, setzen wir ihn hier ein". Ich werde in den nächsten Abschnitten erklären, was der Begriff "Runnable" bedeutet, aber im Moment ist es in Ordnung, ihn zu ignorieren.

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

Aber ist es wirklich kompatibler, Ketten auf diese Weise zu erstellen?
Probieren wir es aus, indem wir die Funktion is_a_product_chain() und die Verzweigung hinzufügen, wenn die Benutzereingabe nicht korrekt ist. Wir können die Kette sogar mit Python Typing tippen, lassen Sie uns dies als gute Übung tun.

Beispiel - Anpassung & Routing mit LCEL

from typing import Dict
from langchain_core.runnables import RunnablePassthrough, RunnableBranch
from langchain.prompts importiere PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.output_parsers import BooleanOutputParser
from langchain_community.llms importieren OpenAI

USER_INPUT = "Harrrison Chase"
llm = OpenAI(Temperatur=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)

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({‘product’: USER_INPUT}))

Wir wollen die Unterschiede auflisten:

  • Die Syntax ist anders, das ist klar.

  • Es werden Zwischenketten definiert und in einer größeren Kette aufgerufen, fast wie Funktionen.

  • Eingänge und Ausgänge sind typisiert, fast wie Funktionen.

  • Das fühlt sich nicht wie Python an.

Warum ist LCEL besser für die Industrialisierung?

Wenn ich diesen Artikel bis genau zu diesem Punkt lesen würde und mich jemand fragen würde, ob ich von LCEL überzeugt bin, würde ich wahrscheinlich nein sagen. Die Syntax ist zu unterschiedlich, und ich kann meinen Code wahrscheinlich in Funktionen organisieren, um fast genau den gleichen Code zu erhalten. Aber ich bin hier und schreibe diesen Artikel, also muss da noch etwas anderes sein.

Sofortiger Aufruf, Stream und Batch

Durch die Verwendung von LCEL hat Ihre Kette automatisch:

  • .invoke(): Sie wollen Ihre Eingabe übergeben und die Ausgabe erhalten, nicht mehr und nicht weniger.

  • .batch(): Wenn Sie mehrere Eingaben übergeben möchten, um mehrere Ausgaben zu erhalten, wird die Parallelisierung für Sie übernommen (schneller als der 3-malige Aufruf von invoke).

  • .stream(): Damit können Sie den Anfang des Abschlusses ausdrucken, bevor der gesamte Abschluss beendet ist.

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="")

Wenn Sie iterieren, können Sie die invoke-Methode verwenden, um den Entwicklungsprozess zu vereinfachen. Wenn Sie jedoch die Ausgabe Ihrer Kette in einer Benutzeroberfläche anzeigen, möchten Sie die Antwort streamen. Sie können jetzt die Stream-Methode verwenden, ohne etwas umzuschreiben.

Standardmäßige asynchrone Methoden

In den meisten Fällen werden Frontend und Backend Ihrer Anwendung getrennt sein, d.h. das Frontend stellt eine Anfrage an das Backend. Wenn Sie mehrere Benutzer haben, müssen Sie möglicherweise mehrere Anfragen an Ihr Backend gleichzeitig bearbeiten.

Da der meiste Code in LangChain nur zwischen den API-Aufrufen wartet, können wir asynchronen Code nutzen, um die Skalierbarkeit der API zu verbessern. Wenn Sie verstehen wollen, warum das wichtig ist, empfehle ich Ihnen, die Concurrent-Burger-Geschichte in der FastAPI-Dokumentation zu lesen.
Um die Implementierung brauchen Sie sich nicht zu kümmern, denn asynchrone Methoden sind bereits verfügbar, wenn Sie LCEL verwenden:

.ainvoke() / .abatch() / .astream: asynchrone Versionen von invoke, batch und stream.

Ich empfehle auch das Lesen der Seite Why use LCEL aus der LangChain-Dokumentation mit Beispielen für jede sync / async Methode.

Langchain hat diese "out of the box"-Funktionen durch die Schaffung einer einheitlichen Schnittstelle namens "Runnable" erreicht . Um LCEL in vollem Umfang nutzen zu können, müssen wir uns nun mit dieser neuen Runnable-Schnittstelle befassen.

Die Schnittstelle Runnable

Alle Objekte, die wir bisher in der LCEL-Syntax verwendet haben, sind Runnables. Es handelt sich um ein Python-Objekt, das von LangChain erstellt wird. Dieses Objekt erbt automatisch jede Funktion, über die wir zuvor gesprochen haben, und noch viel mehr. Durch die Verwendung der LCEL-Syntax wird bei jedem Schritt ein neues Runnable erstellt, was bedeutet, dass das endgültige Objekt ebenfalls ein Runnable ist. Weitere Informationen über die Schnittstelle finden Sie in der offiziellen Dokumentation.

Alle Objekte des folgenden Codes sind entweder Runnable oder Dictionaries, die automatisch in Runnable umgewandelt werden:

from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain.prompts importiere PromptTemplate
from langchain_community.llms importieren 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(ketten_nummer_eins == ketten_nummer_zwei)
>>> Wahr

Warum verwenden wir RunnableParallel() und nicht einfach Runnable()?

Denn jede Runnable innerhalb einer RunnableParallel wird parallel ausgeführt. Das heißt, wenn Sie 3 unabhängige Schritte in Ihrer Runnable haben, werden diese gleichzeitig auf verschiedenen Threads Ihrer Maschine ausgeführt, was die Geschwindigkeit Ihrer Kette kostenlos erhöht!

Nachteile von LCEL

Trotz seiner Vorteile hat LCEL auch einige potenzielle Nachteile:

  • Nicht vollständig PEP-konform: LCEL hält sich nicht vollständig an PEP20, das Zen von Python, das besagt, dass "explizit besser ist als implizit". (Um PEP20 zu überprüfen, können Sie import this in Python ausführen.) Außerdem wird die Syntax von LCEL nicht als "pythonisch" angesehen, da sie sich wie eine andere Sprache anfühlt, was dazu führen könnte, dass LCEL für einige Python-Entwickler weniger intuitiv ist und sie sich weigern könnten, es zu verwenden.

  • LCEL ist eine bereichsspezifische Sprache (DSL): Von den Benutzern wird erwartet, dass sie über ein gewisses Verständnis von Prompts, Chains oder LLMs verfügen, um die Syntax effizient nutzen zu können.

  • Input/Output-Abhängigkeiten: Zwischeneingaben und Endausgaben müssen vom Anfang bis zum Ende weitergereicht werden. Wenn Sie z. B. die Ausgabe eines Zwischenschritts als Endausgabe verwenden wollen, müssen Sie sie durch alle nachfolgenden Schritte tragen. Dies kann zu zusätzlichen Argumenten in den meisten Ihrer Ketten führen, die vielleicht nicht verwendet werden, aber notwendig sind, wenn Sie auf sie in der Ausgabe zugreifen wollen.

Schlussfolgerung

Zusammenfassend lässt sich sagen, dass die LangChain Expression Language (LCEL) ein leistungsfähiges Werkzeug ist, das der Entwicklung von Python-Anwendungen eine neue Perspektive verleiht. Trotz seiner unkonventionellen Syntax kann ich die Verwendung von LCEL aus folgenden Gründen nur empfehlen:

  • Einheitliche Schnittstelle: Bietet eine einheitliche Schnittstelle für alle Chains und erleichtert die Industrialisierung Ihres Codes mit Out-of-the-Box-Stream, Async, Fallback-Modellen, Typisierung, Laufzeitkonfigurationen usw..

  • Automatische Parallelisierung: Führen Sie automatisch mehrere Aufgaben parallel aus, um die Ausführungsgeschwindigkeit Ihrer Ketten zu erhöhen und die Benutzerfreundlichkeit zu verbessern.

  • Kompatibilität: Sie können Ketten leicht zusammenstellen und ändern, wodurch Ihr Code flexibler und anpassungsfähiger wird.

Weiter gehen...

Die lauffähige Abstraktion

In einigen Fällen halte ich es für wichtig, die Abstraktion zu verstehen, die LangChain implementiert hat, damit die LCEL-Syntax funktioniert.

Sie können die grundlegenden Funktionen von Runnable wie folgt einfach neu implementieren:

class Runnable:
def __init__(self, func):
self.func = func

def __or__(self, other):
def chained_func(*args, **kwargs):
# self.func ist links, other ist 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)
Kette = runnable_add_ten | runnable_divide_by_two
result = chain(8) # (8+10) / 2 = 9.0 sollte die Antwort sein
print(ergebnis)
>>> 9.0

Ein Runnable ist einfach ein Python-Objekt, in dem die Methode .__or__() überschrieben wurde.
In der Praxis hat LangChain viele Funktionalitäten hinzugefügt, wie z.B. die Konvertierung von Dictionaries in Runnable, Typisierungsfähigkeiten, Konfigurationsfähigkeiten und Invoke-, Batch-, Stream- und Async-Methoden!

Warum probieren Sie LCEL also nicht bei Ihrem nächsten Projekt aus?

Wenn Sie mehr darüber erfahren möchten, empfehle ich Ihnen, das LangChain-Kochbuch auf LCEL zu lesen.

class="img-responsive

Medium Blog von Artefact.

Dieser Artikel wurde ursprünglich auf Medium.com veröffentlicht.
Folgen Sie uns auf unserem Medium Blog!