Abfragen, abrufen und generieren
Sie schreiben die Query-Pipeline, die die Benutzerfrage einbettet, die Top-k-Chunks abruft, einen erweiterten Prompt formatiert, das LLM aufruft und eine Antwort mit Quellenangaben zurückgibt.
Abfragen, abrufen und generieren ist eine kostenlose AI Engineering Academy-Lektion auf CoddyKit. Dies ist Lektion 4 von 4. Du kannst die komplette Lektion unten kostenlos lesen – dann übst du sie direkt im Browser mit einem integrierten Code-Editor und einem KI-Tutor rund um die Uhr. Sie ist Teil des AI Engineering Academy-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der AI Engineering Academy-Kurs umfasst insgesamt 4 Lektionen.
Die Abfragepipeline: von Anfang bis Ende
Die Abfragepipeline ist die Online-Hälfte von RAG – der Code, der in Echtzeit ausgeführt wird, wenn eine Benutzerin oder ein Benutzer eine Frage stellt. Sie verbindet alle während der Indexierung erstellten Komponenten: das Embedding-Modell, den Vektorspeicher, die Prompt-Vorlage und das LLM. Eine gut implementierte Abfragepipeline wird bei den meisten Workloads in unter 500 ms abgeschlossen und erzeugt fundierte Antworten mit Quellenangaben. In dieser Lektion erstellen wir jeden Schritt von Grund auf.
Schritt 1: Die Benutzerabfrage embedden
Der erste Schritt besteht darin, die natürlichsprachliche Frage der Benutzerin oder des Benutzers mithilfe desselben Modells wie bei der Indexierung in ein Vektor-Embedding umzuwandeln. Dieses Embedding kodiert die semantische Bedeutung der Frage und wird im Vektorspeicher mit den Embeddings der Dokument-Chunks verglichen. Halten Sie diesen Schritt schnell: Verwenden Sie ein leichtgewichtiges Modell wie text-embedding-3-small und cachen Sie Embeddings für wiederholt identische Abfragen.
from openai import OpenAI
client = OpenAI()
def embed_query(question: str) -> list:
response = client.embeddings.create(
model='text-embedding-3-small',
input=[question]
)
return response.data[0].embedding
user_question = 'What is our remote work policy?'
query_vector = embed_query(user_question)
print(f'Query embedded: {len(query_vector)}-dim vector')Schritt 2: Die Top-K-Chunks abrufen
Senden Sie den Abfragevektor an den Vektorspeicher, um die K semantisch ähnlichsten Chunks zu finden. Die zurückgegebenen Treffer werden nach ihrem Kosinusähnlichkeitswert sortiert (typischerweise 0,0 bis 1,0; höher ist besser). Der ideale K-Wert schafft ein Gleichgewicht zwischen reichhaltigem Kontext und den Kosten für das Kontextfenster: K=5 ist ein häufiger Ausgangspunkt. Sie können hier auch Metadatenfilter anwenden, um den Abruf auf eine bestimmte Abteilung, einen Dokumenttyp oder einen Datumsbereich zu beschränken.
def retrieve_chunks(query_vector, index, top_k=5, filters=None):
query_params = {
'vector': query_vector,
'top_k': top_k,
'include_metadata': True
}
if filters:
query_params['filter'] = filters
results = index.query(**query_params)
chunks = []
for match in results.matches:
chunks.append({
'score': match.score,
'text': match.metadata['text'],
'source': match.metadata.get('source', ''),
'page': match.metadata.get('page', '')
})
return chunksSchritt 3: Nach einem Schwellenwert filtern
Nicht alle abgerufenen Chunks sind tatsächlich relevant – einige können niedrige Ähnlichkeitswerte aufweisen und trotzdem unter den Top-K landen, weil die Abfrage außerhalb der Abdeckung des Index liegt. Wenden Sie einen Mindestschwellenwert für den Score an, um Treffer mit geringer Konfidenz herauszufiltern. Wenn alle abgerufenen Chunks unter diesem Schwellenwert liegen, geben Sie eine Antwort wie „Keine Informationen gefunden“ zurück, anstatt irrelevanten Kontext an das LLM zu senden. Dieses würde sonst eine schlechtere Antwort erzeugen, als wenn es die Anfrage höflich ablehnt.
MIN_SCORE_THRESHOLD = 0.75
def filter_by_score(chunks, threshold=MIN_SCORE_THRESHOLD):
relevant = [c for c in chunks if c['score'] >= threshold]
if not relevant:
print(f'No chunks above threshold {threshold}. Scores: {[c["score"] for c in chunks]}')
return relevant
retrieved = retrieve_chunks(query_vector, index, top_k=5)
filtered = filter_by_score(retrieved)
if not filtered:
print('Responding: no relevant information found')Schritt 4: Den Kontextblock formatieren
Fügen Sie die abgerufenen Chunks zu einem strukturierten Kontextblock zusammen, den das LLM lesen wird. Kennzeichnen Sie jeden Chunk mit seiner Quelle, damit das Modell sie korrekt zitieren kann. Fügen Sie zur besseren Übersicht Trennzeichen zwischen den Chunks ein. Halten Sie den gesamten Kontext innerhalb Ihres Token-Budgets: Zählen Sie die Token mit tiktoken und kürzen oder entfernen Sie Chunks mit niedrigerem Score, wenn Sie das Limit überschreiten. Der Kontextblock wird im Prompt zwischen der Systemanweisung und der Benutzerfrage eingefügt.
def format_context(chunks):
parts = []
for i, chunk in enumerate(chunks, start=1):
source_label = chunk['source']
if chunk.get('page'):
source_label += f", page {chunk['page']}"
parts.append(
f'[Document {i} | Source: {source_label}]\n{chunk["text"]}'
)
return '\n\n---\n\n'.join(parts)
context = format_context(filtered)
print(f'Context block: {len(context)} characters')Schritt 5: Den erweiterten Prompt erstellen
Führen Sie den Kontextblock, die Systemanweisung und die Benutzerfrage im finalen Prompt zusammen. Die Systemnachricht weist das Modell an, nur den bereitgestellten Kontext zu verwenden und Quellen anzugeben. Die Benutzernachricht enthält den formatierten Kontext, gefolgt von der Frage. Diese klare Trennung verhindert, dass das Modell Kontextinhalt mit der Frage vermischt, und macht die Grenze zwischen abgerufenen Daten und Benutzereingabe eindeutig.
def build_prompt(question, context):
system_message = (
'You are a helpful assistant. Answer the question using ONLY '
'the information in the provided documents. '
'Cite the document number(s) used, like [Doc 1]. '
'If the documents do not contain the answer, say so.'
)
user_message = (
f'Documents:\n\n{context}\n\n'
f'Question: {question}'
)
return system_message, user_messageSchritt 6: Das LLM aufrufen und die Antwort abrufen
Senden Sie den zusammengestellten Prompt mithilfe der Chat Completions API an das LLM. Verwenden Sie für faktenbasierte Fragen und Antworten eine niedrige Temperatur (0,0 bis 0,3), um konsistente, fundierte Antworten zu erhalten. Höhere Temperaturen erzeugen kreativere Antworten, erhöhen jedoch das Risiko, dass das Modell Informationen hinzufügt, die nicht im Kontext enthalten sind. Verarbeiten Sie die Antwort und geben Sie sowohl den Antworttext als auch die abgerufenen Quellen zurück, damit Ihre Anwendung der Benutzerin oder dem Benutzer Quellenangaben anzeigen kann.
def generate_answer(question, context, sources):
system_msg, user_msg = build_prompt(question, context)
response = client.chat.completions.create(
model='gpt-4o',
temperature=0.1, # low temperature for factual Q&A
messages=[
{'role': 'system', 'content': system_msg},
{'role': 'user', 'content': user_msg}
]
)
answer = response.choices[0].message.content
return {
'answer': answer,
'sources': sources,
'tokens_used': response.usage.total_tokens
}Alles zusammenführen
Die vollständige Abfragepipeline führt diese Schritte nacheinander aus. Jeder Schritt ist eine reine Funktion, die Sie unabhängig testen können, und die Daten fließen sauber von einem Schritt zum nächsten. Wenn Sie bei jedem Schritt Protokollierung hinzufügen, wird die Pipeline beobachtbar: Sie sehen genau, welche Chunks abgerufen wurden, welchen Score sie hatten, wie der Kontext zusammengestellt wurde und wie viele Token verwendet wurden. Diese Transparenz ist für das Debugging und die Verbesserung der Suchqualität unerlässlich.
def answer_question(user_question, vector_index):
# Step 1: Embed query
q_vector = embed_query(user_question)
# Step 2: Retrieve
chunks = retrieve_chunks(q_vector, vector_index, top_k=5)
# Step 3: Filter low-confidence matches
chunks = filter_by_score(chunks, threshold=0.70)
if not chunks:
return {'answer': 'I do not have information about that topic.', 'sources': []}
# Step 4 & 5: Format and build prompt
context = format_context(chunks)
sources = [c['source'] for c in chunks]
# Step 6: Generate
return generate_answer(user_question, context, sources)Latenz optimieren
Die Abfragepipeline enthält zwei I/O-gebundene Schritte: den Embedding-Aufruf und den LLM-Aufruf. Führen Sie sie ohne unnötige Wartezeiten aus: Der Embedding-Aufruf ist schnell (<100 ms), der LLM-Aufruf langsam (500 ms–3 s). Um die wahrgenommene Latenz zu verringern, streamen Sie die LLM-Antwort, damit Token während ihrer Erzeugung erscheinen, anstatt auf die vollständige Antwort zu warten. Cachen Sie das Embedding wiederholt identischer Abfragen, um redundante API-Aufrufe zu vermeiden.
async def answer_question_streaming(question, index):
q_vector = embed_query(question)
chunks = retrieve_chunks(q_vector, index, top_k=5)
chunks = filter_by_score(chunks)
if not chunks:
yield 'I do not have information about that topic.'
return
context = format_context(chunks)
system_msg, user_msg = build_prompt(question, context)
stream = await client.chat.completions.create(
model='gpt-4o',
stream=True,
messages=[
{'role': 'system', 'content': system_msg},
{'role': 'user', 'content': user_msg}
]
)
async for chunk in stream:
delta = chunk.choices[0].delta.content or ''
yield deltaProtokollierung für Beobachtbarkeit
RAG-Pipelines im Produktivbetrieb benötigen strukturierte Protokollierung, damit Sie diagnostizieren können, wann der Abruf fehlschlägt oder das LLM eine schlechte Antwort liefert. Protokollieren Sie für jede Anfrage die Abfrage, die IDs und Scores der abgerufenen Chunks, die Anzahl der Kontext-Token, die Antwort und die Latenz. Speichern Sie diese Protokolle in einer Datenbank oder einer Observability-Plattform. Wenn Benutzerinnen oder Benutzer schlechte Antworten melden, können Sie die exakte Abfrage erneut ausführen und prüfen, welche Chunks abgerufen wurden und warum sie nicht ausreichten.
import time
import logging
import json
def answer_question_with_logging(question, index):
start = time.time()
q_vector = embed_query(question)
chunks = retrieve_chunks(q_vector, index, top_k=5)
chunks = filter_by_score(chunks)
context = format_context(chunks)
result = generate_answer(question, context, [c['source'] for c in chunks])
latency_ms = (time.time() - start) * 1000
log_entry = {
'question': question,
'num_chunks_retrieved': len(chunks),
'chunk_scores': [c['score'] for c in chunks],
'tokens_used': result.get('tokens_used'),
'latency_ms': round(latency_ms)
}
logging.info(json.dumps(log_entry))
return resultAbfrage-Embeddings cachen
Wenn Ihre Anwendung viele wiederholte oder nahezu identische Abfragen erhält – etwa bei FAQ-Bots, bei denen häufig dieselben Fragen gestellt werden –, ist das Caching von Abfrage-Embeddings eine einfache Optimierung mit großer Wirkung. Hashen Sie die Abfragezeichenfolge, prüfen Sie den Redis-Cache auf das zugehörige Embedding und rufen Sie die Embedding API nur bei einem Cache-Miss auf. Cache-Trefferraten von 30–60 % sind bei FAQ- und Support-Chatbots im Produktivbetrieb üblich. Dadurch lassen sich erhebliche API-Kosten vermeiden und 50–100 ms Latenz pro gecachter Abfrage einsparen.
import hashlib
import json
import redis
r = redis.Redis(host='localhost', port=6379)
EMBED_CACHE_TTL = 86400 # 24 hours
def embed_query_cached(question):
cache_key = 'embed:' + hashlib.sha256(question.encode()).hexdigest()
cached = r.get(cache_key)
if cached:
return json.loads(cached) # cache hit
# Cache miss: call the API
vector = embed_query(question)
r.setex(cache_key, EMBED_CACHE_TTL, json.dumps(vector))
return vectorKurztest
Testen Sie Ihr Verständnis der Konzepte aus dem Bereich AI Engineering, die in dieser Lektion behandelt wurden.
Zusammenfassung der Lektion
In dieser Lektion haben Sie Folgendes gelernt: die sechsstufige Abfragepipeline (Abfrage embedden, Chunks abrufen, nach Score filtern, Kontext formatieren, Prompt erstellen, Antwort generieren), das Filtern anhand eines Score-Schwellenwerts für Abfragen außerhalb der Indexabdeckung sowie Verbesserungen für den Produktivbetrieb, darunter gestreamte Antworten, strukturierte Protokollierung und Latenzoptimierung. Als Nächstes lernen wir, wie Sie überprüfen, ob Ihr vollständiges RAG-System tatsächlich korrekt funktioniert.
Häufig gestellte Fragen
Ist die Lektion „Abfragen, abrufen und generieren“ kostenlos?
Ja — der vollständige Text von „Abfragen, abrufen und generieren“ ist hier im Web kostenlos zu lesen. Um sie interaktiv zu üben (integrierter Code-Editor und 24/7 KI-Tutor) und den Rest des AI Engineering Academy-Kurses freizuschalten, upgrade auf CoddyKit PRO. Der AI Engineering Academy-Kurs umfasst insgesamt 4 Lektionen.
Was lerne ich in „Abfragen, abrufen und generieren“?
Sie schreiben die Query-Pipeline, die die Benutzerfrage einbettet, die Top-k-Chunks abruft, einen erweiterten Prompt formatiert, das LLM aufruft und eine Antwort mit Quellenangaben zurückgibt. Du übst AI Engineering Academy mit praktischem Code, den du direkt im Browser ausführst, und ein 24/7 KI-Tutor beantwortet deine Fragen während du die Lektion bearbeitest.
Brauche ich Erfahrung, um AI Engineering Academy zu starten?
Keine Vorkenntnisse erforderlich. AI Engineering Academy auf CoddyKit ist für Anfänger bis fortgeschrittene Lernende strukturiert, sodass du hier starten oder von Anfang an beginnen und in deinem eigenen Tempo voranschreiten kannst. Dies ist Lektion 4 von 4.
Wie lange dauert die Lektion „Abfragen, abrufen und generieren“?
Die meisten CoddyKit-Lektionen dauern etwa 5–10 Minuten. Jede ist kompakt und interaktiv, sodass du stetig Fortschritte machst und genau dort weitermachst, wo du aufgehört hast – im Web und in der App.
Kann ich in dieser AI Engineering Academy-Lektion Code schreiben und ausführen?
Ja. Jede AI Engineering Academy-Lektion enthält einen integrierten Code-Editor, sodass du echten Code direkt in deinem Browser schreibst und ausführst und sofort KI-Feedback erhältst — ohne lokale Einrichtung erforderlich.
Alle Lektionen in diesem Kurs
- Dokumente laden und Text extrahieren
- Chunking-Strategien: fest, satzbasiert oder rekursiv
- Indexierung: Chunks einbetten und speichern
- Abfragen, abrufen und generieren