TL;DR

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

  • Individuelle Kettenerstellung: LCEL vereinfacht die Erstellung benutzerdefinierter Ketten mit einer neuen Syntax.

  • Standardmäßiges Streaming und Batch: LCEL bietet Ihnen kostenlos Batch-, Streaming- und Async-Funktionen.

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

  • LCEL ist die Zukunft der LangChain: LCEL bietet eine neue Perspektive für die LLM-basierte Anwendungsentwicklung. Ich empfehle es sehr für Ihr nächstes LLM-Projekt.

LangChain hat sich zu einem der meistgenutzten Python-Bibliothek zur Interaktion mit LLMs in weniger als einem Jahr, aber LangChain war hauptsächlich eine Bibliothek für POCs da ihm die Fähigkeit fehlte, eine komplexe und skalierbare Anwendungen.
Alles änderte sich im August 2023, als sie die LangChain-Ausdruckssprache (LCEL), eine neue Syntax, die die Lücke zwischen POC zur Produktion. Dieser Artikel führt Sie durch die Besonderheiten von LCEL und zeigt Ihnen, wie es die Erstellung von benutzerdefinierten Ketten und warum Sie es lernen müssen, wenn Sie bauen LLM-Bewerbungen!

Prompts, LLM und Ketten, lassen Sie uns unser Gedächtnis auffrischen

Bevor wir in die LCEL-Syntax eintauchen, sollten wir unsere Erinnerung an LangChain-Konzepte wie LLM und Prompt oder sogar eine Kette auffrischen.

LLM: In langchain ist llm eine Abstraktion des Modells, das zur Erstellung von Vervollständigungen wie openai gpt3.5, claude, etc. verwendet wird.

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

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

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 import PromptTemplate
from langchain_community.llms import OpenAI

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

prompt_template_product = "Was ist ein guter Name für ein Unternehmen, das ?"
firmen_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
firmenname_ausgabe = firmenname_kette(BENUTZER_EINGABE)

prompt_template_business = "Nennen Sie mir die beste Geschäftsmodellidee für mein Unternehmen namens: "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
business_model_output = business_model_chain(unternehmen_name_output["text"])

print(Firmenname_Ausgabe)
print(business_model_output)

>>>
>>>

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

Lassen Sie uns einige Anpassungen vornehmen, um die Fälle zu 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 möchten wir dies erkennen und entsprechend reagieren.

Beispiel - Anpassung & Routing mit alten Ketten

from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_community.llms import 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 ?"
firmen_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))

prompt_template_business = "Nennen Sie mir die beste Geschäftsmodellidee für mein Unternehmen namens: "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))

# -- Neuer Code

prompt_template_is_product = (
"Ihr Ziel ist es, herauszufinden, ob die Eingabe des Benutzers ein plausibler Produktname ist"
"Fragen, Begrüßungen, lange Sätze, Prominente oder andere nicht relevante Eingaben werden nicht als Produkte angesehen."
"Eingabe: n"
"Antworten Sie nur mit 'Richtig' oder 'Falsch' und nichts weiter"
)

prompt_template_cannot_respond = (
"Sie können nicht auf die Benutzereingabe reagieren: n"
"Fordern Sie den Benutzer auf, den Namen eines Produkts einzugeben, damit Sie daraus ein Unternehmen machen können."
)

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 er 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(unternehmen_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 Eingänge und Ausgänge zurückverfolgen, um sie zu verstehen.

  • Wir können bei den Ausgabetypen der Ketten leicht Fehler machen. Die Ausgabe von is_a_product_chain() ist beispielsweise ein str, der später als bool ausgewertet werden sollte.

Was ist die LangChain Expression Language (LCEL)?

LCEL ist eine einheitliche Schnittstelle und Syntax, um zusammensetzbare produktionsbereite Ketten zu schreiben. 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 neu schreiben.

Beispiel - Firmenname & Geschäftsmodell mit LCEL

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

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

prompt_template_product = "Was ist ein guter Name für ein Unternehmen, das ?"
prompt_template_business = "Nennen Sie mir die beste Geschäftsmodellidee für mein Unternehmen namens: "

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

business_model_output = chain.invoke()

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 dazu da, um zu sagen: “Nehmen Sie das Wörterbuch auf der linken Seite und übergeben Sie es als Eingabe für das Objekt auf der rechten Seite”.

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

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

  • Warum brauchen wir eine spezielle Methode .invoke(), anstatt chain() zu schreiben?Das werden wir im nächsten Teil verstehen, aber es ist ein kleiner Vorgeschmack darauf, warum LCEL die Industrialisierung einfacher macht!

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 eintippen, lassen Sie uns das als gute Übung tun.

Beispiel - Anpassung & Routing mit 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(Temperatur=0)

prompt_template_product = "Was ist ein guter Name für ein Unternehmen, das ?"
prompt_template_cannot_respond = (
"Sie können nicht auf die Benutzereingabe reagieren: n"
"Fordern Sie den Benutzer auf, den Namen eines Produkts einzugeben, damit Sie daraus ein Unternehmen machen können."
)
prompt_template_business = "Nennen Sie mir die beste Geschäftsmodellidee für mein Unternehmen namens: "
prompt_template_is_product = (
"Ihr Ziel ist es, herauszufinden, ob die Eingabe des Benutzers ein plausibler Produktname ist"
"Fragen, Begrüßungen, lange Sätze, Prominente oder andere nicht relevante Eingaben werden nicht als Produkte angesehen."
"Eingabe: n"
"Antworten Sie nur mit 'Richtig' oder 'Falsch' und nichts weiter"
)

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),
kann_nicht_antworten_Kette
).with_types(input_type=Dict[str, str], output_type=str)

