In den vorangegangenen Tagen lag der Fokus auf der Konstruktion von Modulen, der Integration von Retrieval-Systemen und der Optimierung durch Teleprompter. An Tag 25 und 26 wird der entscheidende Schritt zum Produkt behandelt: Die Kompilierung des DSPy-Programms in ein statisches Artefakt und dessen Speicherung.
Ziel dieses Beitrags ist es, den compile-Prozess theoretisch einzuordnen und praktisch anhand eines RAG-Systems (Retrieval Augmented Generation) umzusetzen. Dabei wird demonstriert, wie ein optimierter Zustand gespeichert und in einer neuen Umgebung wiederhergestellt wird.
Der Kompilierungsprozess in DSPy
In der klassischen Softwareentwicklung übersetzt ein Compiler menschenlesbaren Code in maschinenlesbare Anweisungen. In DSPy bezieht sich der Begriff „Kompilierung“ auf den Prozess, bei dem ein deklaratives Modul (z. B. dspy.ChainOfThought) mithilfe des Teleprompters und eines Trainingsdatensatzes in eine optimierte Version transformiert wird. Das Ergebnis dieser Kompilierung ist nicht Binärcode, sondern ein Satz von Parametern, der im Modul hinterlegt wird. Dazu gehören verfeinerte System-Prompts oder Aufgabenbeschreibungen und ausgewählte Beispiele aus den Trainingsdaten, die dem Modell helfen, das gewünschte Verhalten (Input → Output) und Format einzuhalten.
Ein kompiliertes DSPy-Programm ist ein in sich geschlossenes Objekt, das ohne erneuten Zugriff auf den Optimizer oder die Trainingsdaten ausgeführt werden kann. Dies ist die Grundvoraussetzung für den Einsatz in Produktionsumgebungen, da es deterministischeres Verhalten und oft höhere Qualität bei geringerer Latenz ermöglicht.
Implementierung und Kompilierung des RAG-Systems
Im Folgenden wird ein RAG-System unter Verwendung lokaler Embeddings, Sprachmodelle und der Vektordatenbank Qdrant aufgesetzt, kompiliert und anschließend für die Produktion exportiert.
Initialisierung der Umgebung
Zunächst erfolgt die Konfiguration der lokalen Modelle. Es wird angenommen, dass entsprechende Server für das Embedding und das LLM lokal verfügbar sind. Ich verwende das qwen3-embedding:0.6b in einer Ollama Instanz und das unsloth/gemma-3-4b-it-GGUF:Q4_K_M LLM das dirket in einem llama.cpp Server mit einer Kontext-Länge von 64k läuft.
import dspy
from datasets import load_dataset
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
# Konfiguration der lokalen Modelle
embedder = dspy.Embedder(
"openai/qwen3-embedding:0.6b",
api_base="http://localhost:11434/v1",
api_key="no_key_needed",
batch_size=100
)
local_llm = dspy.LM(
"openai/unsloth/gemma-3-4b-it-GGUF:Q4_K_M",
api_base="http://localhost:8080/v1",
api_key="no_key_needed",
temperature=0.1,
cache=False,
)
dspy.configure(lm=local_llm, embedder=embedder)
Vorbereitung der Datenbasis (Indizierung)
Für das RAG-System wird ein Wikipedia-Datensatz geladen, in Textsegmente (Chunks) unterteilt und in einer Qdrant-Collection indexiert. Dieser Schritt stellt die Wissensbasis dar, auf die das Modell zugreift. Der verwendete Datensatz „https://huggingface.co/datasets/embedding-data/simple-wiki“ enthält Paare äquivalenter Sätze aus Wikipedia die für eine semantische Suche und Ähnlichkeit von Sätzen genutzt werden kann.
# Laden und Vorbereiten der Dokumente
dataset = load_dataset("embedding-data/simple-wiki", split="train[:1000]")
documents = [" ".join(doc['set']) for doc in dataset]
# Chunking der Dokumente
chunk_size = 1024
all_chunks = []
for doc in documents:
for i in range(0, len(doc), chunk_size):
chunk = doc[i:i+chunk_size]
if len(chunk) > 100:
all_chunks.append(chunk)
# Initialisierung des Qdrant Clients
client = QdrantClient(host="localhost", port=6333)
collection_name = "simple_wiki_rag"
embedding_dim = 1024
# Erstellen der Collection
if not client.collection_exists(collection_name):
client.create_collection(
collection_name=collection_name,
vectors_config=VectorParams(size=embedding_dim, distance=Distance.COSINE)
)
# Embedden und Hochladen der Daten
embeddings = embedder(all_chunks)
points = [
PointStruct(id=i, vector=vec, payload={"text": chunk})
for i, (chunk, vec) in enumerate(zip(all_chunks, embeddings))
]
batch_size = 100
for i in range(0, len(points), batch_size):
client.upsert(collection_name=collection_name, points=points[i:i+batch_size])
print(f"{len(points)} Chunks indexiert.")
Definition des Retrievers und des RAG-Moduls
Um DSPy mit der Qdrant-Datenbank zu verbinden, ist ein benutzerdefinierter Retriever (dspy.Retrieve) erforderlich. Anschließend wird die eigentliche RAG-Klasse definiert.
class QdrantRetriever(dspy.Retrieve):
def __init__(self, client, collection_name, embedder, k=3):
self._client = client
self._collection_name = collection_name
self._embedder = embedder
self._k = k
super().__init__()
def forward(self, query_or_queries, k=None):
k = k if k is not None else self._k
query_embeddings = self._embedder(query_or_queries)
results = [
self._client.query_points(
collection_name=self._collection_name,
query=query_embeddings,
limit=k,
) for emb in query_embeddings
]
# Extraktion des Textes aus dem Payload
passages = [dspy.Prediction(long_text=p.payload["text"]) for p in results[0].points]
return passages
# Konfiguration des Retrievers im DSPy-Settings-Objekt
rm = QdrantRetriever(client, collection_name, embedder)
dspy.settings.configure(rm=rm)
# Definition der RAG-Logik
class RAG(dspy.Module):
def __init__(self, num_passages=3):
super().__init__()
self.retrieve = dspy.Retrieve(k=num_passages)
self.generate_answer = dspy.ChainOfThought("context, question -> answer")
def forward(self, question):
context = self.retrieve(question).passages
prediction = self.generate_answer(context=context, question=question)
return dspy.Prediction(context=context, answer=prediction.answer)
Kompilierung mit BootstrapFewShot
Der Kernschritt ist die Optimierung. Hierbei wird der BootstrapFewShot-Teleprompter verwendet. Dieser generiert anhand eines Trainingsdatensatzes (hier HotPotQA) optimale Beispiele, die dem Prompt hinzugefügt werden, um die Antwortqualität zu steigern.
from dspy.datasets.hotpotqa import HotPotQA
from dspy.teleprompt import BootstrapFewShot
from dspy.evaluate import answer_exact_match
# Datensatz laden
dataset = HotPotQA(train_seed=1, train_size=20, eval_seed=42, dev_size=20, test_size=0)
trainset = [x.with_inputs('question') for x in dataset.train]
devset = [x.with_inputs('question') for x in dataset.dev]
# Metrik definieren
def validate_context_and_answer(example, pred, trace=None):
return dspy.evaluate.answer_exact_match(example, pred)
# Optimizer konfigurieren
teleprompter = BootstrapFewShot(
metric=validate_context_and_answer,
max_bootstrapped_demos=4,
max_labeled_demos=4
)
# Kompilierung durchführen
print("Starte Kompilierung...")
compiled_rag = teleprompter.compile(student=RAG(), trainset=trainset)
print("Kompilierung abgeschlossen.")
Evaluation: Unkompiliert vs. Kompiliert
Um den Effekt der Kompilierung zu messen, wird die Performance des unoptimierten Modells (Zero-Shot) mit der des kompilierten Modells (Few-Shot Optimized) verglichen.
from dspy.evaluate import Evaluate
evaluator = Evaluate(devset=devset, metric=answer_exact_match, num_threads=1, display_progress=True)
print("Evaluation: Unkompiliertes RAG")
evaluator(RAG())
print("Evaluation: Kompiliertes RAG")
evaluator(compiled_rag)
Ergebnis-Analyse:
In dem durchgeführten Testlauf erreichte das unkompilierte RAG-System einen Score von 0.0%. Dies deutet darauf hin, dass das Modell ohne Beispiele Schwierigkeiten hatte, das exakte Ausgabeformat oder den Kontext korrekt zu verarbeiten. Nach der Kompilierung stieg der Score auf 15.0%. Dies zeigt, dass durch das Hinzufügen von gebootstrappten Demonstrationen (Beispielen im Prompt) die Fähigkeit des Modells, korrekte Antworten im erwarteten Format zu liefern, signifikant gesteigert wurde.
6. Speichern und Laden für die Produktion
Für den produktiven Einsatz muss der Zustand des kompilierten Programms gespeichert werden. Dies verhindert, dass der rechenintensive Kompilierungsprozess bei jedem Neustart der Anwendung wiederholt werden muss.
Hinweis: In einigen Versionen von DSPy kann ein Patch der dump_state-Methode für benutzerdefinierte Retriever notwendig sein, um Serialisierungsfehler zu vermeiden.
# Workaround für Serialisierung (falls notwendig)
original_dump_state = dspy.Retrieve.dump_state
def patched_dump_state(self, json_mode=True):
return original_dump_state(self)
dspy.Retrieve.dump_state = patched_dump_state
# Speichern des Zustands
compiled_rag.save("compiled_rag_v1.json")
# Simulation: Produktionsumgebung (Neustart)
production_rag = RAG()
production_rag.load("compiled_rag_v1.json")
# Validierung des geladenen Modells
print("Evaluation: Produktion RAG (geladen)")
evaluator(production_rag)
Das erneute Testen des geladenen production_rag bestätigt, dass die Performance identisch mit dem frisch kompilierten Modell ist (15.0%). Die Konfigurationen und gelernten Prompts wurden erfolgreich wiederhergestellt.
Die erstellte JSON Datei hat folgenden Inhalt:
{
"retrieve": {
"k": 3
},
"generate_answer.predict": {
"traces": [],
"train": [],
"demos": [
{
"question": "Which magazine has published articles by Scott Shaw, Tae Kwon Do Times or Southwest Art?",
"answer": "Tae Kwon Do Times",
"dspy_uuid": "7ac8f15a-6610-429f-9cf7-d319789ea7cd",
"dspy_split": "train"
},
{
"question": "This American guitarist best known for her work with the Iron Maidens is an ancestor of a composer who was known as what?",
"answer": "The Waltz King",
"dspy_uuid": "c6cf2780-0be6-4135-a57c-0c120dd0c3f5",
"dspy_split": "train"
},
{
"question": "On the coast of what ocean is the birthplace of Diogal Sakho?",
"answer": "Atlantic",
"dspy_uuid": "e7c4eb66-7d10-4b57-a4f4-a0a5c8dfdb76",
"dspy_split": "train"
},
{
"question": "The Victorians - Their Story In Pictures is a documentary series written by an author born in what year?",
"answer": "1950",
"dspy_uuid": "b2bf13ff-2b91-4db7-9c07-142477ac5f3f",
"dspy_split": "train"
}
],
"signature": {
"instructions": "Given the fields `context`, `question`, produce the fields `answer`.",
"fields": [
{
"prefix": "Context:",
"description": "${context}"
},
{
"prefix": "Question:",
"description": "${question}"
},
{
"prefix": "Reasoning: Let's think step by step in order to",
"description": "${reasoning}"
},
{
"prefix": "Answer:",
"description": "${answer}"
}
]
},
"lm": null
},
"metadata": {
"dependency_versions": {
"python": "3.12",
"dspy": "3.0.4",
"cloudpickle": "3.1"
}
}
}
Die JSON Datei repräsentiert den gespeicherten Zustand eines optimierten DSPy-Programms (in diesem Fall einer RAG-Pipeline), das kompiliert wurde.
Die Datei speichert im Wesentlichen die „gelernten“ Parameter – bei DSPy sind das vor allem die Few-Shot-Beispiele (Demos) und die Konfiguration der Module, damit das Programm später exakt so wiederhergestellt werden kann.
Die Struktur gliedert sich in drei Hauptbereiche:
retrieve (Konfiguration des Retrievers)
Dieser Block definiert die Einstellungen für das Informationsabruf-Modul (Retriever).
k: 3
Der Parameter besagt, dass bei jeder Anfrage genau 3 relevante Passagen (Kontexte) aus der Wissensdatenbank abgerufen und an das Sprachmodell übergeben werden sollen.
generate_answer.predict (Das Vorhersage-Modul)
Dieser Block enthält die Konfiguration für das Modul, das die Antwort generiert.
demos(Demonstrationen / Few-Shot Examples)
Das ist das Ergebnis descompile-Prozesses. Der DSPy-Optimizer (Teleprompter) hat aus den Trainingsdaten diese 4 spezifischen Beispiele ausgewählt, weil sie dem Modell am besten helfen, korrekte Antworten zu generieren. Wenn das Programm geladen wird, werden diese Beispiele in den Prompt eingefügt („Few-Shot Prompting“).- Jedes Beispiel enthält
question
Die Eingabefrage (z. B. nach „Scott Shaw“ oder „Diogal Sakho“).answer
Die korrekte Antwort („Tae Kwon Do Times“, „Atlantic“).dspy_uuid&dspy_split
Metadaten zur Nachverfolgung der ursprünglichen Datenpunkte.
- Jedes Beispiel enthält
signature(Die Signatur)
Definiert die Schnittstelle des Moduls (Input/Output-Struktur).instructions
Die Systemanweisung an das LLM: „Given the fieldscontext,question, produce the fieldsanswer.“fields
Hier werden die erwarteten Bausteine des Prompts definiert:- Context
Der abgerufene Text (Input). - Question
Die Frage des Users (Input). - Reasoning
„Let’s think step by step…“ – Dies zeigt, dass hier Chain-of-Thought (CoT) verwendet wird. Das Modell soll erst nachdenken, bevor es antwortet. - Answer
Die finale Antwort (Output).
- Context
traces&train
Diese Listen sind hier leer ([]). Sie könnten Debugging-Informationen oder den gesamten Trainingsverlauf enthalten, werden aber für das reine Ausführen (Inference) des gespeicherten Modells oft nicht benötigt oder nicht mitgespeichert.lm
Steht aufnull. Das bedeutet, dass das spezifische Sprachmodell nicht fest in der Datei „hardcodiert“ ist. Beim Laden der Datei wird das aktuell im Python-Skript konfigurierte Standard-LLM verwendet.
metadata
Dieser Block stellt sicher, dass die Umgebung, in der die Datei geladen wird, kompatibel mit der Umgebung ist, in der sie erstellt wurde.
python: „3.12“ (Python-Version)dspy: „3.0.4“ (Version der DSPy-Bibliothek – wichtig, da sich Formate ändern können)cloudpickle: „3.1“ (Bibliothek zum Serialisieren von Python-Objekten)
Wenn man die Datei mit compiled_rag.load(...) läd, baut DSPy die Pipeline genau so wieder auf, dass das LLM diese 4 Beispiele im Kontext erhält und die „Step-by-Step“-Logik anwendet, um basierend auf 3 abgerufenen Dokumenten (k=3) eine Antwort zu geben.
Zusammenfassung
Die Kompilierung ist der Brückenschlag zwischen experimentellem Prototyping und verlässlicher Anwendung. Durch die Nutzung von BootstrapFewShot wurde ein RAG-System von einer nicht funktionsfähigen Basis (0%) auf einen funktionierenden Zustand (15%) gehoben, ohne den Programmcode manuell zu ändern. Durch save und load lässt sich dieses optimierte Verhalten dauerhaft speichern und portieren.
