Ein Prompt ist keine beliebige Zeichenkette. Zumindest nicht in LangChain.
Wer produktionsnahe Anwendungen mit Large Language Models baut, steht früher oder später vor einer Erkenntnis: Die Art und Weise, wie wir mit einem Modell kommunizieren, entscheidet über Erfolg oder Scheitern der gesamten Architektur. Ein schlampig formulierter Prompt führt zu inkonsistenten Antworten. Ein zu rigider Prompt macht das System unflexibel. Und ein Prompt, der direkt im Code als String-Literal herumliegt, wird zum Wartungsalptraum, sobald sich Anforderungen ändern.
LangChain adressiert diese Probleme mit einem klaren Konzept: Prompts sind Objekte, keine Strings. Sie haben Struktur, sie können parametrisiert werden, sie lassen sich versionieren und testen. Sie sind der zentrale Steuerungsmechanismus für das Verhalten eines LLMs – und verdienen entsprechende Aufmerksamkeit.
Die zentrale Abstraktion in LangChain ist der
PromptValue. Dieser Typ fungiert als Brücke zwischen
verschiedenen Darstellungsformen eines Prompts. Ein
PromptValue kann sowohl als einfacher String als auch als
strukturierte Liste von Messages interpretiert werden. Das mag auf den
ersten Blick wie overengineering wirken, ergibt aber Sinn, sobald man
mit verschiedenen Modelltypen arbeitet.
Completion-Modelle (die klassischen GPT-3-artigen Systeme) erwarten
einen einzelnen String als Input. Chat-Modelle hingegen arbeiten mit
einer Sequenz von Messages – typischerweise unterteilt in System-, User-
und Assistant-Messages. Ein PromptValue macht beide Welten
zugänglich, ohne dass der Entwickler sich um die Details kümmern
muss.
Diese Flexibilität ist kein Luxus. Sie ist Notwendigkeit.
Denn in realen Projekten wechseln Modelle. Mal testet man verschiedene Anbieter, mal migriert man von einem älteren zu einem neueren Modell. Mit strukturierten Prompts bleibt der Code stabil, während die darunterliegende Modellinfrastruktur sich ändert.
LangChain unterscheidet drei fundamentale Prompt-Typen, die jeweils unterschiedliche Anwendungsszenarien bedienen:
1. String PromptTemplates
Der einfachste Fall. Ein Template mit Variablen, die zur Laufzeit
befüllt werden. Klassischer Anwendungsfall: simple Completion-Tasks, bei
denen keine Rollenunterscheidung nötig ist. Denken Sie an Übersetzungen,
Zusammenfassungen, einfache Generierungen.
2. ChatPromptTemplates
Hier wird es strukturierter. Statt eines einzelnen Strings definieren
wir eine Sequenz von Messages mit expliziten Rollen. Das System-Message
legt den Kontext fest (»Du bist ein hilfreicher Assistent für technische
Dokumentation«), User-Messages enthalten die eigentliche Anfrage,
Assistant-Messages können zur Few-Shot-Demonstration eingefügt
werden.
Warum diese Trennung? Weil moderne Chat-Modelle darauf trainiert sind, rollenbasierte Dialoge zu führen. Ein gut formuliertes System-Message verändert das Verhalten eines Modells grundlegend – ohne dass der eigentliche User-Prompt angepasst werden muss.
3. MessagesPlaceholder
Der flexibelste Typ. Hier definieren wir einen Platzhalter für eine
dynamische Liste von Messages. Das ist essentiell für
Konversationssysteme mit Verlauf: Wir bauen ein Template, das eine feste
Struktur hat (System-Message, eventuell einige Few-Shot-Beispiele), aber
einen variablen Slot für den bisherigen Gesprächsverlauf vorsieht.
Alle Prompt-Templates in LangChain folgen einem einheitlichen Muster: Sie erwarten bei der Instanziierung ein Dictionary, dessen Keys den Variablennamen im Template entsprechen. Diese Konvention mag auf den ersten Blick trivial wirken, hat aber weitreichende Konsequenzen für die Architektur.
Ein Dictionary als Input ermöglicht: - Klare Benennung: Variablen haben sprechende Namen, keine Positionsargumente - Optionale Parameter: Nicht alle Keys müssen immer gesetzt sein - Chainability: Outputs einer Komponente können direkt als Inputs für die nächste verwendet werden - Testbarkeit: Test-Inputs lassen sich sauber als Dictionaries definieren
Das ist das Fundament für LangChains Chain-Konzept – aber dazu später mehr. Fokussieren wir uns zunächst auf die praktische Anwendung.
Theorie ist das eine. Sehen wir uns an, wie diese Konzepte in echtem Code aussehen.
Beginnen wir mit dem einfachsten Fall: ein einzelnes Template, eine Variable, ein Completion-Task. Angenommen, wir bauen ein Tool, das technische Begriffe für verschiedene Zielgruppen erklärt. Die Zielgruppe variiert, die Struktur der Anfrage bleibt gleich.
from langchain.chat_models import init_chat_model
from langchain_core.prompts import PromptTemplate
# Template definieren: Ein Satz mit Platzhalter
template = PromptTemplate.from_template(
"Erkläre den Begriff '{concept}' für {audience}. "
"Verwende konkrete Beispiele und vermeide Fachjargon."
)
# Template instanziieren mit konkreten Werten
prompt = template.invoke({
"concept": "Event Sourcing",
"audience": "Product Owner ohne technischen Hintergrund"
})
# Prompt ist jetzt ein PromptValue-Objekt
# Für String-Modelle: zu String casten
print(prompt.to_string())
# Mit einem LLM verwenden
llm = init_chat_model("ollama:gpt-oss", temperature=0.3)
response = llm.invoke(prompt)
print(response)Zwei Dinge fallen auf: Erstens, die
from_template-Methode erwartet eine String-Vorlage mit
geschweiften Klammern als Variablenmarkierung. Das ist
Python-String-Formatting, nichts Exotisches. Zweitens, die
invoke-Methode gibt kein String zurück, sondern ein
PromptValue-Objekt. Für die Weiterverarbeitung mit einem
String-basierten Modell müssen wir explizit zu String casten.
Warum dieser Umweg?
Weil wir dasselbe Template auch mit einem Chat-Modell verwenden könnten – und dort würde LangChain automatisch eine passende Message-Struktur erzeugen. Das Template bleibt identisch, die Repräsentation ändert sich je nach Zielmodell.
Die meisten modernen Anwendungen arbeiten mit Chat-Modellen. Hier brauchen wir explizite Rollenunterscheidung. Schauen wir uns an, wie ein Template für einen Code-Review-Assistenten aussehen könnte.
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
# Chat-Template mit System- und User-Message
template = ChatPromptTemplate([
("system",
"Du bist ein erfahrener Software-Architekt mit Fokus auf "
"Clean Code und Wartbarkeit. Deine Reviews sind konstruktiv, "
"präzise und priorisieren kritische Issues."),
("user",
"Reviewe folgenden {language}-Code:\n\n{code}\n\n"
"Fokussiere dich auf: {focus_areas}")
])
# Template mit Werten befüllen
prompt = template.invoke({
"language": "Python",
"code": """
def calc(x, y, op):
if op == '+':
return x + y
elif op == '-':
return x - y
# ... weitere Operationen
""",
"focus_areas": "Fehlerbehandlung, Erweiterbarkeit"
})
# Mit Chat-Modell verwenden
chat_model = init_chat_model("ollama:gpt-oss", temperature=0.2)
response = chat_model.invoke(prompt)
print(response.content)Hier wird die Struktur explizit: Die erste Nachricht ist vom Typ
system, die zweite vom Typ user. Das
System-Message definiert Persona und Erwartungshaltung. Das User-Message
enthält die eigentliche Aufgabe mit drei Variablen: Programmiersprache,
Code und Fokusbereich.
Ein entscheidender Unterschied zu String-Templates: Das System-Message ist fix, aber mächtig. Es beeinflusst den gesamten Ton und Stil der Antwort, ohne dass wir es bei jedem Aufruf neu definieren müssen. Das ist Separation of Concerns auf Prompt-Ebene.
Eine häufige Fehlerquelle: Variablennamen im Template stimmen nicht
mit Dictionary-Keys überein. LangChain wirft einen
KeyError, wenn eine erwartete Variable fehlt. Das ist gut –
fail fast ist besser als stille Fehler. Aber es erfordert Disziplin beim
Naming.
Zweite Stolperfalle: Wenn der Text selbst geschweifte Klammern enthalten soll (etwa JSON-Beispiele oder Code), müssen diese escaped werden:
template = PromptTemplate.from_template(
"Generiere JSON im Format: {{'name': '{name}', 'age': {age}}}"
)Doppelte geschweifte Klammern signalisieren: Das ist kein Platzhalter, sondern ein Literal.
Die wahre Stärke von LangChain zeigt sich bei Systemen mit Kontext.
Ein Chatbot, der sich an vorherige Nachrichten erinnert. Ein
Debugging-Assistent, der schrittweise Lösungen entwickelt. Hier brauchen
wir MessagesPlaceholder.
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage
# Template mit festem System-Message und variablem Gesprächsverlauf
template = ChatPromptTemplate([
("system",
"Du bist ein interaktiver Python-Tutor. Erkläre Konzepte schrittweise "
"und passe dich dem Verständnisniveau des Lernenden an."),
MessagesPlaceholder("conversation_history"),
("user", "{current_question}")
])
# Initialer Zustand: keine History
first_interaction = template.invoke({
"conversation_history": [],
"current_question": "Was ist eine List Comprehension?"
})
llm = init_chat_model("ollama:dolphin-mixtral", temperature=0.2)
first_response = llm.invoke(first_interaction)
# Zweite Interaktion: jetzt mit History
conversation_so_far = [
HumanMessage(content="Was ist eine List Comprehension?"),
AIMessage(content=first_response.content)
]
second_interaction = template.invoke({
"conversation_history": conversation_so_far,
"current_question": "Kannst du ein Beispiel mit Filter zeigen?"
})
second_response = llm.invoke(second_interaction)
print(second_response.content)Beachten Sie die Struktur: Der MessagesPlaceholder sitzt
zwischen System-Message und aktueller User-Frage. Das ist
bewusst. Der Gesprächsverlauf gehört nicht zur System-Instruktion (die
bleibt konstant), aber er muss vor der aktuellen Frage stehen, damit das
Modell den Kontext versteht.
Eine subtile, aber wichtige Designentscheidung.
Die History wird als Liste von HumanMessage und
AIMessage Objekten übergeben. Nicht als Strings, nicht als
Dictionaries, sondern als typisierte Message-Objekte. Das mag
umständlich wirken, zahlt sich aber aus: Wir können Messages mit
Metadaten anreichern, sie separat verarbeiten, sie serialisieren und aus
Datenbanken laden.
Die Dokumentation zeigt eine kompaktere Syntax, die ohne explizite
MessagesPlaceholder-Instanz auskommt:
template = ChatPromptTemplate([
("system", "Du bist ein hilfreicher Assistent"),
("placeholder", "{msgs}") # Äquivalent zu MessagesPlaceholder("msgs")
])Hier wird "placeholder" als Typ-String verwendet, analog
zu "system" oder "user". Das ist syntactic
sugar – unter der Haube passiert dasselbe. Welche Variante Sie wählen,
ist Geschmackssache. Die explizite Form mit
MessagesPlaceholder ist semantisch klarer, die
Inline-Variante kompakter.
Ein mächtiges Pattern, das sich aus der Chat-Struktur ergibt: Few-Shot Learning direkt im Prompt. Wir demonstrieren dem Modell das gewünschte Verhalten durch Beispiel-Interaktionen.
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
# Template mit eingebetteten Beispielen
few_shot_template = ChatPromptTemplate([
("system",
"Du klassifizierst technische Support-Anfragen nach Priorität. "
"Antworte immer im Format: PRIORITY: [LOW|MEDIUM|HIGH] - Begründung"),
# Beispiel 1: Niedrige Priorität
("user", "Wie ändere ich mein Passwort?"),
("assistant", "PRIORITY: LOW - Standardanfrage, keine Dringlichkeit"),
# Beispiel 2: Mittlere Priorität
("user", "Nach dem letzten Update lädt die Seite langsamer."),
("assistant",
"PRIORITY: MEDIUM - Funktioniert, aber Performance-Problem betrifft User Experience"),
# Beispiel 3: Hohe Priorität
("user", "Kann mich nicht einloggen, Fehlermeldung 500"),
("assistant",
"PRIORITY: HIGH - Kritische Funktion nicht verfügbar, möglicherweise Server-Problem"),
# Platzhalter für echte Anfrage
("user", "{ticket_description}")
])
# Verwendung
prompt = few_shot_template.invoke({
"ticket_description": "Dashboard zeigt falsche Zahlen seit heute Morgen"
})
llm = init_chat_model("ollama:gpt-oss", temperature=0.0)
response = llm.invoke(prompt)
print(response.content)Die Beispiele sind fest im Template verankert – das ist beabsichtigt. Bei jeder Anfrage sieht das Modell dieselben Demonstrationen. Das führt zu konsistentem Output-Format, auch ohne explizites Schema oder Parsing.
Ein Wort zur Performance: Few-Shot Prompts erhöhen die Token-Anzahl erheblich. Drei Beispiel-Paare à 50 Tokens summieren sich. Bei hohem Throughput kann das teuer werden. Die Alternative: Fine-Tuning. Aber das ist eine andere Diskussion.
Lange Templates stoßen schnell an die Grenzen des Kontext-Fensters. Ein typisches GPT-4-Modell hat 8.000 oder 32.000 Token Kapazität – je nach Variante. Ein ausführliches System-Message (500 Tokens), einige Few-Shot-Beispiele (600 Tokens), Konversationshistorie (2.000 Tokens) und die aktuelle Frage (200 Tokens) – schon sind wir bei 3.300 Tokens, bevor das Modell überhaupt antwortet.
Die Antwort braucht auch Platz. Bei 1.500 Token Output bleiben noch 2.500 Token Reserve. Das klingt nach viel, reicht aber nicht ewig. Sobald Konversationen länger werden oder Dokumente eingefügt werden sollen, wird’s eng.
Die Lösung liegt außerhalb des Prompt-Systems: Context Window Management, Summarization, Sliding Windows. Aber es ist wichtig zu verstehen, dass Prompts nicht unbegrenzt wachsen können. Design accordingly.
Ein oft unterschätzter Aspekt: Wie sieht der Prompt eigentlich aus, den wir ans Modell schicken? Bei Templates mit vielen Variablen und verschachtelten Strukturen verliert man schnell den Überblick.
LangChain bietet dafür hilfreiche Methoden:
# Für String-Templates
string_prompt = PromptTemplate.from_template("Erkläre {concept}")
filled = string_prompt.invoke({"concept": "Closures"})
print(filled.to_string())
# Für Chat-Templates
chat_prompt = ChatPromptTemplate([
("system", "System-Instruktion"),
("user", "{question}")
])
filled_chat = chat_prompt.invoke({"question": "Was ist Polymorphismus?"})
# Als Messages inspizieren
for msg in filled_chat.to_messages():
print(f"{msg.type}: {msg.content}")Die to_messages()-Methode ist besonders nützlich: Sie
zeigt die exakte Message-Struktur, die ans Modell geht. Inklusive Typen,
Reihenfolge und Inhalt. Das hilft beim Debuggen und beim Verstehen,
warum ein Modell vielleicht unerwartet reagiert.
Prompting in LangChain ist keine Nachgedanke. Es ist ein zentrales Designprinzip.
Indem Prompts als Objekte modelliert werden – parametrisierbar, typisiert, komposierbar –, wird das Verhalten von LLMs greifbar und steuerbar. String-Templates für einfache Tasks, Chat-Templates für strukturierte Dialoge, MessagesPlaceholder für zustandsbehaftete Konversationen: Jeder Typ hat seinen Platz, seine Stärken, seine idiomatische Verwendung.
Das mag anfangs wie zusätzliche Komplexität wirken. Aber in dem Moment, in dem Sie den dritten Prompt für dieselbe Aufgabe schreiben, in dem Sie versuchen, Kontext konsistent zu halten, in dem Sie unterschiedliche Modelle vergleichen wollen – in diesem Moment zahlt sich die Struktur aus.
Prompts sind der Steuercode Ihrer KI-Anwendung. Behandeln Sie sie entsprechend.
Versionieren Sie sie. Testen Sie sie. Iterieren Sie über sie. Denn letztlich ist ein exzellent strukturierter Prompt der Unterschied zwischen einem System, das funktioniert, und einem System, das zuverlässig funktioniert.