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:
- 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). - 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 embeddings↔embeddings_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:
- Die Abfrage wird mit
input_type="search_query"eingebettet (anderer Sub-Space alssearch_document). - KNN über
embeddings_vecmitk * 4-Overfetch. - Filterung nach Projekt + optionalem
doc_typeim SQL-JOIN. - Rückgabe der Top-K-Passagen, sortiert nach aufsteigender L2-Distanz.
Öffentliche Fassaden
GET /api/crm/projects/:name/rag/search?q=...&k=...&include_global=true&doc_types=...— HTTP-Einstiegspunkt. Treibtarc kb search+ dasask_notebooklm-Tool im Chat an.chat.ts executeAskNotebooklm— führt die Projekt- und die globale Suche parallel aus, mergt nach Distanz, fällt auf Schlüsselwortsuche zurück, wenn der Top-Trefferd > 1.6hat oder keine Ergebnisse vorliegen.skills.ts handleGenerateSkill— nutzt RAG als House-Style-Retrieval und ruft dann Claude Sonnet für die Generierung auf.help.ts ragSemantic— der Arc-Help-Chat liefert Projekt- + globale Treffer als Modellkontext.
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
- Der Cohere-API-Key liegt im Vault (
COHERE_API_KEY) und wird über Platform Settings → RAG / Semantic search rotiert. Der Live-Test-Button führt einen 1-Token-Embed-Call aus, um die Erreichbarkeit zu bestätigen. - Kein Prompt-Leak zu Cohere. Embeddings sind Bag-of-Words-Derivate; sie zurück in den Quelltext zu invertieren ist unpraktikabel. Cohere erklärt selbst, nicht auf API-Traffic zu trainieren.
- Multi-Tenancy. Projektnamen sind Namespace-Schlüssel;
canAccessProjectschützt/api/crm/projects/:name/*, bevor der Such-Handler läuft. Globale Skills leben in einem Sentinel-Namespace_global_, mit dem kein Benutzerprojektname kollidieren kann (der Validator verbietet Namen, die mit_beginnen). - GDPR Art. 17.
DELETE /api/auth/accountkaskadiertrag.removeProjectfür jedes eigene Projekt, sodass Embeddings die Löschung nicht überleben.
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
- Tool-Name bleibt
ask_notebooklm. Die Anthropic-Tool-Definition wird downstream an Claude in der Cloud-PM-Konversation ausgeliefert. Eine Umbenennung würde laufendetool_use-Loops brechen. Die Implementierung ist jetzt reines RAG; der öffentliche Vertrag blieb stabil. - Audio-Overview ist weg. Die automatisch generierten Audio-Zusammenfassungen von NotebookLM haben in der selbst-gehosteten Pipeline kein Äquivalent.
POST /api/crm/projects/:name/memory/fetch-artifactliefert jetzt410 Gone. Falls das zurückkommen soll, ist das eine eigene Phase (wahrscheinlich ein Whisper-TTS-Durchlauf über das Projekt-Wiki). - Der Re-Index-Button funktioniert weiterhin.
POST /api/crm/projects/:name/memory/refreshbettet jetztMANIFEST.md+ROADMAP.md+ Schlüsseldateien in den lokalenembeddings-Store neu ein, statt zu NotebookLM hochzuladen. Benutzerseitiges Verhalten: gleicher Button, gleiches Ergebnis („project knowledge is now fresh"), anderer Speicher. - Keine Notebook-URLs.
GET /api/crm/projects/:name/notebooksliefert{ notebooks: [], retired: "phase-71.8" }, damit alte Frontend-Builds nicht in 404 laufen. Der Neural-Memory-Tab im CRM wird in einem späteren kosmetischen Durchgang entfernt.
11. Referenzen
shared/embeddings.ts,shared/rag.ts,shared/rag-hooks.ts,shared/routes/rag-search.tsshared/migrations/049_embeddings.ts,shared/migrations/050_drop_notebook_id.tsscripts/phase-71-backfill-rag.ts- Issues #321 (Parent) · #358–#367 (Sub-Phasen) · #322 / #324 (parallel geschlossen)
- Entscheidungsprotokoll:
docs/architecture/PHASE_71_RAG_MIGRATION.md