print(full_chain.invoke())

Lassen Sie uns die Unterschiede auflisten:

  • Die Syntax ist anders, das versteht sich von selbst.

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

  • Eingaben und Ausgaben 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 mehr 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 mit dem Druck des Anfangs der Fertigstellung beginnen, bevor die gesamte Fertigstellung abgeschlossen ist.

my_chain = prompt | llm

# ---invoke--- #
result_with_invoke = my_chain.invoke(“Hallo Welt!”)

# ---batch--- #
result_with_batch = my_chain.batch([“hallo”, “welt”, “!”])

# ---strom--- #
for chunk in my_chain.stream(“Hallo Welt!”):
print(chunk, flush=True, end=””)

Wenn Sie iterieren, können Sie die Methode invoke verwenden, um den Entwicklungsprozess zu vereinfachen. Aber wenn Sie 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.

Sofort einsatzbereite asynchrone Methoden

In den meisten Fällen werden Frontend und Backend Ihrer Anwendung getrennt sein, d.h. das Frontend wird eine Anfrage an das Backend stellen. 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 Lektüre des gleichzeitige Burger-Geschichte der FastAPI-Dokumentation.
Sie brauchen sich nicht um die Implementierung 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 die Lektüre der Warum die LCEL-Seite aus der LangChain-Dokumentation verwenden? mit Beispielen für jede sync / async Methode.

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

Die Schnittstelle Runnable

Alle Objekte, die wir bisher in der LCEL-Syntax verwendet haben, sind Runnables. Dabei handelt es 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 komponieren wir bei jedem Schritt ein neues Runnable, was bedeutet, dass das letztendlich erstellte Objekt ebenfalls ein Runnable ist. Mehr über die Schnittstelle erfahren Sie in der offiziellen Dokumentation.

Alle Objekte aus dem folgenden Code sind entweder Runnable oder Dictionaries, die automatisch in ein Runnable konvertiert werden:

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
| # <- DAS WIRD SICH ÄNDERN
| PromptTemplate.from_template(prompt_template_business)
| llm
)

chain_number_two = (
PromptTemplate.from_template(prompt_template_product)
| llm
| RunnableParallel(Firma=RunnablePassthrough()) # <- DIES HAT SICH GEÄNDERT
| 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 Ihres Rechners 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. Dies könnte LCEL für einige Python-Entwickler weniger intuitiv machen, so dass sie sich weigern könnten, es zu verwenden.

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

  • Input/Output-Abhängigkeiten: Zwischeninputs und Endoutputs müssen vom Anfang bis zum Ende weitergereicht werden. Wenn Sie z.B. die Ausgabe eines Zwischenschritts als Endausgabe verwenden möchten, 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 durch die Ausgabe zugreifen wollen.

Fazit

Zusammenfassend lässt sich sagen, dass die LangChain Expression Language (LCEL) ein leistungsfähiges Tool 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:

  • Vereinheitlichte Schnittstelle: Bietet eine konsistente Schnittstelle für alle Chains, die die Industrialisierung Ihres Codes mit Out-of-the-Box-Streams, Asynchronität, Fallback-Modellen, Typisierung, Laufzeitkonfigurationen usw. erleichtert.

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

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

Um weiter zu gehen...

Die ablauffä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 ganz einfach wie folgt neu implementieren:

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

def __or__(self, other):
def chained_func(*args, **kwargs):
# self.func ist auf der linken Seite, andere ist auf der rechten Seite
return other(self.func(*args, **kwargs))
return Runnable(verkettete_Funktion)

def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)

def add_ten(x):
Rückgabe x + 10

def divide_by_two(x):
Rückgabe x / 2

runnable_add_ten = Runnable(add_ten)
runnable_divide_by_two = Runnable(divide_by_two)
Kette = runnable_add_ten | runnable_divide_by_two
Ergebnis = Kette(8) # (8+10) / 2 = 9.0 sollte die Antwort sein
print(Ergebnis)
>>> 9.0

Ein Runnable ist einfach ein Python-Objekt, bei dem die Methode .__or__() überschrieben wurde.
In der Praxis hat LangChain viele Funktionen hinzugefügt, wie z.B. die Konvertierung von Dictionaries in Runnable, Typisierungsmöglichkeiten, Konfigurierbarkeit sowie Invoke-, Batch-, Stream- und Async-Methoden!

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

Wenn Sie mehr erfahren möchten, empfehle ich Ihnen einen Blick auf LangChain Kochbuch auf LCEL.

Mittel Blog von Artefact.

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