
Implementierung einer Custom RAG-Pipeline für technische Dokumente


Retrieval technischer Dokumente mit Qdrant, PyMuPDF und DSPy
Viele unserer Kunden verwalten umfangreiche Bestände an technischen Dokumenten für Maschinenanlagen, die detaillierte Betriebsanleitungen und umfassende Nutzungsrichtlinien enthalten. Diese Dokumente sind für den täglichen Betrieb kritisch, doch ihre Länge und Komplexität machen die manuelle Suche nach spezifischen Informationen mühsam und zeitaufwendig.
Standard-RAG-Lösungen (Out-of-the-box) stoßen oft an ihre Grenzen, wenn es um die erforderliche Ergebnisqualität geht. Sie sind typischerweise für eine breite Palette von Dokumententypen konzipiert und lassen oft das spezialisierte Domänenwissen vermissen. Maßgeschneiderte Lösungen, die Open-Source-Tools nutzen und auf die spezifischen Anforderungen der Domäne zugeschnitten sind, liefern in der Regel signifikant bessere Ergebnisse.
Technische Dokumentationen enthalten oft Bilder, Diagramme und Tabellen, die für das Verständnis des Gesamtkontextes unerlässlich sind. Während rein textbasierte Suchen in Vektordatenbanken relevante Passagen finden können, übersehen sie häufig den Kontext, den diese visuellen Elemente liefern.
Einführung
In diesem Artikel untersuchen wir den Einsatz von Qdrant für die effiziente Speicherung und das Retrieval von Embeddings. Wir integrieren gpt4o-mini für Optical Character Recognition (OCR), um zusätzlichen Kontext aus Bildern zu extrahieren, und führen unsere Datenbasis in DSPy als unsere Retrieval-Augmented Generation (RAG) Pipeline zusammen.
Warum Qdrant?
Bei der Entwicklung von Produktivanwendungen ist Geschwindigkeit entscheidend. Durch die Nutzung der Hierarchical Navigable Small World (HNSW) Indexierungstechnik gewährleistet Qdrant schnelle Suchzeiten, selbst wenn das Datenvolumen signifikant ansteigt. HNSW optimiert den Suchprozess durch eine mehrschichtige Graphenstruktur, was die Zeitkomplexität beim Auffinden ähnlicher Vektoren reduziert. Dies ermöglicht es Qdrant, effizient auf Millionen von Vektoren zu skalieren und dabei schnelle Antwortzeiten beizubehalten.
Qdrant steigert die Performance zusätzlich durch fortschrittliche Methoden wie Vektorquantisierung, die den Speicherbedarf minimieren, ohne die Geschwindigkeit zu beeinträchtigen. Tatsächlich erreicht Qdrant einen bis zu 15-fach höheren Query-Durchsatz im Vergleich zu pgvector – und das bei hoher Genauigkeit. Diese Ergebnisse unterstreichen, warum Qdrant bei vielen Organisationen, die effiziente generative KI-Anwendungen bauen, so beliebt ist.
Wenn es um Retrieval-Systeme geht, müssen wir jedoch auch ein Embedding-Modell wählen, das zu unserer Aufgabe passt.
Wie wähle ich das richtige Embedding-Modell?
Ein hervorragender Startpunkt ist das Massive Text Embedding Benchmark (MTEB) Leaderboard. Es bietet einen Überblick über Embedding-Modelle, einschließlich proprietärer und Open-Source-Optionen.
Das Leaderboard bewertet Modelle basierend auf ihrer Leistung in verschiedenen Aufgabenbereichen wie Reranking, Summarization und Retrieval. Indem Sie ein Modell wählen, das über diese Aufgaben hinweg gut abschneidet, stellen Sie sicher, dass Ihre Embeddings sowohl präzise als auch vielseitig einsetzbar sind.

