Kurz gesagt

  • Schnellerer Übergang vom Proof-of-Concept zur Produktion: Wie in der Langchain-Dokumentation beschrieben, ist „LCEL eine deklarative Methode, um Chains auf einfache Weise miteinander zu verknüpfen. LCEL wurde von Anfang an so konzipiert, dass Prototypen ohne Codeänderungen in die Produktion übernommen werden können“.

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

  • Streaming und Stapelverarbeitung von Haus aus: LCEL bietet Ihnen kostenlos Funktionen für Stapelverarbeitung, Streaming und asynchrone Verarbeitung.

  • Einheitliche Schnittstelle: Sie bietet automatische Parallelisierung, Typisierungsfunktionen sowie alle zukünftigen Funktionen, die LangChain möglicherweise entwickeln wird.

  • LCEL ist die Zukunft von LangChain: LCEL eröffnet neue Perspektiven für die Entwicklung von LLM-basierten Anwendungen. Ich kann Ihnen nur wärmstens empfehlen, es für Ihr nächstes LLM-Projekt zu nutzen.

LangChain hat sich in weniger als einem Jahr zu einer der meistgenutzten Python-Bibliotheken für die Interaktion mit LLMs entwickelt, doch LangChain diente bislang vor allem als Bibliothek für Proof-of-Concepts (POCs), da es an der Fähigkeit mangelte, komplexe und skalierbare Anwendungen zu erstellen.
Das änderte sich im August 2023, als LangChain Expression Language (LCEL) veröffentlicht wurde – eine neue Syntax, die die Lücke zwischen Proof-of-Concept und Produktion schließt. Dieser Artikel führt Sie durch die Besonderheiten von LCEL und zeigt Ihnen, wie es die Erstellung benutzerdefinierter Ketten vereinfacht und warum Sie es lernen müssen, wenn Sie LLM-Anwendungen entwickeln!

Prompts, LLM und Chains – frischen wir unser Gedächtnis auf

Bevor wir uns mit der LCEL-Syntax befassen, halte ich es für sinnvoll, unser Wissen über LangChain-Konzepte wie LLM, Prompt oder auch die Chain aufzufrischen.

LLM: In Langchain ist ein LLM eine Abstraktion des Modells, das für die Textvervollständigung verwendet wird, wie beispielsweise OpenAI GPT-3.5, Claude usw.

Eingabeaufforderung: Dies ist die Eingabe für das LLM-Objekt, das dem LLM Fragen stellt und dessen Ziele vorgibt.

Kette: Damit ist eine Abfolge von Aufrufen eines LLM oder beliebiger data gemeint.

class="lazyload

Nachdem wir nun die Definitionen geklärt haben, nehmen wir einmal an, wir wollen ein Unternehmen gründen! Wir brauchen einen richtig coolen und einprägsamen Namen und ein Geschäftsmodell, um Geld zu verdienen!

Beispiel – Firmenname und Geschäftsmodell mit „Old Chains“

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

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

prompt_template_product = "Wie lautet ein guter Name für ein Unternehmen, das ... herstellt?"
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
company_name_output = company_name_chain(USER_INPUT)

prompt_template_business = "Gib mir die beste Idee für ein Geschäftsmodell für mein Unternehmen mit dem Namen: "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
business_model_output = business_model_chain(company_name_output["text"])

print(company_name_output)
print(business_model_output)

>>>
>>>

Das ist recht leicht nachzuvollziehen; es gibt zwar ein paar Wiederholungen, aber das ist zu verkraften.

Lassen Sie uns einige Anpassungen vornehmen, indem wir Fälle behandeln, in denen der Benutzer unsere Kette nicht wie erwartet nutzt.
Vielleicht gibt der Benutzer etwas ein, das überhaupt nichts mit dem Ziel unserer Kette zu tun hat? In diesem Fall möchten wir dies erkennen und entsprechend reagieren.

Beispiel – Anpassung und Weiterleitung mit alten Ketten

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)

# —- Gleicher Code wie zuvor
prompt_template_product = "Wie lautet ein guter Name für ein Unternehmen, das ? herstellt?"
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))

prompt_template_business = "Gib mir die beste Idee für ein Geschäftsmodell für mein Unternehmen mit dem Namen: "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))

