Retrieval-Augmented Generation: Intelligente Dokumentensuche für Unternehmen
RAG kombiniert Vektor-Suche mit generativer KI, um präzise Antworten aus privaten Dokumenten-Repositories zu liefern. Das System holt relevante Inhalte aus PDFs, Excel-Dateien, Code und mehr und generiert Erklärungen ausschließlich basierend auf den Quellmaterialien – was die Wahrscheinlichkeit von Halluzinationen deutlich reduziert.
Der Prozess beginnt mit der Vorverarbeitung: Dokumente werden analysiert, in intelligente Abschnitte unterteilt, wobei der Inhaltstyp berücksichtigt wird, und in Embeddings umgewandelt, die in einer Vektordatenbank gespeichert werden.
Textextraktion aus Dateiformaten
Das System unterstützt die Verarbeitung mehrerer Formate ohne Datenverlust. Jedes Format nutzt einen spezialisierten Extraktor.
SUPPORTED = {
'.pdf': lambda f: ''.join(p.extract_text() or '' for p in PyPDF2.PdfReader(f).pages),
'.docx': lambda f: '\n'.join(p.text for p in Document(f).paragraphs),
'.pptx': lambda f: '\n'.join(
shape.text
for slide in Presentation(f).slides
for shape in slide.shapes
if hasattr(shape, "text") and shape.text.strip()
),
'.xlsx': extract_xlsx,
'.csv': extract_csv,
# .txt, .json, .py, .js, etc.
}
Ausgabe ist sauberer, verarbeitbarer Text.
Intelligente Aufteilung nach Inhaltstyp
Text wird nicht nach festen Größen geteilt – stattdessen erfolgt eine semantische Segmentierung. Zuerst wird der Inhaltstyp erkannt:
def detect_content_type(text):
if re.search(r'<table|</td>|<tr>', text, re.IGNORECASE):
return 'table'
tab_lines = sum(1 for line in text.splitlines() if '\t' in line)
if tab_lines > max(3, len(text.splitlines()) * 0.4):
return 'table'
if re.search(r'function\s+\w+\s*\(|\bdef\s+\w+\s*\(|\bclass\s+\w+\b', text):
return 'code'
return 'prose'
- Tabellen: Wiederholte Überschriften in jedem Abschnitt
- Code: Aufteilung nach Funktionen/Klassen ohne logische Unterbrechung
- Prosa: Semantische Grenzen mittels Satz-Embeddings
Für Tabellen:
def split_table_by_rows(text, chunk_size):
lines = [l for l in text.splitlines() if l.strip()]
header = lines[0]
chunks = []
cur_lines = [header]
for line in lines[1:]:
if sum(len(l) for l in cur_lines) + len(line) >= chunk_size:
chunks.append('\n'.join(cur_lines))
cur_lines = [header, line]
else:
cur_lines.append(line)
if cur_lines:
chunks.append('\n'.join(cur_lines))
return chunks
Die semantische Aufteilung nutzt die Kosinus-Ähnlichkeit zwischen benachbarten Sätzen mit einem adaptiven Schwellwert (25. Perzentil).
Embedding-Erzeugung
Abschnitte werden mit Modellen wie text-embedding-3-small (1536 Dimensionen) vektorisiert. Vektoren und Metadaten werden in der Datenbank gespeichert.
def get_embeddings(texts, batch_size=100):
embeddings = []
for i in range(0, len(texts), batch_size):
resp = requests.post(
"https://openrouter.ai/api/v1/embeddings",
headers=HEADERS,
json={
"model": "openai/text-embedding-3-small",
"input": texts[i:i + batch_size],
"encoding_format": "float"
},
).json()
embeddings.extend(d["embedding"] for d in resp.get("data", []))
return np.array(embeddings)
Mehrstufige Suche
Die Abfrage wird in einen Vektor umgewandelt, mithilfe einer LLM in drei Varianten umformuliert und dann über eine hybride Suche verarbeitet.
Hybride Suche: Vektoren + BM25
- Vektor-Suche: Kosinus-Ähnlichkeit im Embedding-Raum
- BM25: TF-IDF mit k1=1.5, b=0.75 für exakte Treffer
Ergebnisse werden mittels RRF fusioniert:
# RRF-Fusion
for rank, idx in enumerate(sorted_by_vector):
rrf_scores[idx] += 1.0 / (k_rrf + rank)
for rank, idx in enumerate(sorted_by_bm25):
rrf_scores[idx] += bm25_normalized[idx] * (1.0 / (k_rrf + rank))
Nachrangung und Filter
Die Top-50-Kandidaten werden durch eine LLM bewertet, wobei 5–10 beste ausgewählt werden. Erzwungene Einbeziehung:
- Zitate in Anführungszeichen
- Zahlen/IDs (Regex: \b(№|N|number)\s*\d+\b)
quote_matches = re.findall(r'«([^»]+)»|"([^"+])"', query)
for match in quote_matches:
quote = (match[0] or match[1]).strip()
if len(quote) > 5:
for i, chunk in enumerate(chunks_data):
if quote in chunk['content']:
forced_indices.append(i)
break
Leistungssteigerungen
Hierarchische Suche
Zuerst werden die relevantesten Dokumente über Annotationen identifiziert; danach werden die Abschnitte innerhalb dieser Dokumente durchsucht.
doc_summaries = [chunks_data_by_doc[doc_id][0]['content'][:500] for doc_id in all_doc_ids]
doc_embs = get_embeddings(doc_summaries)
sims = np.dot(doc_embs_normalized, query_embedding_normalized)
top_doc_ids = [all_doc_ids[i] for i in np.argsort(sims)[::-1][:5]]
chunks_data = [ch for ch in chunks_data if ch['doc_id'] in top_doc_ids]
Iterative Suche
Bei komplexen Abfragen werden mehrere Durchläufe mit Feedback der LLM zur Feinjustierung der Ergebnisse durchgeführt.
Wichtige Erkenntnisse
- RAG minimiert Halluzinationen, indem Antworten ausschließlich aus relevanten Abschnitten generiert werden
- Hybride Suche (Vektoren + BM25 + RRF) steigert die Genauigkeit um 20–30 % gegenüber rein vektorbasierter Suche
- Semantische Aufteilung bewahrt Kontext bei Tabellen und Code und reduziert Rauschen
- Hierarchischer Ansatz beschleunigt die Suche in großen Korpora (>10.000 Dokumente)
- Erzwungene Filter sorgen dafür, dass Fakten mit Zahlen oder zitierten Texten gefunden werden
— Editorial Team
Noch keine Kommentare.