Taha @ MJTHD
/
GitHub
Blog
·2 Min. Lesezeit

Grounded Antworten: Retrieval, pgvector und Embedding-Konsistenz

Wie Dokki eine Frage in Top-k Chunks des aktiven Snapshots übersetzt — HNSW auf halfvec, ein geteiltes Embedding-Modell und Quellen, die man nachschlagen kann.

airagretrievaldokki

Die Query Plane von Dokki ist zustandslos und hat genau eine Aufgabe: eine Frage geerdet gegen den aktiven Snapshot beantworten. Der Weg dahin ist kurz, aber jeder Schritt hat eine Fallgrube.

Retrieval gegen genau einen Snapshot

Das Retrieval-SQL joint die Chunks über die snapshot_members des aktiven Snapshots — so sieht eine Query nie etwas, das nicht promotet wurde:

SELECT c.content, c.metadata, c.embedding <=> $queryEmbedding AS distance
FROM vectors.chunks c
JOIN knowledge.snapshot_members sm ON sm.document_version_id = c.document_version_id
WHERE sm.snapshot_id = $activeSnapshotId
ORDER BY c.embedding <=> $queryEmbedding
LIMIT $k;

Welcher Snapshot aktiv ist, weiß der Service über einen gecachten Pointer, den er bei snapshot.activated invalidiert — kein Tabellen-Lookup pro Frage.

halfvec-Index nutzen. Damit der HNSW-Index greift, muss der ORDER BY-Ausdruck zur indexierten Expression passen. Bei halfvec-Cast also beidseitig casten: ORDER BY c.embedding::halfvec(1024) <=> $queryEmbedding::halfvec(1024) — sonst wird es ein Sequential Scan. Der Cast halbiert nebenbei den Speicher (4 → 2 Byte pro Dimension).

Dasselbe Modell auf beiden Seiten

Die subtilste Fehlerquelle in RAG: Frage und Dokumente werden mit verschiedenen Embedding-Modellen vektorisiert. Dann liegen Query- und Dokument-Vektoren nicht im selben Raum und die Cosine-Distanz ist Müll.

Dokki erzwingt Konsistenz strukturell:

  • Ollama (bge-m3) als eigenes Deployment, das Ingest und RAG aufrufen — eine Implementierung, kein dupliziertes Modell.
  • embedding_model wird pro Version und pro Snapshot festgehalten.
  • Bei einer Query liest der Service das embedding_model des aktiven Snapshots und embeddet die Frage mit exakt diesem Modell.
  • Ein Modellwechsel ist deshalb kein In-Place-Update, sondern ein kompletter Re-Ingest in einen neuen Snapshot — erst nach Eval aktiviert.

Multilingual ist hier kein Nice-to-have

Eine Collection wie Lughaty enthält arabisches Lernmaterial. bge-m3 ist explizit multilingual und läuft lokal — bei arabischem Text (RTL, Diakritika) ist die AR-Abdeckung des Modells direkt retrieval-relevant. Das Embedding-Modell ist damit keine reine Infrastruktur-Wahl, sondern eine Qualitätsentscheidung.

Optional: Reranking

Nach dem Vektor-Retrieval kann ein Cross-Encoder die Top-k Chunks neu sortieren, bevor sie in den Prompt wandern — präziser, wenn reine Cosine-Distanz zu grob ist. Per Request über rerank: true steuerbar.

Die Antwort kennt ihre Quellen

Eine Antwort enthält immer die verwendeten Quellen aus den Chunk-Metadaten — Dateiname, Ordinal/Seite, Distanz, Excerpt:

{
  "answer": "Der Dual (المثنى) wird durch das Anhängen von ...",
  "snapshotId": "s5a4b3c2-...",
  "embeddingModel": "bge-m3",
  "sources": [
    {
      "filename": "arabische-grammatik.pdf",
      "ordinal": 142,
      "distance": 0.18,
      "excerpt": "Der Dual bezeichnet genau zwei ..."
    }
  ]
}

Claudes native Citations API liefert dabei die Textspannen auf den übergebenen Chunks; das sources[]-Mapping (Dateiname, Ordinal, Distanz) kommt weiter aus den Chunk-Metadaten. Das Ergebnis ist eine Antwort, deren jede Aussage sich bis zur Seite im Originaldokument zurückverfolgen lässt — der ganze Sinn der Übung.


Damit schließt sich die Serie: von der Drei-Plane-Architektur über immutable Snapshots und das Quality Gate bis zur geerdeten Antwort hier.