# —- Neuer Code

prompt_template_is_product = (
"Deine Aufgabe ist es, festzustellen, ob die Eingabe des Benutzers ein plausibler Produktname ist"
"Fragen, Begrüßungen, lange Sätze, Prominente oder andere irrelevante Eingaben gelten nicht als Produktnamen"
"Eingabe: n"
"Antworte nur mit 'True' oder 'False' und nichts weiter"
)

prompt_template_cannot_respond = (
"Du kannst auf die Benutzereingabe nicht reagieren: n"
"Bitte den Benutzer, den Namen eines Produkts einzugeben, damit du daraus ein Unternehmen bilden kannst.n"
)

cannot_respond_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_cannot_respond))
company_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 eine nicht leere Zeichenkette anwenden, ergibt dies „True“; daher benötigen wir „literal_eval“
is_a_product = ast.literal_eval(is_a_product_chain(USER_INPUT)["text"])
if is_a_product:
company_name_output = company_name_chain(USER_INPUT)
business_model_output = business_model_chain(company_name_output["text"])
print(business_model_output)
else:
print(cannot_respond_chain(USER_INPUT))

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

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

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

Es treten nach und nach mehrere Probleme auf:

  • Der Code ist etwas überflüssig, da er viele Standardformulierungen enthält.

  • Es ist schwer zu erkennen, welche LLMChain mit welcher LLMChain verknüpft ist; wir müssen die Ein- und Ausgänge nachverfolgen, um das zu verstehen.

  • Es kann leicht zu Fehlern bei den Rückgabetypen der Ketten kommen; so ist beispielsweise die Rückgabe von `is_a_product_chain()` ein String, der später als Boolwert ausgewertet werden sollte.

Was ist die LangChain Expression Language (LCEL)?

LCEL ist eine einheitliche Schnittstelle und Syntax zum Erstellen von kombinierbaren, produktionsreifen Ketten; es gibt viel zu erklären, 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 und 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(temperature=0)

prompt_template_product = "Was wäre ein guter Name für ein Unternehmen, das … herstellt?"
prompt_template_business = "Schlag mir die beste Geschäftsmodellidee für mein Unternehmen mit dem Namen … vor:"

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

business_model_output = chain.invoke()

Viel ungewöhnlicher Code, in nur wenigen Zeilen:

  • Zwischen einem PromptTemplate, einem LLM und einem Wörterbuch steht ein seltsamer |-Operator?! Der |-Operator dient lediglich dazu, zu sagen: „Nimm das Wörterbuch auf der linken Seite und übergebe es als Eingabe an das Objekt auf der rechten Seite“.

  • Warum übergeben wir die Variable „product“ innerhalb eines Wörterbuchs und nicht wie zuvor als Zeichenkette? Wenn du Punkt 1 gelesen hast, weißt du, dass der Operator „|“ die Eingaben als Wörterbuch erwartet; daher übergeben wir das Argument „product“ innerhalb eines Wörterbuchs.

  • Warum lautet der Funktionsname „RunnablePassthrough()“ statt des Firmennamens? „RunnablePassthrough()“ ist ein Platzhalter, der besagt: „Wir haben den Firmennamen derzeit noch nicht, aber sobald wir ihn haben, wird er hier eingefügt.“ Was der Begriff „Runnable“ bedeutet, werde ich in den nächsten Abschnitten erläutern; vorerst können Sie ihn getrost ignorieren.

  • Warum brauchen wir eine spezielle Methode .invoke() anstelle von chain()?Das werden wir im nächsten Teil verstehen, aber hier schon mal ein kleiner Vorgeschmack darauf, warum LCEL die Industrialisierung vereinfacht!

Aber lässt sich auf diese Weise wirklich eine Kette besser zusammenstellen?
Probieren wir es aus, indem wir die Funktion `is_a_product_chain()` hinzufügen und eine Verzweigung einbauen, falls die Benutzereingabe nicht korrekt ist. Wir können die Kette sogar mit Python Typing typisieren – machen wir das als bewährte Vorgehensweise.

Beispiel – Anpassung und Weiterleitung 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(temperature=0)