Für unsere Aufgabe haben wir uns für das Modell text-embedding-3-large von OpenAI entschieden, das sich hervorragend für unseren Anwendungsfall des Dokumenten-Retrievals eignet. Es liefert qualitativ hochwertige Embeddings und lässt sich über die Embeddings API einfach integrieren.
Schauen wir uns nun an, wie all diese Komponenten zusammenwirken.
Voraussetzungen
- Installation der Abhängigkeiten:
1# Databases2pip install qdrant-client fastembed pymongo34# PDF processing5pip install pymupdf pymupdf4llm67# Text embedding8pip install openai tiktoken910# Utilities11pip install python-dotenv uuid6 tqdm
- Qdrant-Setup: Zunächst richten wir den Qdrant-Server ein. Hierfür können wir das offizielle Docker-Image verwenden, das von Qdrant bereitgestellt wird.
- MongoDB-Setup: Wir richten MongoDB als unseren Bildspeicher ein.
Wir erstellen dazu die folgende docker-compose.yml:
1services:2 qdrant:3 container_name: qdrant4 image: qdrant/qdrant:latest5 ports:6 - "6333:6333"7 - "6334:6334"8 env_file:9 - .env10 mongodb:11 container_name: mongodb12 image: mongo:latest13 hostname: mongodb14 ports:15 - "27017:27017"
- Umgebungsvariablen: Wir speichern die benötigten Umgebungsvariablen in einer .env-Datei, wie folgt:
1HOST=host.docker.internal2QDRANT_PORT=63333MONGODB_PORT=270174OPENAI_API_KEY=<your openai api key>
- Sobald Ihre Konfiguration steht, können wir sowohl die Qdrant- als auch die MongoDB-Container starten:
1docker-compose up -d --build
- Einrichtung der MongoDB-Klasse: Wir speichern unsere Bilder separat in einer MongoDB-Collection.
Ein multimodaler Ansatz ist zwar hilfreich, um sowohl Text als auch Bilder in einem Vektorraum zu erfassen, bietet aber möglicherweise nicht die exakte Übereinstimmung (Matching) zwischen beiden. Der Einfachheit halber und um die kontextuelle Zuordnung sicherzustellen, haben wir uns entschieden, alle Bilder separat in einer MongoDB-Collection zu speichern und sie über ihre UUID zu verknüpfen.
1import os23import pymongo4from bson.json_util import dumps, loads5from dotenv import load_dotenv6from pymongo.errors import DuplicateKeyError78load_dotenv()910class MongoDBWrapper:11 """Wrapper class for MongoDB Client."""1213 def __init__(self):14 self.client = pymongo.MongoClient(15 os.getenv("DOCKER_INTERNAL_HOST"), int(os.getenv("MONGODB_PORT"))16 )17 self.db_name = "my_pdfs"1819 self.db = self.client[self.db_name]20 self.images_collection = self.db["images"]2122 # Create compound index for machine and page23 self.images_collection.create_index({"machine": 1, "page": 1}, unique=True)2425 def upsert(26 self,27 uuid: str,28 image: str,29 machine: str,30 page: int,31 ) -> None:32 """Upsert data in MongoDB."""33 try:34 self.images_collection.update_one(35 {"id": uuid, "machine": machine, "page": page},36 {"$set": {"image": image}},37 upsert=True,38 )39 except DuplicateKeyError:40 pass
Schritt-für-Schritt-Anleitung: Dokumenten-Retrieval
- Einrichtung der Chunk-Klasse: Wir nutzen eine einfache Chunk-Klasse als strukturierten Container, um die mit PyMuPDF extrahierten Daten zu speichern und die Elemente zu organisieren, die wir aus den PDF-Dokumenten gewinnen.
1class Chunk:2 """Chunking Class for Qdrant to store data."""34 def __init__(5 self,6 texts: Optional[list[str]] = [],7 images: Optional[list[Image.Image]] = [],8 path: Optional[str] = None,910 ):11 self.texts = texts12 self.images = images13 self.path = path
- Einrichtung des Qdrant-Clients: Wir initialisieren den Qdrant-Client mit der folgenden Klasse:
1import os23from qdrant_client import QdrantClient45# Load environment variables6load_dotenv()78class QdrantWrapper:9 """Wrapper for Qdrant Client."""1011 def __init__(self):12 self.url = (13 f"http://{os.getenv('HOST')}:{os.getenv('QDRANT_PORT')}"14 )15 self.client = QdrantClient(url=self.url, timeout=10)16 self.embedding_model_name = "text-embedding-3-large"
- Dokumenten-Preprocessing: Wir verarbeiten unser Dokument vor, indem wir es mithilfe von PyMuPDF in Bilder und Text zerlegen (Chunking) und die Daten für das Embedding vorbereiten:
1import re2import logging34import fitz5from tqdm import tqdm6from PIL import Image78logging.basicConfig(9 level=logging.INFO, format="%(asctime)s - %(message)s", datefmt="%d-%b-%y %H:%M:%S"10)1112def chunk_document(13 self,14 file_path: str,15 dpi: str = 600,16 image_format: str = "png",17 table_strategy: str = "lines",18) -> list:19 """20 Extract text, tables and images from PDF file.2122 Args:23 file_path (str): Path to the PDF file.24 dpi (str): Resolution for image extraction (default: 600).25 image_format (str): Format for extracted images (default: png).26 table_strategy (str): Strategy for table extraction (default: lines).27 """28 chunks = []29 doc = fitz.open(file_path)3031 try:32 # Use PyMuPDF4LLM to read and convert PDF into markdown-like format33 md_reader = pymupdf4llm.to_markdown(34 doc,35 page_chunks=True,36 dpi=dpi,37 image_format=image_format,38 table_strategy=table_strategy,39 )4041 # Iterate through pages and extract text, tables, and images42 for i, page in tqdm(enumerate(doc), total=len(doc), desc=f"Scraping {file_path} with pymupdf4llm"):43 page_data = md_reader[i]44 text = page_data["text"]45 cleaned_text = re.sub(r"\n{3,}", "\n\n", text).strip()4647 pix = page.get_pixmap()48 img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)49 chunks.append(50 Chunk([cleaned_text], [img], file_path)51 )52 except Exception as e:53 logging.error(f"Failed to scrape {file_path} with pymupdf4llm. Falling back to PyMuPDF. Error: {e}")5455 # Fallback method using basic PyMuPDF56 for i in tqdm(range(len(doc)), desc=f"Scraping {file_path} with PyMuPDF"):57 page = doc[i]58 text = page.get_text()59 cleaned_text = re.sub(r"\n{3,}", "\n\n", text).strip()6061 pix = page.get_pixmap()62 img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)63 chunks.append(64 Chunk([cleaned_text], [img], file_path)65 )66 finally:67 doc.close()68 return chunks
1import io2import os34import tiktoken56def preprocess_chunks(self, chunks: list) -> tuple[list[str], list[str], list[bytes]]:7 """Preprocesses data chunks."""89 # Extract texts and images from chunks10 texts = [chunk.texts[0].strip().replace("\n \n", "").replace("\n", " ") for chunk in chunks]11 images = [chunk.images[0] for chunk in chunks]1213 def _convert_image_to_binary(image) -> bytes:14 """Convert an image to binary data (PNG format)."""15 buffer = io.BytesIO()16 image.save(buffer, format="PNG")17 binary_string = buffer.getvalue()18 buffer.close()19 return binary_string2021 def _num_tokens_from_string(string: str) -> int:22 """Calculate and validate the number of tokens in a text string."""23 encoding = tiktoken.get_encoding(24 tiktoken.encoding_for_model(self.embedding_model_name).name25 )26 num_tokens = len(encoding.encode(string))27 if num_tokens > 8192:28 raise ValueError(f"Input is too long ({num_tokens} > 8192)")29 return num_tokens3031 def _sanitize(text: str) -> str:32 """Sanitize and return non-empty text."""33 if not text:34 return " "35 return text3637 # Sanitize and validate texts38 sanitized_texts = []39 for text in texts:40 _num_tokens_from_string(text)41 sanitized_text = _sanitize(text)4243 if " \n" == sanitized_text:44 sanitized_text = "Empty string \n"4546 sanitized_texts.append(sanitized_text)4748 # Convert images to binary format49 binary_images = [_convert_image_to_binary(image) for image in images]50 return sanitized_texts, binary_images
- Embedding: Wir erstellen Embeddings der vorverarbeiteten Texte unter Verwendung des text-embedding-3-large Modells von OpenAI:
1import openai2from dotenv import load_dotenv3from qdrant_client.models import PointStruct4from uuid6 import uuid756load_dotenv()78openai_client = openai.Client(api_key=os.getenv("OPENAI_API_KEY"))910def embed_and_upload(self, texts: list, images: list, machine: str) -> list[PointStruct]:11 """Embeds the preprocessed texts and uploads images to MongoDB."""12 # Initialize MongoDB wrapper13 mongodb = MongoDBWrapper()1415 # Create embeddings using OpenAI's API16 embeddings_response = openai_client.embeddings.create(17 input=texts, model=self.embedding_model_name18 )1920 points = []21 for idx, (data, text, image) in enumerate(zip(embeddings_response.data, texts, images)):22 # Generate a UUID for each point23 uuid_str = uuid7().hex2425 # Calculate the page number26 page = idx + 12728 # Create a point structure for Qdrant29 point = PointStruct(30 id=uuid_str,31 vector=data.embedding,32 payload={"id": uuid_str, "page": page, "machine": machine, "text": text},33 )34 points.append(point)3536 # Upsert image data into MongoDB37 mongodb.upsert(uuid=uuid_str, image_data=str(image), machine=machine, page=page)38 return points
- Upload nach Qdrant: Für die Vektorparameter nutzen wir size=3072 und distance=Distance.COSINE. Um Timeout-Fehler zu vermeiden, laden wir die Embeddings in Paketen (Chunks) von maximal 200 Punkten pro Anfrage hoch:
1from qdrant_client.models import Distance, VectorParams23from qdrant_wrapper import QdrantWrapper45# Initialize Qdrant wrapper6qdrant = QdrantWrapper()7collection_name = "pdf-manuals"89# Create Qdrant collection10qdrant.client.create_collection(11 collection_name=collection_name,12 vectors_config=VectorParams(size=3072, distance=Distance.COSINE)13)1415# Get chunks16chunks = qdrant.chunk_document(file_path="MLC.pdf")1718# Preprocess chunks19texts, images = qdrant.preprocess_chunks(chunks)2021# Embed and upload embeddings22points = qdrant.embed_and_upload(texts, images, machine="MLC")2324# Upload embeddings to Qdrant25if len(points) >= 200:26 list_of_points = [points[i : i + 200] for i in range(0, len(points), 200)]2728 for point in list_of_points:29 qdrant.client.upsert(collection_name, point)30else:31 qdrant.client.upsert(collection_name, points)
- Suche in Qdrant: Wir wollen einen Query-Filter implementieren, um exakt nach der gesuchten Maschine zu filtern. Durch die Anwendung eines must-Filters stellen wir sicher, dass nur relevante technische Dokumente einbezogen werden, und verhindern effektiv Überschneidungen mit themenfremden PDF-Dokumenten:
1from qdrant_client import models23def search(4 self,5 collection_name: str,6 query: str,7 machine: str,8 limit: int = 10,9) -> list:10 """Search in Qdrant collection."""11 search_result = self.client.search(12 collection_name=collection_name,13 query_filter=models.Filter(14 must=[15 models.FieldCondition(16 key="machine",17 match=models.MatchValue(value=machine),18 )19 ]20 ),21 query_vector=openai_client.embeddings.create(22 input=[query], model=self.embedding_model_name23 )24 .data[0]25 .embedding,26 limit=limit,27 )28 return search_result2930# Initialize Qdrant wrapper31qdrant = QdrantWrapper()3233# Search example34search_result = qdrant.search(35 collection_name="pdf-manuals",36 query="The signal lamp of my MLC machine is blinking green. What does that mean?",37 machine="MLC"38)39top_result = search_result[0]
- Output: Wir erhalten das Top-Ergebnis der Suche:
1image = '<PIL.PngImagePlugin.PngImageFile image mode=RGB size=596x842 at 0x7FE5368C3E20>'2machine = 'MLC'3page = 44passage = '**1.1.1** **Schlüsselberechtigung** |Farbe|Anwender|Berechtigung / Funktion| |---|---|---| |grau (kein Schlüssel)|Weber (weaver)|Maschine in Produktion halten (minimale Geräteeinstellungen vornehmen, Produkti- onseinstellungen1) ansehen)| |blau|Einrichter (fitter)|Maschine mechanisch und textiltechnisch einrichten und verwalten (Geräte einstellen und Produktionseinstellungen1) vornehmen, Update und Diagnose durchführen, Zusatz- informationen ansehen)| |gelb|Vorgesetzter (supervisior)|Maschine statistisch und netzwerktechnisch einrichten und verwalten (Zeit und Schich- ten konfigurieren, Netzwerkeinstellungen vornehmen)| 1) Produktionseinstellungen = Auftrag, Artikel, Muster **1.2** **Signallampe** Die Maschine verfügt über folgende Störungsanzeigen: Multifunktions-Signalleuchte Störungsmeldungen MÜDATA-Display **RGB Multifunktions-Signalleuchte** Lampe leuchtet Lampe blink |Farbe|Muster|Fehler| |---|---|---| |dunkel||Maschine läuft| |weiss||System bereit, Stop| |weiss||Initialisierung läuft| |rot||No...'5query = 'The signal lamp of my MLC machine is blinking green. What does that mean?'6score = 0.46674937
Image-to-Text
Um die Genauigkeit des Retrievals zu verbessern, könnten wir die Vision-Fähigkeiten von OpenAI nutzen, um zusätzlichen Kontext aus Bildern zu extrahieren. Angenommen, wir haben die folgende Tabelle, in der wir nach spezifischen Informationen suchen wollen:

Unsere hochgeladenen Text-Embeddings beinhalten die Symbole (Muster) aus der zweiten Spalte nicht als Kontext. Das macht es unmöglich, ein präzises Ergebnis zu erhalten, wenn wir die Anfrage später an ein LLM übergeben.
- OpenAI Vision: Wir nutzen gpt-4o-mini, um dieses Bild im Rahmen eines OCR-Tasks in Text umzuwandeln:
1import io2import os3import base644import requests5from PIL import Image6from dotenv import load_dotenv78load_dotenv()910class GPT:11 """Wrapper class to interact with OpenAI API Vision."""1213 def __call__(self, image: Image.Image):14 """Analyze image with OpenAI API."""15 buffered = io.BytesIO()16 image.save(buffered, format="PNG")17 image_bytes = buffered.getvalue()1819 # Encode image to base6420 base64_image = base64.b64encode(image_bytes).decode("utf-8")2122 headers = {23 "Content-Type": "application/json",24 "Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",25 }2627 payload = {28 "model": "gpt-4o-mini",29 "messages": [30 {31 "role": "user",32 "content": [33 {34 "type": "text",35 "text": "Format this image precisely to text. Be very particular with your formatting.",36 },37 {38 "type": "image_url",39 "image_url": {40 "url": f"data:image/jpeg;base64,{base64_image}"41 },42 },43 ],44 },45 ],46 "max_tokens": 400,47 "top_p": 0.148 }4950 response = requests.post(51 "https://api.openai.com/v1/chat/completions", headers=headers, json=payload52 )53 response.raise_for_status()54 return response.json().get("choices")[0].get("message").get("content")5556query = 'The signal lamp of my MLC machine is blinking green. What does that mean?'57openai_vision = GPT()58image_context = openai_vision(query, top_result.image)
- Output: Wir haben zwar kein perfektes Ergebnis erzielt, konnten aber die Qualität des Kontextes, der als Input für das LLM dient, deutlich verbessern:
1**RGB Multifunktions-Signalleuchte**23⊙ Lampe leuchtet4⊗ Lampe blinkt56| Farbe | Muster | Fehler |7|-------|--------|--------|8| dunkel | Maschine läuft | |9| weiss | System bereit, Stop | |10| weiss | Initialisierung läuft | |11| rot | Not-HALT | |12| rot | Webstellanabdeckung geöffnet | |13| blau | Auftragsende | |14| grün | Schussfadenbruch | |15| grün | Kettfadenbruch / Scheibeltstopp | |16| pink | Handling Mode / Webstellanabdeckung geschlossen | |17| pink | Handling Mode und Webstellanabdeckung geöffnet | |18| gelb | Hilfsfadenbruch | |19| türkis | Aufwickelsicherung |
- DSPy Modul: Wir können dspy nutzen, um alle relevanten Informationen an gpt-4o-mini zu übergeben.
Definieren wir zunächst eine Klasse, um die relevanten Informationen zu speichern.
1import ast2import io3from typing import Optional45from PIL import Image6from qdrant_client.http.exceptions import UnexpectedResponse78from qdrant_wrapper import QdrantWrapper9from mongodb_wrapper import MongoDBWrapper1011class Response:12 """Response object for Qdrant search."""1314 def __init__(15 self,16 query: str,17 machine_type: str,18 passage: Optional[str] = None,19 image: Optional[Image.Image] = None,20 page: Optional[int] = None,21 score: Optional[float] = None,22 ):23 self.query = query24 self.machine_type = machine_type25 self.passage = passage26 self.image = image27 self.page = page28 self.score = score2930 def process_query(self, k: int) -> list["QdrantResponse"]:31 """Process the query and return the top k results."""32 qdrant = QdrantWrapper()33 mongodb = MongoDBWrapper()3435 # Vector search and show results36 try:37 search_results = qdrant.search(38 "pdf-manuals", self.query, self.machine_type39 )40 except UnexpectedResponse as e:41 raise ValueError(f"Something went wrong with the Qdrant API: {e}")4243 top_results = []44 for result in search_results[:k]:45 payload = result.payload46 passage = payload["text"]47 page = payload["page"]4849 # Get image with the same UUID50 collection_item = mongodb.get(payload["id"])5152 byte_data = ast.literal_eval(collection_item["image"])53 image = Image.open(io.BytesIO(byte_data)) if byte_data else None5455 top_results.append(56 Response(57 self.query,58 self.machine_type,59 passage,60 image,61 page,62 result.score,63 )64 )65 return top_results
Definieren wir nun unser Retriever-Modell, das Input- und Output-Schema und bauen die RAG-Pipeline auf.
1from typing import Optional23import dspy4from pydantic import BaseModel, Field56from gpt_wrapper import GPT7from qdrant_wrapper import Response89mini = dspy.OpenAI(model="gpt-4o-mini", max_tokens=500)10dspy.configure(lm=mini, trace=["Test"])111213class QdrantRMClient(dspy.Retrieve):14 """Custom Qdrant Retrieval Model Client."""1516 def __init__(self, machine_type: str, k: int = 3) -> None:17 super().__init__(k=k)18 self.machine_type = machine_type1920 def forward(self, query: str, k: int) -> Response:21 """Forward pass of the QdrantRMClient."""22 k = k if k else self.k23 response = Response(query, self.machine_type).process_query(k)24 return dspy.Prediction(passages=response)252627class Input(BaseModel):28 """Input Schema for the RAG model."""2930 context: str = Field(description="May contain relevant facts")31 question: str = Field()32 image_context: Optional[str] = Field(description="May contain additional facts")333435class Output(BaseModel):36 """Output Schema for the RAG model."""3738 answer: str = Field(description="The answer for the question")39 pdf_path: str = Field()40 image: bytes = Field()41 page: str = Field()42 machine_type: str = Field()434445class QASignature(dspy.Signature):46 """Answer the question based on the context provided."""4748 input: Input = dspy.InputField()49 output: Output = dspy.OutputField()505152class RAG(dspy.Module):53 """PDF Manuals RAG Pipeline."""5455 def __init__(self, machine_type: str, max_hops: int = 3) -> None:56 super().__init__()5758 self.predictor = dspy.TypedChainOfThought(QASignature)59 self.qdrant_retrieve = QdrantRMClient(machine_type)60 self.openai_vision_retrieve = GPT()61 self.max_hops = max_hops6263 def forward(self, question: str) -> dspy.Prediction:64 """Forward pass of the RAG model."""65 retrieval = retrieval[0] # Get the top k passage66 buffered = io.BytesIO()67 retrieval.image.save(buffered, format="PNG")68 image_bytes = buffered.getvalue()6970 input_fields = Input(71 context=retrieval.passage,72 question=question,73 image_context=self.openai_vision_retrieve(retrieval.image),74 )75 prediction = self.predictor(input=input_fields)76 return dspy.Prediction(77 answer=prediction.output.answer,78 pdf_path=retrieval.path,79 image=image_bytes,80 page=retrieval.page,81 machine_type=retrieval.machine_type,82 )8384if __name__ == "__main__":85 machine_type = "MLC"86 question = "The signal lamp of my MLC machine is blinking green. What does that mean?"87 rag = RAG(machine_type)88 pred = rag.forward(question)89 print(pred.answer)
LLM-Ausgabe
1'The blinking green signal lamp on your MLC machine indicates a shot thread break or a warp thread break/separation sheet stop.'
Grenzen (Limitations)
OCR-Tools, einschließlich derer, die von Sprachmodellen genutzt werden, haben manchmal Schwierigkeiten bei der Extraktion von Nicht-Standard-Zeichen, kleinen Icons oder speziellen Symbolen (wie den Kreisen in diesem Fall). Wenn die Symbole in einer kleineren oder weniger klaren Schrift gerendert sind, kann die OCR sie möglicherweise nicht erkennen. Die Genauigkeit hängt stark von der Komplexität und Qualität des analysierten Bildes ab – und davon, wie gut das Bild bei Bedarf vorverarbeitet wurde.
Fazit
In diesem Artikel haben wir gezeigt, wie man ein System zum Abruf technischer Dokumente baut. Wir nutzten Qdrant für das Retrieval textbasierter Embeddings, OpenAIs gpt-4o-mini für OCR, PyMuPDF für die PDF-Extraktion und DSPy, um die Passagen abzurufen und mit Kontext in einer vereinheitlichten Pipeline anzureichern.
Um das Retrieval unserer Dokumente weiter zu verbessern, könnten wir ein Subset unserer Daten für die Ähnlichkeitssuche (Similarity Search) trainieren, indem wir Quaterion nutzen – ein Framework, das für das Fine-Tuning von Similarity-Learning-Modellen entwickelt wurde. Dies würde es uns ermöglichen, unsere Embeddings besser an die spezifischen Nuancen unseres Datasets anzupassen.
Wenn Sie mehr darüber erfahren möchten, werfen Sie einen Blick in den Quaterion Quick Start Guide.

Optimieren Sie Ihre CDN-Effizienz mit Cache Control-Headers
Nutzen Sie die volle Power von CDNs mit optimalen Cache-Control-Headern. Entdecken Sie, wie must-revalidate und Etags die Content-Auslieferung verbessern und warum sie no-cache überlegen sind.


Prototyping von Maschinen-Dashboards mit Node-RED
Ein schneller und einfacher Weg, um Dashboards für Maschinen in Ihrer Fertigung einzurichten.

