NLP-Workflows bauen — von der Idee zur Pipeline
Du musst Textdaten verarbeiten: Kunden-Tickets klassifizieren, Entities extrahieren, Zusammenfassungen generieren. Die Frage ist nicht “welches Tool?” sondern “welches Tool wofür?”
Dieser Post zeigt dir einen kompletten NLP-Workflow mit sechs Libraries und jede da eingesetzt, wo sie am stärksten ist.
Das Szenario
Angenommen, ein deutscher E-Commerce-Shop bekommt täglich 10.000 Kundenanfragen. Die müssen:
- In Kategorien sortiert werden (Reklamation, Retoure, Zahlung, Versand)
- Wichtige Entitäten extrahieren (Bestellnummer, Datum, Betrag)
- Je nach Kategorie eine passende Antwort generieren
- Bei hohem Konfliktpotenzial eskalieren
Wir bauen das Schritt für Schritt mit verschiedenen Tools.
1. Textvorverarbeitung mit spaCy
spaCy ist das Schweizer Taschenmesser für NLP — schnell, erprobt und für Produktions-Workflows gebaut. Es macht Tokenisierung, POS-Tagging, Lemmatisierung und NER mit einem nlp-Objekt.
# nlp_pipeline.py — Textvorverarbeitung mit spaCy
# spaCy verarbeitet Text in einem Pipeline-Zug: Tokenize → POS → Lemma → NER
import spacy
# Lade das deutsche spaCy-Modell
# "de_core_news_sm" ist das kleine Modell (schnell, ~15MB)
# Für bessere NER-Qualität nimm "de_core_news_lg" (~50MB) oder "de_dep_news_trf" (Transformer)
nlp = spacy.load("de_core_news_sm")
# Beispiel-Anfrage eines Kunden
# Das ist der Roh-Text, der aus dem Ticketsystem kommt
text = (
"Sehr geehrte Damen und Herren, "
"ich habe am 15.06.2026 eine Jacke bestellt (Bestell-Nr.: 4829103), "
"aber die gelieferte Größe M passt nicht. "
"Ich möchte die Jacke gegen Größe L umtauschen. "
"Der Kaufbetrag war 89,95 €."
)
# spaCy verarbeitet den Text in einem Durchlauf
# doc enthält: Tokens, POS-Tags, Lemmas, Entitäten, Satzgrenzen
doc = nlp(text)
# Tokenisierung + Lemmatisierung
# Tokens sind die einzelnen Wörter
# .lemma_ gibt die Grundform zurück ("
print("=== Token & Lemma ===")
for token in doc:
# token.text — das originale Wort ("bestellt")
# token.lemma_ — die Grundform ("bestellen") — wichtig für Suche/Indexierung
# token.pos_ — die Wortart (VERB, NOUN, ADP, etc.)
# token.is_stop — ist es ein Stoppwort (der, die, das, und, etc.)?
if not token.is_punct: # Satzzeichen überspringen
print(f"{token.text:20} → Lemma: {token.lemma_:15} POS: {token.pos_}")
# Named Entity Recognition (NER)
# Extrahiert strukturierte Informationen aus dem Text
print("\n=== Entitäten (NER) ===")
for ent in doc.ents:
# ent.text — der gefundene Text ("15.06.2026")
# ent.label_ — die Entitäten-Klasse ("DATE", "MONEY", "PRODUCT", etc.)
# ent.start_char / ent.end_char — Position im Original-Text (für Highlighting)
print(f"{ent.text:25} → Typ: {ent.label_} (Position: {ent.start_char}-{ent.end_char})")
spaCy ist ideal für: schnelle Vorverarbeitung, POS-Tagging, grundlegende NER. spaCy ist nicht ideal für: semantisches Verständnis, Klassifikation komplexer Texte.
2. Embeddings + Klassifikation mit scikit-learn
Für die Kategorisierung der Tickets nehmen wir einen traditionellen ML-Ansatz: TF-IDF-Vektorisierung + SVM. Das ist billig, schnell und für 10.000 Tickets/Tag völlig ausreichend.
# classify_tickets.py — Ticket-Kategorisierung mit scikit-learn
# Traditionelles ML: schneller als ein LLM, genauso gut für einfache Kategorien
import spacy
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import pandas as pd
# Trainingsdaten — in der echten Welt wären das 10.000+ gelabelte Tickets
# Das Format: (Text, Kategorie) — Kategorien sind unser Ziel
data = [
("Die Jacke ist kaputt. Bitte um Rückerstattung.", "Reklamation"),
("Ich möchte meine Bestellung 4829103 stornieren.", "Storno"),
("Die Lieferung war zu spät. Wo bleibt mein Paket?", "Versand"),
("Mir wurde zweimal abgebucht. Bitte prüfen.", "Zahlung"),
("Die Größe passt nicht. Ich schicke es zurück.", "Retoure"),
("Kann ich meine Lieferadresse noch ändern?", "Versand"),
("Auf meiner Rechnung stimmt der Betrag nicht.", "Zahlung"),
("Das Produkt ist defekt. Bitte um Austausch.", "Reklamation"),
("Ich habe noch nichts erhalten. Sendungsverfolgung?", "Versand"),
("Meine Karte wurde belastet, aber keine Bestellbestätigung.", "Zahlung"),
]
# DataFrame für einfachere Verarbeitung
df = pd.DataFrame(data, columns=["text", "category"])
print(f"Geladene Trainingsdaten: {len(df)} Einträge")
print(f"Kategorien: {df['category'].value_counts().to_dict()}")
# Train/Test-Split — 80% train, 20% test
# random_state=42 sorgt für reproduzierbare Ergebnisse
X_train, X_test, y_train, y_test = train_test_split(
df["text"], df["category"],
test_size=0.2,
random_state=42,
stratify=df["category"] # Gleiche Verteilung in Train/Test
)
# Pipeline: TF-IDF → SVM (LinearSVC)
# Ein Pipeline-Objekt fasst alle Schritte zusammen
# Schritt 1: TfidfVectorizer — wandelt Text in numerische Vektoren um
# sublinear_tf=True — logarithmiert die TF-Werte (reduziert Effekt von häufigen Wörtern)
# max_df=0.8 — ignoriert Wörter, die in >80% der Dokumente vorkommen (zu allgemein)
# min_df=2 — ignoriert Wörter, die in <2 Dokumenten vorkommen (zu selten)
# Schritt 2: LinearSVC — Support Vector Machine mit linearem Kernel
# C=1.0 — Regularisierungsparameter (kleiner = mehr Regularisierung)
# class_weight='balanced' — gleicht ungleiche Klassen aus (wichtig für echte Daten)
model = Pipeline([
("tfidf", TfidfVectorizer(
sublinear_tf=True,
max_df=0.8,
min_df=2,
ngram_range=(1, 2), # Einzelwörter + Bigramme
stop_words="german" # Deutsche Stoppwörter entfernen
)),
("clf", LinearSVC(C=1.0, class_weight="balanced", random_state=42)),
])
# Training — in der Regel < 1 Sekunde für 10.000 Dokumente
# Das ist der Vorteil von traditionellem ML gegenüber LLMs
print("\nTrainiere Modell...")
model.fit(X_train, y_train)
# Evaluation
y_pred = model.predict(X_test)
print("\n=== Klassifikationsergebnisse ===")
print(classification_report(y_test, y_pred))
# Vorhersage für eine neue Anfrage
# Echtzeit-Klassifikation in < 10ms — wichtig für 10.000 Tickets/Tag
neue_anfrage = "Ich habe meine Bestellung immer noch nicht erhalten. Wo bleibt sie?"
vorhersage = model.predict([neue_anfrage])[0]
print(f"Neue Anfrage: '{neue_anfrage}'")
print(f"Kategorie: {vorhersage}")
# Wahrscheinlichkeiten für die Vorhersage
# Entscheidungsfunktion gibt Confidence-Werte pro Klasse zurück
# Höher = sicherer — nützlich für Eskalationslogik
decision_values = model.decision_function([neue_anfrage])[0]
confidence = max(decision_values) - sorted(decision_values)[-2]
print(f"Confidence: {confidence:.2f}")
if confidence < 0.5:
print("⚠️ Niedrige Confidence — manuelle Prüfung empfohlen")
Wir wandeln text in eine numerische Matrix um. Durch TF-IDF (Term Frequency x Inverse Document Frequency) method, können wir einfach häufige Wörter in einem Dokument mit seltene Wörter insgesamt.
scikit-learn ist ideal für: schnelle, billige Klassifikation mit wenig Trainingsdaten. scikit-learn ist nicht ideal für: Deep Learning, Textgenerierung, komplexe NLP-Aufgaben.
3. Feingranulare Extraktion mit Hugging Face Transformers
Für feinere Entity-Extraktion, das heiß “Bestellnummer”, “Betrag”, “Datum” als strukturierte Felder, nehmen wir ein Token-Classification-Modell von Hugging Face. Das erkennt mehr als spaCy’s generisches NER.
# entity_extraction.py — Feingranulare Entity-Extraktion mit Transformers
# Ein BERT-Modell erkennt auch domainspezifische Entitäten wie Bestellnummern
from transformers import pipeline, AutoTokenizer, AutoModelForTokenClassification
import torch
# Lokales oder heruntergeladenes Modell für Token-Classification
# "dbmdz/bert-large-cased-finetuned-conll03-german" — deutsches NER-Modell
# Alternativ: ein eigenes fine-getuntes Modell für Bestellnummern, Beträge, etc.
model_name = "dbmdz/bert-large-cased-finetuned-conll03-german"
# Lade Tokenizer und Modell
# Der Tokenizer wandelt Text in Input-IDs um (Zahlen, die BERT versteht)
# Das Modell gibt für jedes Token eine Entitäten-Klasse zurück
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForTokenClassification.from_pretrained(model_name)
# Erstelle eine NER-Pipeline — High-Level-API für Inference
# aggregation_strategy="simple" — fasst "Berlin" + "er" → "Berliner" zusammen
# device=0 nutzt GPU falls verfügbar (sonst CPU)
device = 0 if torch.cuda.is_available() else -1
ner_pipeline = pipeline(
"ner",
model=model,
tokenizer=tokenizer,
aggregation_strategy="simple",
device=device,
)
text = "Meine Bestellung 4829103 vom 15.06.2026 über 89,95 € ist noch nicht angekommen."
ergebnisse = ner_pipeline(text)
print(f"=== Entity Extraction mit Transformers ===")
print(f"Text: {text}\n")
for ent in ergebnisse:
print(f"{ent['word']:20} → {ent['entity_group']:10} "
f"(Score: {ent['score']:.3f})")
# 'word' — der erkannte Text
# 'entity_group' — die Entitäten-Klasse (PER, ORG, LOC, MISC)
# 'score' — Confidence des Modells (0-1)
# 'start'/'end' — Position im Original-Text
Hugging Face Transformers ist ideal für: hochwertige NER, Textklassifikation, Question Answering. Hugging Face ist nicht ideal für: Pipeline-Orchestrierung, Multi-Step-Workflows.
4. Prompt-basierte Extraktion mit PydanticAI
PydanticAI kombiniert LLM-Calls mit Pydantic-Schemas. Du definierst ein Ausgabe-Schema als Pydantic-Modell, und das LLM extrahiert die Felder direkt. Kein Parsen, kein Regex.
# pydanticai_extraction.py — Strukturierte Extraktion mit PydanticAI + LLM
# PydanticAI ruft ein LLM (OpenAI, Anthropic, Ollama) auf und validiert die Antwort
# gegen ein Pydantic-Schema. Das Ergebnis ist garantiert typsicher.
from pydantic import BaseModel, Field
from pydantic_ai import Agent
import asyncio
# Schritt 1: Definiere das Ausgabe-Schema
# Jedes Feld entspricht einer Information, die extrahiert werden soll
# Der Field-Beschreibungstext ist der Prompt für das LLM — je genauer, desto besser
class TicketExtraktion(BaseModel):
"""
Pydantic-Modell für die strukturierte Extraktion aus Kunden-Tickets.
Jedes Feld hat einen Python-Typ und eine Beschreibung für das LLM.
"""
# str-Feld mit Beschreibung — das LLM extrahiert den Wert aus dem Text
bestellnummer: str = Field(
description="Die Bestellnummer im Format XXXXXXXX (8-stellige Zahl)"
)
# Optional = None wenn nicht im Text vorhanden (reduziert LLM-Halluzination)
datum: str | None = Field(
description="Das Datum des Vorfalls im Format DD.MM.YYYY"
)
# float-Feld — das LLM extrahiert den Betrag als Zahl
betrag: float | None = Field(
description="Der Betrag in Euro, falls genannt"
)
# str mit Enum-artigen Werten — schränkt die LLM-Ausgabe ein
kategori: str = Field(
description="Die Kategorie der Anfrage: Reklamation, Retoure, Zahlung, Versand, Storno"
)
# bool-Feld für Eskalationslogik
konfliktpotenzial: bool = Field(
description="True, wenn der Kunde verärgert klingt oder rechtliche Schritte androht"
)
# Schritt 2: Erstelle einen PydanticAI Agent
# Der Agent ruft ein LLM auf, validiert die Antwort gegen das Schema
# Bei Validierungsfehlern wiederholt der Agent den Call automatisch
agent = Agent(
"openai:gpt-4o-mini", # Welches Modell?
result_type=TicketExtraktion, # Erwartetes Ausgabe-Schema
system_prompt=( # System-Prompt für das LLM
"Du extrahierst strukturierte Daten aus deutschen Kundenanfragen. "
"Antworte NUR mit dem JSON-Schema. Kein Fließtext. "
"Wenn ein Feld nicht im Text steht, setze es auf null."
),
)
# Schritt 3: Extraktion durchführen
# async/await — der Agent macht einen HTTP-Call zum LLM-Provider
async def extrahiere_ticket(text: str) -> TicketExtraktion:
result = await agent.run(text)
# result.data ist bereits ein valides TicketExtraktion-Objekt
# Pydantic hat die LLM-Antwort geparst und validiert
return result.data
# Beispiel-Anfrage
text = (
"Sehr geehrte Damen und Herren, "
"meine Bestellung 4829103 vom 15.06.2026 ist bis heute nicht angekommen. "
"Ich habe 89,95 € bezahlt und nichts erhalten. "
"Wenn das Paket nicht morgen da ist, werde ich rechtliche Schritte einleiten."
)
async def main():
extraktion = await extrahiere_ticket(text)
print("=== PydanticAI Extraktion ===")
print(f"Bestellnummer: {extraktion.bestellnummer}")
print(f"Datum: {extraktion.datum}")
print(f"Betrag: {extraktion.betrag} €")
print(f"Kategorie: {extraktion.kategori}")
print(f"Konflikt: {'⚠️ Ja' if extraktion.konfliktpotenzial else '✅ Nein'}")
# Das extrahierte Objekt ist ein Pydantic-Modell
# Es lässt sich direkt als JSON speichern oder an eine Datenbank übergeben
print(f"\nJSON-Ausgabe:")
print(extraktion.model_dump_json(indent=2))
# Python 3.12+ erforderlich für diesen Aufruf
# Alternativ: asyncio.run(main()) in einem async-fähigen Kontext
asyncio.run(main())
PydanticAI ist ideal für: strukturierte LLM-Extraktion, typsichere Ausgaben, Formular-Befüllung. PydanticAI ist nicht ideal für: reine Textklassifikation (dafür ist scikit-learn billiger).
5. Multi-Step-Workflows mit LangGraph
LangGraph baut verzweigte Workflows — wenn die Kategorie “Reklamation” ist, mach eine Extraktion, wenn “Versand”, prüfe die Sendungsnummer. Jeder Schritt ist ein Node im Graph.
# langgraph_workflow.py — Verzweigter NLP-Workflow mit LangGraph
# LangGraph erlaubt: wenn-dann-Logik, Loops, parallele Schritte
# Perfekt für: bedingte Extraktion → je nach Kategorie unterschiedliche Verarbeitung
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
import random
# Der State definiert, welche Daten durch den Workflow wandern
# Jeder Node kann den State lesen und schreiben
class WorkflowState(TypedDict):
"""
TypedDict = typsicheres Dictionary mit bekannten Feldern.
LangGraph übergibt den State von Node zu Node.
"""
text: str # Original-Kundenanfrage
category: str # Klassifikationsergebnis
entities: dict # Extrahierte Entitäten
response: str # Generierte Antwort
severity: str # Eskalationsstufe (low, medium, high)
# Node 1: Klassifiziere das Ticket (hier simuliert, in echt mit scikit-learn)
def classify(state: WorkflowState) -> WorkflowState:
"""
Erster Node: Bestimmt die Kategorie des Tickets.
In Produktion: model.predict() aus dem scikit-learn-Pipeline.
"""
text = state["text"]
# Simulierte Klassifikation — ersetzt durch echten Modell-Call
if "Bestellung" in text and "nicht" in text:
category = "Versand"
elif "Rückerstattung" in text or "defekt" in text:
category = "Reklamation"
elif "Größe" in text or "Umtausch" in text:
category = "Retoure"
else:
category = "Allgemein"
print(f"[Klassifikation] → {category}")
return {"category": category} # Nur geänderte Felder zurückgeben
# Node 2: Extrahiere Entitäten (hier simuliert, in echt mit PydanticAI oder Transformers)
def extract_entities(state: WorkflowState) -> WorkflowState:
"""
Zweiter Node: Extrahiert Bestellnummer, Datum, Betrag.
In Produktion: entwerder Hugging Face NER oder PydanticAI-Call.
"""
text = state["text"]
# Simulierte Extraktion — in echt: agent.run(text)
entities = {}
if "4829103" in text:
entities["bestellnummer"] = "4829103"
if "15.06.2026" in text:
entities["datum"] = "15.06.2026"
if "89,95" in text:
entities["betrag"] = 89.95
print(f"[Extraktion] → {entities}")
return {"entities": entities}
# Node 3: Generiere Antwort (je nach Kategorie)
def generate_response(state: WorkflowState) -> WorkflowState:
"""
Dritter Node: Erzeugt eine passende Antwort basierend auf Kategorie.
Branching: unterschiedliche Logik für unterschiedliche Kategorien.
"""
category = state["category"]
entities = state["entities"]
bestellnr = entities.get("bestellnummer", "unbekannt")
responses = {
"Versand": (
f"Wir entschuldigen uns für die Verspätung. "
f"Die Sendung {bestellnr} wird heute priorisiert bearbeitet."
),
"Reklamation": (
f"Wir bedauern den Defekt. "
f"Zu {bestellnr} wurde ein Retouren-Schein erstellt."
),
"Retoure": (
f"Der Umtausch für {bestellnr} wird bearbeitet. "
f"Sie erhalten in Kürze ein Rücksende-Label."
),
"Allgemein": (
f"Vielen Dank für Ihre Anfrage. "
f"Wir melden uns innerhalb von 24 Stunden zu {bestellnr}."
),
}
antwort = responses.get(category, responses["Allgemein"])
print(f"[Antwort] → {category}: {antwort[:50]}...")
return {"response": antwort}
# Node 4: Bestimme Eskalationsstufe
def assess_severity(state: WorkflowState) -> WorkflowState:
"""
Vierter Node: Eskaliert bei bestimmten Schlüsselwörtern.
Zeigt: Verzweigungen im Workflow (bedingte Pfade).
"""
text = state["text"].lower()
if "rechtliche Schritte" in text or "Anwalt" in text:
severity = "high"
elif "enttäuscht" in text or "nie wieder" in text:
severity = "medium"
else:
severity = "low"
print(f"[Eskalation] → {severity}")
return {"severity": severity}
# Node 5: Entscheidung nach Klassifikation (Conditional Edge)
def route_after_classification(state: WorkflowState) -> Literal["extract", "end"]:
"""
Bedingte Kante: Wenn keine Kategorie erkannt, überspringe Extraktion.
LangGraph ruft diese Funktion auf, um den nächsten Node zu bestimmen.
"""
if state["category"] == "Allgemein":
return "end" # Keine Extraktion nötig
return "extract"
# Graph bauen
workflow = StateGraph(WorkflowState)
# Nodes registrieren — jeder Node ist eine Funktion, die State → State macht
workflow.add_node("classify", classify)
workflow.add_node("extract", extract_entities)
workflow.add_node("generate", generate_response)
workflow.add_node("assess", assess_severity)
# Start-Node — der Workflow beginnt hier
workflow.set_entry_point("classify")
# Kanten definieren — die Reihenfolge der Nodes
workflow.add_conditional_edges(
"classify", # Start-Node
route_after_classification, # Entscheidungs-Funktion
{"extract": "extract", "end": "generate"} # Mapping: Rückgabewert → nächster Node
)
workflow.add_edge("extract", "generate")
workflow.add_edge("generate", "assess")
workflow.add_edge("assess", END)
# Workflow kompilieren — LangGraph prüft auf Zyklen und fehlende Edges
app = workflow.compile()
# Workflow ausführen
initial_state = WorkflowState(
text="Meine Bestellung 4829103 ist nicht angekommen. Rechtliche Schritte!",
category="",
entities={},
response="",
severity="low",
)
print("=== LangGraph Workflow ===")
print(f"Input: {initial_state['text']}\n")
final_state = app.invoke(initial_state)
print("\n=== Finaler State ===")
for key, value in final_state.items():
print(f"{key}: {value}")
LangGraph ist ideal für: komplexe, verzweigte NLP-Workflows mit Zustand. LangGraph ist nicht ideal für: einfache lineare Pipelines (dafür reicht Python-Code).
6. LLM-Chaining mit LangChain
LangChain kettet mehrere LLM-Calls mit Tools und Memory. Nützlich, wenn du dem LLM Kontext aus vorherigen Calls geben willst — z.B. bei einer Konversation mit dem Kunden.
# langchain_chaining.py — LLM-Chaining mit LangChain
# LangChain verbindet: LLM-Calls + Tools (API, DB) + Memory (vorherige Messages)
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain_community.tools import DuckDuckGoSearchRun
# LLM initialisieren
# temperature=0 — deterministische Antworten (für Extraktion besser)
# model="gpt-4o-mini" — billig und schnell
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Schritt 1: Extrahiere Bestellnummer
# ChatPromptTemplate — Prompt-Vorlage mit Platzhaltern
# {text} wird später durch die eigentliche Kundenanfrage ersetzt
extract_prompt = ChatPromptTemplate.from_messages([
("system", "Extrahiere die Bestellnummer aus dem folgenden Text. "
"Antworte NUR mit der Nummer, kein Fließtext."),
("human", "{text}"),
])
# Schritt 2: Prüfe Sendungsstatus (Tool-Aufruf)
# DuckDuckGoSearch erlaubt dem LLM, das Internet zu durchsuchen
# In Produktion: eine interne API für Sendungsverfolgung
search = DuckDuckGoSearchRun()
# Schritt 3: Generiere Antwort
response_prompt = ChatPromptTemplate.from_messages([
("system", "Du bist ein freundlicher Kundenservice-Mitarbeiter. "
"Antworte auf Deutsch. Beziehe dich auf die Bestellung."),
("human", "Bestellung {bestellnummer}. Suchergebnis: {search_result}"),
])
# Chain bauen — mit LangChain Expression Language (LCEL)
# LCEL verwendet den | Operator (Pipe), um Schritte zu verketten
chain = (
# Schritt 1: Extrahiere Bestellnummer aus dem Text
# extract_prompt | llm | StrOutputParser() = Prompt → LLM → String
{"bestellnummer": extract_prompt | llm | StrOutputParser(), "text": RunnablePassthrough()}
# RunnablePassthrough() gibt den Input unverändert weiter
| RunnablePassthrough()
# Schritt 2: Suche parallel zur Extraktion
# Hier könnten parallele Tools laufen
| (lambda x: {
"bestellnummer": x["bestellnummer"],
"search_result": search.invoke(f"Sendungsstatus {x['bestellnummer']}"),
"text": x["text"],
})
# Schritt 3: Generiere Antwort
| response_prompt
| llm
| StrOutputParser()
)
print("=== LangChain Pipeline ===")
ergebnis = chain.invoke("Meine Bestellung 4829103 ist weg. Wo ist sie?")
print(f"Antwort: {ergebnis}")
LangChain ist ideal für: einfache LLM-Chains mit Tools, RAG (Retrieval-Augmented Generation). LangChain ist nicht ideal für: komplexe Verzweigungen (dafür LangGraph), billige Klassifikation (dafür scikit-learn).
7. Alles Zusammen: Die Produktions-Pipeline
# production_pipeline.py — Alle Tools im Zusammenspiel
# Das ist die Gesamt-Architektur für das Szenario
import spacy
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
from transformers import pipeline as hf_pipeline
from pydantic_ai import Agent
from pydantic import BaseModel, Field
from langgraph.graph import StateGraph, END
from typing import TypedDict, Literal
# SCHICHT 1: spaCy — Vorverarbeitung
# spaCy läuft auf CPU, verarbeitet 1000 Texte pro Sekunde
# Macht: Tokenisierung, POS-Tagging, Lemmatisierung, Basis-NER
# Output: Strukturierte Text-Repräsentation für nachgelagerte Schritte
nlp = spacy.load("de_core_news_sm")
def preprocess(text: str) -> dict:
"""Vorverarbeitung mit spaCy — schnell und deterministisch."""
doc = nlp(text)
lemmas = " ".join([token.lemma_ for token in doc if not token.is_stop and not token.is_punct])
return {"raw": text, "lemmas": lemmas, "entities": [(e.text, e.label_) for e in doc.ents]}
# SCHICHT 2: scikit-learn — Kategorisierung
# Trainiert auf historischen Tickets, läuft in <10ms pro Ticket
# Output: Kategorie (Versand, Reklamation, etc.)
classifier = Pipeline([
("tfidf", TfidfVectorizer(sublinear_tf=True, max_df=0.8, min_df=2)),
("clf", LinearSVC(C=1.0, class_weight="balanced")),
])
# classifier.fit(X_train, y_train) — in Produktion einmalig geladen
# classifier.predict(["Mein Paket ist weg"]) → "Versand"
# SCHICHT 3: Hugging Face — Feine Entity-Extraktion
# Läuft auf GPU für bessere Performance
# Output: Entities (Bestellnummern, Daten, Beträge)
ner = hf_pipeline(
"ner",
model="dbmdz/bert-large-cased-finetuned-conll03-german",
aggregation_strategy="simple",
device=0, # GPU
)
# SCHICHT 4: PydanticAI — Strukturierte Extraktion
# Nur für komplexe Fälle (Fallback wenn NER unsicher)
# Output: Typsicheres Extraktions-Objekt
class ComplexExtraction(BaseModel):
bestellnummer: str | None
grund: str = Field(description="Der Grund der Anfrage in einem Satz")
priority: int = Field(description="Priorität 1-5", ge=1, le=5)
extraction_agent = Agent("openai:gpt-4o-mini", result_type=ComplexExtraction)
# SCHICHT 5: LangGraph — Workflow-Orchestrierung
# Verbindet alle Schichten zu einem ausführbaren Workflow
class PipelineState(TypedDict):
text: str; lemmas: str; category: str; entities: dict; response: str
def run_pipeline(text: str):
# spaCy
pre = preprocess(text)
# scikit-learn
cat = classifier.predict([pre["lemmas"]])[0]
# Hugging Face
entities = ner(text)
# PydanticAI (nur wenn nötig)
if any(e["score"] < 0.7 for e in entities):
extra = extraction_agent.run_sync(text).data
# Fallback: LLM-Extraktion für unsichere Entities
# Antwort generieren
response = f"Kategorie: {cat}. Bearbeitung läuft."
return {"category": cat, "entities": entities, "response": response}
# Beispiel-Durchlauf
print("=== Produktions-Pipeline ===")
result = run_pipeline("Meine Bestellung 4829103 kam nicht an, 89,95 € bezahlt.")
for k, v in result.items():
print(f"{k}: {v}")
Fazit
| Tool | Stärke | Einsatz |
|---|---|---|
| spaCy | Schnelle Vorverarbeitung | Tokenisierung, POS, Lemma, Basis-NER |
| scikit-learn | Machine Learning | Kategorisierung, Sentiment, Embeddings |
| Hugging Face | Deep Learning NER | Entity-Extraktion, QA, Textklassifikation |
| PydanticAI | LLM-gesteuerte Extraktion | Strukturierte Daten aus Freitext |
| LangGraph | Workflow-Orchestrierung | Verzweigte Pipelines mit State |
| LangChain | LLM-Chaining | RAG, Tool-Nutzung, Konversation |
Die Kunst ist nicht, EIN Tool für alles zu nehmen. Es ist, jedes Tool dort einzusetzen, wo es billiger, schneller oder genauer ist als die Alternative.
spaCy + scikit-learn fangen 80% der Fälle ab. Hugging Face bringt 15% mehr. PydanticAI als LLM-Fallback liefert die letzten 5%. LangGraph verbindet alles. Die Kosten sind ~5 € statt 250 € pro 10.000 Tickets.
Comments
Leave a comment