prompt_template_product = "Wie lautet ein guter Name für ein Unternehmen, das ? herstellt?"
prompt_template_cannot_respond = (
"Du kannst auf die Benutzereingabe nicht antworten: n"
"Bitte den Benutzer, den Namen eines Produkts einzugeben, damit du daraus ein Unternehmen machen kannst.n"
)
prompt_template_business = "Gib mir die beste Geschäftsmodell-Idee für mein Unternehmen mit dem Namen: "
prompt_template_is_product = (
"Dein Ziel ist es, herauszufinden, ob die Eingabe des Benutzers ein plausibler Produktname ist"
"Fragen, Begrüßungen, lange Sätze, Prominente oder andere irrelevante Eingaben gelten nicht als Produktnamen"
"Eingabe: n"
"Antworte nur mit 'True' oder 'False' 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),
cannot_respond_chain
).with_types(input_type=Dict[str, str], output_type=str)

print(full_chain.invoke())

Zählen wir die Unterschiede auf:

  • Die Syntax ist anders, okay, das ist klar.

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

  • Eingaben und Ausgaben sind typisiert, fast wie Funktionen.

  • Das fühlt sich nicht nach Python an.

Warum eignet sich LCEL besser für die industrielle Fertigung?

Hätte ich diesen Artikel bis genau zu dieser Stelle gelesen und mich jemand gefragt, ob ich von LCEL überzeugt sei, hätte ich wahrscheinlich mit Nein geantwortet. Die Syntax ist zu anders, und ich könnte meinen Code wahrscheinlich in Funktionen gliedern, um fast genau denselben Code zu erhalten. Aber ich sitze hier und schreibe diesen Artikel, also muss es noch mehr geben.

Sofort einsatzbereit: Aufrufen, Streamen und Batch-Verarbeitung

Durch den Einsatz von LCEL verfügt Ihre Kette automatisch über:

  • .invoke(): Du möchtest deine 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 automatisch übernommen (schneller als drei Aufrufe von `invoke`).

  • .stream(): Damit kannst du den Anfang der Vervollständigung ausgeben, noch bevor die vollständige Vervollständigung fertiggestellt ist.

my_chain = Eingabeaufforderung | 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=„“)

Bei der Iteration können Sie die `invoke`-Methode nutzen, um den Entwicklungsprozess zu vereinfachen. Wenn Sie jedoch die Ausgabe Ihrer Kette in einer Benutzeroberfläche anzeigen möchten, sollten Sie die Antwort streamen. Sie können nun die `stream`-Methode verwenden, ohne etwas umschreiben zu müssen.

Vorkonfigurierte asynchrone Methoden

In den meisten Fällen sind Frontend und Backend Ihrer Anwendung voneinander getrennt, was bedeutet, dass das Frontend eine Anfrage an das Backend sendet. Wenn Sie mehrere Benutzer haben, müssen Sie möglicherweise mehrere Anfragen gleichzeitig in Ihrem Backend bearbeiten.

Da der Großteil des Codes in LangChain lediglich aus Wartezeiten zwischen API-Aufrufen besteht, können wir asynchrone Code-Strukturen nutzen, um die Skalierbarkeit der API zu verbessern. Wenn Sie verstehen möchten, warum dies wichtig ist, empfehle ich Ihnen, die „Concurrent Burgers“-Geschichte in der FastAPI-Dokumentation zu lesen.
Sie müssen sich keine Gedanken über die Umsetzung machen, da asynchrone Methoden bereits verfügbar sind, wenn Sie LCEL verwenden:

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

Ich empfehle außerdem, die Seite „Why use LCEL“ aus der LangChain-Dokumentation zu lesen, die Beispiele für jede synchrone bzw. asynchrone Methode enthält.

Langchain hat diese „Out-of-the-Box“-Funktionen durch die Entwicklung einer einheitlichen Schnittstelle namens „Runnable“ realisiert . Um LCEL nun voll auszuschöpfen, müssen wir uns näher 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 dabei um ein von LangChain erstelltes Python-Objekt, das automatisch alle zuvor besprochenen Funktionen und vieles mehr übernimmt. Durch die Verwendung der LCEL-Syntax setzen wir in jedem Schritt ein neues „Runnable“ zusammen, was bedeutet, dass das am Ende erstellte Objekt ebenfalls ein „Runnable“ ist. Weitere Informationen zur Schnittstelle finden Sie in der offiziellen Dokumentation.

