RAG-Architektur — selbst-gehostete semantische Suche

Status: Live seit Phase 71 (2026-06-05). Ersetzt die Google-NotebookLM-Bridge aus Phase 36.

TL;DR: Wiki + Issues + Skills jedes Arc-Projekts werden in einen projektbezogenen Vektor-Index eingebettet, der innerhalb der bestehenden SQLite-SSOT liegt. Abfragen liefern semantisch relevante Passagen über Projektinhalte + einen gemeinsamen globalen Skill-Pool. Kein externer Retrieval-Dienst, keine Google-Session pro Benutzer, keine Quellen-Obergrenzen.


1. Warum wir von NotebookLM weg sind

Die Phase-36-Bridge kapselte Google NotebookLM als Vektor-Store über reverse-engineerte Cookie-Authentifizierung. Daraus ergaben sich zwei Probleme, die das zugrunde liegende Produkt nicht beheben kann:

  1. Quellen-Obergrenze. NotebookLM Plus endet bei 100 Quellen pro Notebook. Bis Mitte 2026 hatten mehrere Arc-Projekte die Grenze gesprengt; die Eviction-Logik wurde zum dauerhaften Notbehelf (#324).
  2. Kopplung an ein persönliches Konto. Jedes neue Projekt erzeugte ein Notebook im persönlichen NotebookLM des CEO. Auf N Kunden zu skalieren bedeutete N Notebooks in der Sidebar einer einzigen Person — ein unkontrollierter Seiteneffekt, den die Bridge nicht entkoppeln konnte.

Phase 71 tauscht die Retrieval-Schicht gegen eine selbst-gehostete Pipeline aus, die das Google-Konto des Benutzers nie berührt.


2. Komponenten-Übersicht

                Write path                          Read path
                ──────────                          ─────────
 wiki.ts handleWikiSave ────┐                 chat.ts executeAskNotebooklm ──┐
 cli-routes.ts handleCreate/                                                 │
   UpdateIssue ─────────────┤              shared/routes/rag-search.ts ──────┤
 skills.ts handleSave/      │                 GET /api/crm/projects/:name/   │
   Delete + global CRUD ────┤                 rag/search                     │
                            │                                                │
                            ▼                                                ▼
              shared/rag-hooks.ts                              shared/rag.ts search()
                syncIssue / syncWiki /                            │  embeds query →
                syncSkill (fire-and-forget)                       │  KNN over vec0 →
                            │                                     │  project + doc_type filter
                            ▼                                     │  → top-K passages
              shared/rag.ts upsert()                              │
                paragraph-aware chunker (~1800 chars)              │
                  → atomic transaction:                            │
                       embeddings ⨯ embeddings_vec                 │
                            │                                     │
                            ▼                                     │
              shared/embeddings.ts ◄────────────────────────────  │
                Cohere embed-multilingual-v3.0                    │
                (1024-dim float32, batch ≤96 inputs/call)         │
                            │                                     │
                            ▼                                     │
              SQLite (data/citadel.db)  ◄──────────────────────── ┘
                 ┌──────────────────────┐   ┌─────────────────────┐
                 │ embeddings           │   │ embeddings_vec      │
                 │ (project, doc_type,  │   │ (vec0 virtual,      │
                 │  doc_id, chunk_ix,   │   │  embedding FLOAT    │
                 │  text, created_at)   │ ──┤  [1024])            │
                 │ id ←→ rowid          │   │                     │
                 └──────────────────────┘   └─────────────────────┘

2.1 Module auf einen Blick

Modul Rolle
shared/embeddings.ts Cohere-Client — embedBatch(texts, inputType) für die Indexierung + embedQuery(text) für Abfragen. Retry mit 3× exponentiellem Backoff bei transienten Fehlern, Fail-Fast bei 401/403.
shared/rag.ts Storage-Schicht — upsert / search / removeDoc / removeProject / stats. Besitzt den Chunker und die atomare embeddingsembeddings_vec-Transaktion.
shared/rag-hooks.ts Fire-and-Forget-Wrapper (syncIssue, syncWiki, syncSkill + remove*-Spiegel). Fehler werden geloggt, rollen aber nie den auslösenden Disk-/SQL-Write zurück.
shared/routes/rag-search.ts Öffentlicher HTTP-Einstiegspunkt. GET /api/crm/projects/:name/rag/search?q=...
scripts/phase-71-backfill-rag.ts One-Shot-idempotenter Seeder für Bestandsinhalte. Flags: --dry-run / --force / --project / --throttle.

3. Embedding-Wahl — Cohere embed-multilingual-v3.0

Eigenschaft Wert
Dimensionalität 1024-dim float32
Mehrsprachig Nativ — Ukrainisch + Englisch + 100+ Sprachen teilen sich einen Sub-Space
Kosten ~$0.10 / 1M Tokens (Production-Tier). Geschätzt ~$4/Monat bei 50-Benutzer-Skalierung laut Kostenmodell.
Sub-Space-Trennung input_type="search_document" für die Indexierung vs. input_type="search_query" für Laufzeitabfragen. Das Vermischen der beiden ruiniert den Recall.

Live-Cross-Lingual-Smoke gegen das Prod-Korpus:

Abfrage Sprache Top-Treffer Distanz
Safari cookie auth bug EN issue/174 (auth/password reset) 1.00
множинні тенанти UK issue/49 (Phase 53.11.2 Multi-Worker TG Topics Mode) 1.07

Die UK-Abfrage schlug die entsprechende EN-EN-Distanz — die mehrsprachige Sub-Space-Ausrichtung ist real, keine Marketingbehauptung.


4. Storage — sqlite-vec innerhalb der SSOT

Die Migration 049_embeddings erstellt zwei Tabellen:

CREATE TABLE embeddings (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  project     TEXT NOT NULL,
  doc_type    TEXT NOT NULL,         -- 'wiki' | 'issue' | 'skill' | 'transcript'
  doc_id      TEXT NOT NULL,         -- filename, issue id, skill name
  chunk_ix    INTEGER NOT NULL DEFAULT 0,
  text        TEXT NOT NULL,
  created_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE VIRTUAL TABLE embeddings_vec USING vec0(
  embedding FLOAT[1024]
);
-- embeddings.id ←→ embeddings_vec.rowid

Die Aufteilung hält die Metadaten in regulärem SQLite (joinbar mit den Tabellen wiki/issues/skills, einfaches LIST/DELETE), während die virtuelle vec0-Tabelle die dichten Vektoren für die ANN-Suche enthält.

Dimensionierung. 1024-dim float32 = 4096 Bytes / Zeile. 100K Zeilen ≈ 400 MB. Beim aktuellen Maßstab unproblematisch; bei 1M+ Zeilen neu bewerten.

Laden. sqlite-vec wird als Laufzeit-Extension in shared/db.ts initDb() geladen, bevor die Migrationen laufen. Migration 049 prüft vec_version() und bricht sauber ab, falls die Extension nicht geladen wurde.


5. Write-Pfad — Re-Embed-Hooks (Phase 71.5)

Jede Schreiboberfläche, die indexierbaren Inhalt verändert, feuert einen Fire-and-Forget-Hook, damit das Embedding ohne separaten Sync-Job frisch bleibt.

Auslöser Hook Anmerkungen
PUT /api/crm/projects/:name/wiki/save syncWiki(project, path, content) Nachdem Bun.write erfolgreich war
POST /api/mcp/issues/:project (create) syncIssue(project, id, title, body) Nach dem SQLite-Write
PUT /api/mcp/issues/:project/:id (update) syncIssue(project, id, title, body) Ebenso
POST /api/mcp/issues/:project/:id/log (activity) keiner Das Activity-Log ändert weder indexierten Titel noch Body — Re-Embedding würde Quota für eine No-Op verbrennen
PUT /api/crm/projects/:name/skills/save syncSkill(project, name, content) Projektbezogener Skill
DELETE /api/crm/projects/:name/skills/delete removeSkill(project, name) Spiegel
POST /api/crm/skills (global create) syncSkill("_global_", name, content) Globaler Pool
PUT /api/crm/skills/:id (global update) syncSkill("_global_", name, content) Nur wenn sich das content-Feld tatsächlich geändert hat
DELETE /api/crm/skills/:id (global delete) removeSkill("_global_", name) Spiegel
DELETE /api/auth/account (GDPR Art. 17) removeProject(name) pro eigenem Projekt Embeddings überleben die Kontolöschung nicht
Transkript-Pipeline (Phase 73.6): status → summarized → embedding → done upsert(project, "transcript", id, ragText) via transcript-worker.ts ragText = Transkript-Text + [Xs] frame descriptions + Summary tldr/key_points/decisions/action_items. Übersprungen bei embed_to_rag=0. Nicht fatal: RAG-Fehler finalisiert den Job trotzdem auf done.

Regel: Cohere-Fehler loggen eine Zeile und sterben. Sie rollen den auslösenden Disk-/SQL-Write nie zurück. Persistente Fehler konvergieren über den nächtlichen Backfill, nicht über Retries im Hot Path.


6. Read-Pfad — search() + Fassaden pro Oberfläche

import { search } from "shared/rag";

const hits = await search("arc-v2", "How does the multi-tenancy gate work?", {
  k: 6,
  doc_types: ["wiki", "issue", "skill", "transcript"], // optional narrow
});
// hits: Array<{ doc_type, doc_id, chunk_ix, text, distance }> sorted by distance

Unter der Haube:

  1. Die Abfrage wird mit input_type="search_query" eingebettet (anderer Sub-Space als search_document).
  2. KNN über embeddings_vec mit k * 4-Overfetch.
  3. Filterung nach Projekt + optionalem doc_type im SQL-JOIN.
  4. Rückgabe der Top-K-Passagen, sortiert nach aufsteigender L2-Distanz.

Öffentliche Fassaden


7. Backfill — scripts/phase-71-backfill-rag.ts

bun scripts/phase-71-backfill-rag.ts                       # all projects + global
bun scripts/phase-71-backfill-rag.ts --dry-run             # inventory only
bun scripts/phase-71-backfill-rag.ts --project arc-v2      # one project + global
bun scripts/phase-71-backfill-rag.ts --skip-global         # projects only
bun scripts/phase-71-backfill-rag.ts --doc-types wiki,issue
bun scripts/phase-71-backfill-rag.ts --force               # re-embed even if row exists
bun scripts/phase-71-backfill-rag.ts --limit 100           # cap docs per project
bun scripts/phase-71-backfill-rag.ts --throttle 700        # ms between Cohere calls (default 700; set 0 on Production keys)

Idempotenz. Jeder Kandidat wird auf Vorhandensein von (project, doc_type, doc_id) in embeddings geprüft und übersprungen, falls vorhanden — außer --force ist gesetzt. Wiederholte Läufe kosten null Cohere-Quota.

Production-Lauf (2026-06-05). 814 Kandidaten über 20 Projekte + _global_ (37 Wiki + 482 Issue + 295 Skill). Der erste Durchlauf verbrannte die monatliche Trial-Obergrenze von 1000 Calls bei den globalen Skills. Nach dem Upgrade auf einen Production-Key waren die verbleibenden 178 Dokumente in 57 s fehlerfrei fertig. Insgesamt geschriebene Chunks: ~3.150.


8. Sicherheitsgrenzen


9. Migration von NotebookLM (einmalig, 2026-06-05)

Schritt Erledigt
Cohere-API-Key + Platform-Settings-Tab + Live-Probe #358
sqlite-vec-Extension + Migration 049 #359
shared/embeddings.ts-Client #360
shared/rag.ts-Fassade #361
Re-Embed-Hooks auf Wiki-/Issue-/Skill-Writes #362
Backfill-Skript + Prod-Lauf #363
Call-Sites tauschen (chat.ts, skills.ts, neues /rag/search, arc kb search) #364
services/notebooklm-bridge/ außer Betrieb nehmen + projects.notebook_id entfernen (Migration 050) #365
Doku (diese Seite + 7 Locale-Stubs) #366
Soak-Validierung #367

10. Betriebshinweise


11. Referenzen