Alle Objekte aus dem folgenden Code sind entweder „Runnable“ oder Wörterbücher, 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
| # <— 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

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

Denn jedes `Runnable` innerhalb eines `RunnableParallel` wird parallel ausgeführt. Das bedeutet: Wenn Ihr `Runnable` drei unabhängige Schritte enthält, werden diese gleichzeitig auf verschiedenen Threads Ihres Computers ausgeführt, wodurch sich die Geschwindigkeit Ihrer Kette ganz von selbst erhöht!

Nachteile von LCEL

Trotz seiner Vorteile weist LCEL jedoch auch einige potenzielle Nachteile auf:

  • Nicht vollständig PEP-konform: LCEL hält sich nicht vollständig an PEP 20, den „Zen of Python“, in dem es heißt: „Explizit ist besser als implizit“. (Um PEP 20 zu überprüfen, können Sie den Befehl „import this“ in Python ausführen.) Darüber hinaus gilt die Syntax von LCEL nicht als „pythonesk“, da sie sich wie eine andere Sprache anfühlt; dies könnte LCEL für manche Python-Entwickler weniger intuitiv machen, sodass sie sich möglicherweise weigern, es zu verwenden.

  • LCEL ist eine domänenspezifische Sprache (DSL): Von den Benutzern wird erwartet, dass sie über gewisse Kenntnisse in Bezug auf Prompts, Ketten oder LLMs verfügen, um die Syntax effizient nutzen zu können.

  • Eingabe-/Ausgabe-Abhängigkeiten: Zwischenergebnisse und Endergebnisse müssen vom Anfang bis zum Ende weitergegeben werden. Wenn Sie beispielsweise das Ergebnis eines Zwischenschritts als Endergebnis verwenden möchten, müssen Sie es durch alle nachfolgenden Schritte mitführen. Dies kann in den meisten Ihrer Ketten zu zusätzlichen Argumenten führen, die zwar möglicherweise nicht verwendet werden, aber notwendig sind, wenn Sie über die Ausgabe darauf zugreifen möchten.

Fazit

Zusammenfassend lässt sich sagen, dass die LangChain Expression Language (LCEL) ein leistungsstarkes Werkzeug ist, das neue Perspektiven für die Entwicklung von Python-Anwendungen eröffnet. Trotz ihrer unkonventionellen Syntax empfehle ich den Einsatz von LCEL aus folgenden Gründen wärmstens:

  • Einheitliche Schnittstelle: Bietet eine einheitliche Schnittstelle für alle Ketten und erleichtert so die Industrialisierung Ihres Codes durch sofort einsatzbereite Stream-, Async- und Fallback-Modelle, Typisierung, Laufzeitkonfigurationen usw.

  • Automatische Parallelisierung: Führen Sie mehrere Aufgaben automatisch parallel aus, um die Ausführungsgeschwindigkeit Ihrer Ketten zu steigern und das Benutzererlebnis zu verbessern.

  • Kombinierbarkeit: Damit lassen sich Ketten einfach zusammenstellen und anpassen, wodurch Ihr Code flexibler und anpassungsfähiger wird.

Weiterführende Informationen…

Die ausführbare Abstraktion

Meiner Meinung nach ist es in manchen Fällen 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 nachimplementieren:

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

def __or__(self, other):
def chained_func(*args, **kwargs):
# self.func steht links, other steht 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 sollte das Ergebnis sein
print(result)
>>> 9.0

Ein „Runnable“ ist einfach ein Python-Objekt, bei dem die Methode .__or__() überschrieben wurde.
In der Praxis hat LangChain zahlreiche Funktionen hinzugefügt, darunter die Konvertierung von Wörterbüchern in „Runnables“, Typisierungsfunktionen, Konfigurationsmöglichkeiten sowie die Methoden „invoke“, „batch“, „stream“ und „async“!

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

Wenn Sie mehr erfahren möchten, empfehle ich Ihnen wärmstens, das LangChain-Cookbook auf LCEL durchzusehen.

class="lazyload

Medium-Blog von Artefact.

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