feat: Control Library UI, dedup migration, QA tooling, docs
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 31s
CI/CD / test-python-backend-compliance (push) Successful in 1m35s
CI/CD / test-python-document-crawler (push) Successful in 20s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Has been skipped

- Control Library: parent control display, ObligationTypeBadge,
  GenerationStrategyBadge variants, evidence string fallback
- API: expose parent_control_uuid/id/title in canonical controls
- Fix: DSFA SQLAlchemy 2.0 Row._mapping compatibility
- Migration 074: control_parent_links + control_dedup_reviews tables
- QA scripts: benchmark, gap analysis, OSCAL import, OWASP cleanup,
  phase5 normalize, phase74 gap fill, sync_db, run_job
- Docs: dedup engine, RAG benchmark, lessons learned, pipeline docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-21 11:56:08 +01:00
parent c52dbdb8f1
commit 643b26618f
28 changed files with 5781 additions and 75 deletions

View File

@@ -2,7 +2,23 @@
## Übersicht
Die Control Quality Pipeline prüft und verbessert die ~9.000 Canonical Controls der Compliance-Bibliothek. Sie nutzt **PDF-basierte Verifizierung** als Ground Truth — jeder Control-Originaltext wird direkt im Quelldokument (PDF) lokalisiert.
Die Control Quality Pipeline prüft und verbessert die Canonical Controls der Compliance-Bibliothek. Sie nutzt **PDF-basierte Verifizierung** als Ground Truth — jeder Control-Originaltext wird direkt im Quelldokument (PDF) lokalisiert.
Alle Scripts liegen in **`scripts/qa/`**. Starten auf dem Mac Mini via Runner-Script:
```bash
# Job starten (laedt .env automatisch, PID-Lock, unbuffered output)
ssh macmini "bash ~/Projekte/breakpilot-compliance/scripts/qa/run_job.sh <script.py> [args...]"
# Status aller Jobs
ssh macmini "bash ~/Projekte/breakpilot-compliance/scripts/qa/run_job.sh --status"
# Log ansehen
ssh macmini "bash ~/Projekte/breakpilot-compliance/scripts/qa/run_job.sh --log <script.py>"
# Job stoppen
ssh macmini "bash ~/Projekte/breakpilot-compliance/scripts/qa/run_job.sh --kill <script.py>"
```
## Architektur
@@ -55,20 +71,24 @@ Jeder Control hat ein Feld `source_original_text` — der Chunk-Text aus dem Que
| Metrik | Wert |
|---|---|
| Controls mit source_original_text | 7.943 |
| Im PDF lokalisiert | **6.259 (79%)** |
| Nicht gefunden (Sprachmismatch) | 1.651 |
| Kein PDF vorhanden | 33 |
| 100% Match-Rate | 19 Regulations (inkl. DSGVO, KI-VO, NIS2, NIST 800-53) |
| Controls mit source_original_text | 5.751 (86%) |
| Im PDF lokalisiert | **5.063 (88%)** |
| Nicht gefunden | 649 |
| Kein PDF vorhanden | 29 |
| Recital_suspect markiert | 648 |
| 100% Match-Rate | 20+ Regulations (inkl. DSGVO, KI-VO, NIS2, NIST 800-53, Blue Guide) |
**Verlauf:** v1 (4.110, 52%) → v2 (6.091, 77%) → v3 (6.259, 79%) → v4 +Blue Guide EN (6.803, 86%) → v5 nach Cleanup (5.063/5.741, 88%)
### Nicht-matchende Controls
| Ursache | Controls | Erklärung |
| Ursache | Controls | Status |
|---|---|---|
| Blue Guide EN vs. DE PDF | ~562 | Controls aus englischem PDF, wir haben nur deutsches |
| OWASP multilingual | ~632 | Controls aus PT/AR/ID/ES-Übersetzungen |
| ~~Blue Guide EN vs. DE PDF~~ | ~~562~~ | ✅ Gelöst — EN-PDF beschafft, 544/544 gematcht |
| ~~OWASP Top 10 multilingual~~ | ~~324~~ | ✅ Als duplicate markiert — Übersetzungen ohne Mehrwert |
| CRA Encoding | ~76 | PDF-Ligaturen/Sonderzeichen-Differenzen |
| CISA Secure by Design | ~113 | Falsches PDF (ENISA statt CISA) |
| OWASP ASVS | ~173 | PDF-Matching-Problem (meist EN) |
## Brute-Force-Suche
@@ -100,34 +120,276 @@ Controls aus Erwägungsgründen (`article_type = preamble`) sind **kein Nachteil
Die 1.195 v1-Controls **ohne** Originaltext sind manuell erstellt (`strategy=ungrouped`) und haben keine Chunk-Referenz.
## DB-Status (Stand 2026-03-20)
## OWASP Cleanup (2026-03-20)
- **324 OWASP Top 10 multilingual Controls** → `duplicate` markiert (ZH, AR, ID, FR, ES, PT — Übersetzungen derselben 10 Kategorien)
- **47 Controls** mit falscher Quellenzuordnung korrigiert (z.B. als "OWASP Top 10" getaggt, aber tatsächlich aus ASVS/SAMM/API/MASVS)
- **~200 OWASP ASVS/SAMM/MASVS EN Controls** behalten — unique Content aus GitHub/Website, nicht im PDF auffindbar
## NIST OSCAL Import (2026-03-20)
**776 neue Controls** aus NIST SP 800-53 Rev 5 OSCAL (Public Domain, maschinenlesbar):
- Quelle: `usnistgov/oscal-content` (JSON Catalog)
- Vor allem **Control Enhancements** (z.B. AC-2(3), SC-7(8)) — die atomaren Unteranforderungen
- Jeder Control enthält: Statement + Guidance + Assessment-Methoden + Cross-References + Parameters
- `pipeline_version = 4`, `generation_strategy = 'oscal_import'`
- Kein Pass 0a/0b nötig — Controls sind **bereits atomar**
| Metrik | Vorher | Nachher |
|---|---|---|
| SP 800-53 Controls (aktiv) | 1.107 | **1.883** |
| OSCAL-Abdeckung | 238/1.014 (23%) | **1.014/1.014 (100%)** |
## Phase 5: RAG-Deduplizierung + Normalisierung (2026-03-20)
### Durchgeführte Schritte
| Schritt | Beschreibung | Controls |
|---|---|---|
| 5.1 | OSCAL Controls: `source_regulation` in generation_metadata gesetzt | 776 |
| 5.2 | v3 Controls ohne Source → `needs_review` mit `missing_source` Flag | 20 |
| 5.3 | Leerer Source-Name korrigiert (AT TKG) | 1 |
| 5.4 | OWASP regulation_code Fehlzuordnungen korrigiert | 47 |
| 5.5 | **duplicate/too_close Controls hart gelöscht** | **3.301** |
| 5.6 | Processed Chunks bereinigt (gelöschte Control-IDs entfernt) | 2.520 |
### Ergebnis
- **Vorher:** 9.936 Controls (6.635 aktiv, 2.998 duplicate, 303 too_close)
- **Nachher:** 6.635 Controls, **alle aktiv** (0 duplicate/too_close)
- Alle regulation_codes haben jetzt einheitliche Source-Namen
- OWASP-Controls sind korrekt ihren Quellen zugeordnet
## DB-Status (Stand 2026-03-20, nach Phase 7.4)
| release_state | Count |
|---|---|
| draft | 5.365 |
| needs_review | 818 |
| duplicate | 2.674 |
| too_close | 303 |
| **Aktiv** | **6.183** |
| draft | ~6.030 |
| needs_review | 838 |
| **Gesamt** | **6.868** |
## Scripts
## Scripts (`scripts/qa/`)
Alle QA-Scripts liegen in `scripts/qa/`:
### Kern-QA (PDF-Matching)
| Script | Beschreibung |
|---|---|
| `pdf_qa_all.py` | Haupt-QA: Controls gegen PDFs matchen |
| `pdf_qa_inventory.py` | Inventar: Regulations, Controls, PDFs |
| `apply_pdf_qa_results.py` | Ergebnisse in DB schreiben |
| `preamble_dedup.py` | Preamble vs. Artikel Duplikat-Erkennung |
| `qa_dedup_controls.py` | Jaccard-basierte Titel-Dedup |
| `qa_normalize_sources.py` | Source-Namen normalisieren |
| `db_status.py` | DB-Status-Übersicht |
| `pdf_qa_all.py` | **Haupt-QA**: Controls gegen PDFs matchen, Artikel-Index aufbauen. Enthaelt `SOURCE_FILE_MAP`, alle Index-Builder (EU, DE, NIST, OWASP, generic). 526 Zeilen. |
| `pdf_qa_inventory.py` | Inventar: Welche Regulations haben Controls, wie viele, welche PDFs existieren |
| `apply_pdf_qa_results.py` | Ergebnisse aus `pdf_qa_all.py` in DB schreiben (`article_type`, `recital_suspect`) |
| `pdf_article_lookup_poc.py` | POC: Control-Text in PDF lokalisieren, Headings von Cross-Refs unterscheiden |
## Nächste Schritte
### Lueckenanalyse + Control-Generierung
1. **Blue Guide EN-PDF** beschaffen → +562 Controls matchen
2. **CISA Secure by Design** echtes PDF finden → +113 Controls
3. **Brute-Force Ergebnisse anwenden** — 44 falsche Source-Zuordnungen korrigieren
4. **Frontend-Anzeige**`article_type` im Control-Detail anzeigen
5. **Continuous QA** — Bei neuen Controls automatisch PDF-Match prüfen
| Script | Beschreibung |
|---|---|
| `gap_analysis.py` | **Phase 7.3**: Artikel im PDF vs. Controls in DB vergleichen, Luecken identifizieren |
| `phase74_generate_gap_controls.py` | **Phase 7.4**: Neue Controls fuer Luecken via Anthropic API generieren. `pipeline_version=5`. 624 Zeilen. |
| `benchmark_llm_controls.py` | LLM-Vergleich: gpt-oss-120b vs. Claude Sonnet fuer Control-Generierung |
| `test_pass0a.py` | **Pass 0a Test**: Obligation Extraction + 3-Tier-Klassifizierung (Pflicht/Empfehlung/Kann). Standalone, speichert JSON. |
### Deduplizierung + Normalisierung
| Script | Beschreibung |
|---|---|
| `preamble_dedup.py` | Preamble vs. Artikel Duplikat-Erkennung (Jaccard >= 0.40) |
| `qa_dedup_controls.py` | Jaccard-basierte Titel-Deduplizierung |
| `qa_apply_and_dedup.py` | Ergebnisse anwenden + Duplikate in einem Schritt markieren |
| `qa_normalize_sources.py` | Source-Namen normalisieren (kanonische Namen) |
| `phase5_normalize_and_cleanup.py` | **Phase 5**: Normalisierung + 3.301 Duplikate hart loeschen |
| `qa_delete_gpsr_dupe.py` | GPSR-Duplikate loeschen |
| `delete_gpsr_prod.py` | GPSR-Duplikate aus Production-Qdrant entfernen |
### Quellen-spezifische Scripts
| Script | Beschreibung |
|---|---|
| `blue_guide_en_match.py` | Blue Guide EN-PDF matchen (544/544 Erfolg) |
| `owasp_cleanup.py` | OWASP multilingual Cleanup (324 Duplikate) + Source-Fix (47 korrigiert) |
| `owasp_github_match.py` | OWASP ASVS/SAMM/MASVS gegen GitHub-Markdown matchen |
| `oscal_import.py` | NIST OSCAL Import (776 Controls aus JSON Catalog) |
| `oscal_analysis.py` | NIST OSCAL Analyse: Abdeckung, fehlende Controls |
### Diagnose + Utilities
| Script | Beschreibung |
|---|---|
| `db_status.py` | DB-Status: release_state Counts, pipeline_version, source Verteilung |
| `debug_low_match.py` | Debugging: Warum matchen Blue Guide / OWASP / CISA schlecht? |
| `qa_article_map_all_chunks.py` | Alle Chunks Artikel-Nummern zuordnen (Bulk) |
| `backfill_job_66228863.py` | Einmaliger Backfill-Job |
| `sync_controls_to_prod.py` | Controls von Dev nach Production synchronisieren |
### Runner
| Script | Beschreibung |
|---|---|
| `run_job.sh` | **Job-Runner**: Laedt `.env`, PID-Lock, Monitoring (`--status`, `--log`, `--kill`) |
## Phase 7: PDF-Validierung + Enrichment (2026-03-20)
### 7.1 + 7.2: Controls gegen PDFs validiert + Ergebnisse angewendet ✅
- 5.063 Controls erfolgreich im Original-PDF lokalisiert (88%)
- `article_type` fuer alle gematchten Controls gesetzt
- 648 Preamble-Controls als `recital_suspect` in `generation_metadata` markiert
- 332 Controls nicht matchbar (OWASP ASVS 132, CISA 72, ENISA 38, OWASP SAMM 31, CRA 28)
### 7.3: Lueckenanalyse ✅
**494 Artikel-Luecken** in 15 Quellen identifiziert. Geschaetzt ~300 davon actionable.
| Source | Luecken | Coverage | Bemerkung |
|---|---:|---:|---|
| AML-Verordnung | 91 | 5% | Kaum ingestiert |
| MiCA | 71 | 52% | Grosse Verordnung |
| NIST SP 800-53 | 59 | 83% | Meist Section-Header, nur SA-15 fehlt |
| OWASP ASVS 4.0 | 47 | 35% | Requirement-Gruppen fehlen |
| Batterieverordnung | 41 | 58% | |
| DSGVO | 35 | 65% | Einige Governance/Aufsicht-Artikel |
| ENISA ICS/SCADA | 34 | 31% | |
| ENISA Supply Chain | 26 | 7% | |
| CRA | 23 | 68% | |
| NIS2 | 16 | 65% | |
| KI-Verordnung | 15 | 87% | Fast komplett |
| Maschinenverordnung | 5 | 91% | Fast komplett |
### 7.4: Neue Controls fuer Luecken generieren ✅ (2026-03-20)
Script: `phase74_generate_gap_controls.py --resume`
- **494 Artikel-Luecken** in 15 Quellen → Anthropic Claude Sonnet 4.6
- `pipeline_version = 5`, `generation_strategy = 'phase74_gap_fill'`
- Direkt PDF-Text als Input (nicht RAG-Chunks)
- Starten via: `run_job.sh phase74_generate_gap_controls.py --resume`
**Ergebnis:**
| Source | Luecken | Generiert |
|---|---:|---:|
| AML-Verordnung | 91 | 97 |
| MiCA | 71 | 68 |
| NIST SP 800-53 | 59 | 19 |
| KI-Verordnung | 15 | 15 |
| OWASP ASVS 4.0 | 47 | 11 |
| Batterieverordnung | 41 | 9 |
| DSGVO | 35 | 4 |
| OWASP Top 10 | 12 | 3 |
| NIS2 | 16 | 3 |
| CRA | 23 | 3 |
| OECD KI-Empfehlung | 4 | 1 |
| **Gesamt** | **494** | **233** |
Nicht generiert: 75 zu kurzer Text, 29 NIST-Intros, 11 Parse-Errors, 162 ID-Konflikte (COMP-1000 etc.).
API-Kosten: ~$7,55 (109 min Laufzeit).
## Pass 0a: Obligation Extraction — 3-Tier-Klassifizierung
### Konzept
Pass 0a zerlegt Rich Controls (~6.000) in **atomare Obligations** per LLM (Claude Sonnet 4.6).
Jede Obligation wird durch den **Quality Gate** klassifiziert — nicht gefiltert:
| obligation_type | Signal | Beispiel |
|---|---|---|
| **pflicht** | müssen, muss, ist zu, hat zu, shall, must, required | "Der Betreiber muss alle Daten verschluesseln" |
| **empfehlung** | soll, sollen, should, sicherstellen, gewaehrleisten, dokumentieren | "Der Betreiber soll regelmaessige Audits durchfuehren" |
| **kann** | kann, koennen, darf, duerfen, may, optional | "Der Betreiber kann zusaetzliche Massnahmen ergreifen" |
**Wichtig:** Nichts wird mehr rejected wegen fehlendem normativem Signal. Obligations ohne Signal werden als `empfehlung` klassifiziert. Rejected werden nur noch: Evidence-Only, zu kurz (<20 Zeichen), fehlender Parent-Link.
### Warum auch Empfehlungen behalten?
Empfehlungen helfen Firmen, ihre Systeme sicherer zu machen — ueber das Pflichtprogramm hinaus. Im Frontend erhalten Kunden einen Marker, der klar anzeigt:
- **Pflicht** = gesetzlich/regulatorisch vorgeschrieben
- **Empfehlung** = Best Practice, freiwillig, aber wertvoll
- **Kann** = optional, weitergehende Massnahme
### Quality Gate — Kritische Flags
| Flag | Kritisch? | Beschreibung |
|---|---|---|
| `has_normative_signal` | Nein | Informativer Check, kein Ablehnungsgrund |
| `obligation_type` | — | Klassifizierung (pflicht/empfehlung/kann) |
| `not_evidence_only` | **Ja** | Kein reiner Nachweis-Eintrag |
| `min_length` | **Ja** | Mindestens 20 Zeichen |
| `has_parent_link` | **Ja** | Verbindung zum Parent-Control |
| `single_action` | Nein | Nur ein Hauptverb (heuristisch) |
| `not_rationale` | Nein | Keine reine Begruendung |
### Normative Signal Detection — Regex-Tiers
```
Tier 1 (Pflicht): muessen, muss, ist/sind/hat/haben zu + Infinitiv,
Compound-Verben (festzustellen, vorzunehmen),
Gerundivum (mitzuteilen, bereitzustellen),
shall, must, required
Tier 2 (Empfehlung): soll, sollen, sollte, sollten,
gewaehrleisten, sicherstellen,
should, ensure, recommend,
dokumentieren, implementieren, ueberpruefen
Tier 3 (Kann): kann, koennen, darf, duerfen, may, optional
```
### Testergebnisse (3 Iterationen, 2026-03-20)
| Run | Controls | Obligations | Validated | Rejected | Kosten |
|---|---:|---:|---:|---:|---:|
| 1 (v0 Regex) | 10 | ~100 | 68% | 32% | $0,28 |
| 2 (v1 Regex) | 50 | ~530 | 78% | 22% | $1,43 |
| 3 (v2 Regex) | 50 | ~530 | 86% | 14% | $1,44 |
| 4 (3-Tier) | 60 | — | — | — | — |
Run 4 laeuft mit dem neuen Klassifizierer — statt PASS/REJECT wird jetzt PFLICHT/EMPFEHLUNG/KANN ausgegeben.
### Scripts
| Script | Beschreibung |
|---|---|
| `test_pass0a.py` | **Test-Script**: Standalone (kein SQLAlchemy), psycopg2 + Anthropic API. Speichert Ergebnisse als JSON. |
```bash
# Test mit 10 Controls
run_job.sh test_pass0a.py --limit 10
# Test mit bestimmter Quelle
run_job.sh test_pass0a.py --limit 20 --source "DSGVO"
# Ergebnisse: /tmp/pass0a_results_<N>controls.json
```
### Backend-Code
- **Klassifizierung:** `backend-compliance/compliance/services/decomposition_pass.py`
- `classify_obligation_type()` — 3-Tier-Klassifizierung
- `quality_gate()` — gibt `obligation_type` in Flags zurueck
- `passes_quality_gate()``has_normative_signal` nicht mehr kritisch
- `ObligationCandidate.obligation_type` — neues Feld
### Hochrechnung (basierend auf 50-Control-Runs)
| Metrik | Wert |
|---|---|
| Kosten pro Control | ~$0,029 |
| Kosten fuer ~6.000 Controls | **~$172** |
| Laufzeit (geschaetzt) | ~25h |
| Obligations pro Control | ~10,5 |
---
## Naechste Schritte
1. ~~**Phase 5 Cleanup** → 3.301 Duplikate geloescht, Source normalisiert~~
2. ~~**Phase 6 Pipeline-Haertung** → Source aus REGULATION_LICENSE_MAP~~
3. ~~**Phase 7.1-7.3** → PDF-Validierung + Enrichment + Lueckenanalyse~~
4. ~~**Phase 7.4** → 233 neue Controls fuer Luecken generiert ($7,55)~~
5. **Pass 0a** → Obligation Extraction mit 3-Tier-Klassifizierung (Tests laufen, ~$172)
6. **Pass 0b** → Atomic Control Composition aus validierten Obligations
7. **Pass 1-5** → Multi-Layer Migration (Code + 500 Tests bereits vorhanden)
8. **Phase 8** → Qdrant Re-Ingestion (Runtime-Betrieb, ZULETZT)
9. **needs_review Triage** — 838 Controls klassifizieren
10. **Frontend**`obligation_type` (Pflicht/Empfehlung/Kann) + `article_type` anzeigen

View File

@@ -0,0 +1,206 @@
# RAG Pipeline Benchmark & Optimierungen
Stand: 2026-03-21. Vergleich unserer Implementierung mit State of the Art. Priorisierte Empfehlungen nach Impact/Effort.
---
## Aktuelle Pipeline (Ist-Zustand)
```mermaid
flowchart LR
A[Dokumente] -->|Document Crawler| B[Chunks 512/50]
B -->|bge-m3| C[Qdrant Dense]
C -->|Cosine Search| D[Control Generator v2]
D -->|LLM| E[Rich Controls 6.373]
E -->|Pass 0a| F[Obligations]
F -->|Pass 0b| G[Atomare Controls]
G -->|4-Stage Dedup| H[Master Controls ~18K]
```
| Komponente | Implementierung | SOTA-Bewertung |
|-----------|----------------|----------------|
| **Chunking** | Rekursiv, 512 Zeichen, 50 Overlap | Zu klein fuer Rechtstexte |
| **Embedding** | bge-m3 (1024-dim, Ollama) | Gut, aber nur Dense genutzt |
| **Vector DB** | Qdrant mit Payload-Filtering | Hybrid Search nicht aktiviert |
| **Retrieval** | Pure Dense Cosine Similarity | Kein Re-Ranking, kein BM25 |
| **Extraktion** | 3-Tier (Exact → Embedding → LLM) | Solide Architektur |
| **Dedup** | 4-Stage (Pattern → Action → Object → Embedding) | Ueberdurchschnittlich |
| **QA** | 5-Metrik Similarity + PDF-QA Matching | Gut, RAGAS fehlt |
---
## Tier 1: Quick Wins (Tage, nicht Wochen)
### 1. Chunk-Groesse erhoehen: 512 → 1024, Overlap 50 → 128
**Problem:** NAACL 2025 Vectara-Studie zeigt: fuer analytische/juristische Queries sind 512-1024 Token optimal. Unsere 512-Zeichen-Chunks (= ~128 Token) sind deutlich zu klein.
**Unsere Lessons Learned:** "Chunks werden mitten im Absatz abgeschnitten. Artikel- und Paragraphennummern fehlen."
**Aenderung:** Config-Parameter in `ingest-phase-h.sh` anpassen.
| Metrik | Vorher | Nachher |
|--------|--------|---------|
| Chunk Size | 512 chars (~128 Token) | 1024 chars (~256 Token) |
| Overlap | 50 chars (10%) | 128 chars (12.5%) |
**Impact:** HOCH | **Effort:** NIEDRIG
### 2. Ollama JSON-Mode fuer Obligation Extraction
**Problem:** `_parse_json` in `decomposition_pass.py` hat Regex-Fallback — das zeigt, dass LLM-Output nicht zuverlaessig JSON ist.
**Aenderung:** `format: "json"` in Ollama-API-Calls setzen.
**Impact:** MITTEL | **Effort:** NIEDRIG (1 Parameter)
### 3. Chain-of-Thought Prompting fuer Pass 0a/0b
**Problem:** LegalGPT-Framework zeigt: explizite Reasoning-Chains ("Erst Addressat identifizieren, dann Aktion, dann normative Staerke") verbessern Extraktionsqualitaet signifikant.
**Impact:** MITTEL | **Effort:** NIEDRIG (Prompt Engineering)
---
## Tier 2: High Impact, Medium Effort (1-2 Wochen)
### 4. Hybrid Search (Dense + Sparse) via Qdrant
**Problem:** Reine Dense-Suche. Juristische Queries enthalten spezifische Begriffe ("DSGVO Art. 35", "Abs. 3"), die BM25/Sparse besser findet.
**Loesungsansatz:** BGE-M3 generiert bereits Sparse Vectors — wir verwerfen sie aktuell!
```
Qdrant Query API:
- Dense: bge-m3 Cosine (wie bisher)
- Sparse: bge-m3 Sparse Vectors (neu)
- Fusion: Reciprocal Rank Fusion (RRF)
```
**Benchmarks (Anthropic):** 49% weniger fehlgeschlagene Retrievals mit Contextual Retrieval, 67% mit Re-Ranking.
**Impact:** SEHR HOCH | **Effort:** MITTEL
### 5. Cross-Encoder Re-Ranking
**Problem:** Top-5 Ergebnisse direkt an LLM — keine Qualitaetspruefung der Retrieval-Ergebnisse.
**Loesungsansatz:** BGE Reranker v2 (MIT-Lizenz) auf Top-20 Ergebnisse, dann Top-5 an LLM.
| Re-Ranker | Lizenz | Empfehlung |
|-----------|--------|------------|
| BGE Reranker v2 | MIT | Empfohlen |
| Jina Reranker v2 | Apache-2.0 | Alternative |
| ColBERT v2 | MIT | Spaeter |
**Impact:** HOCH | **Effort:** MITTEL
### 6. Cross-Regulation Dedup Pass
**Problem:** Dedup filtert immer nach `pattern_id` — Controls aus DSGVO Art. 25 und NIS2 Art. 21 (beide Security-by-Design) werden nie verglichen.
**Loesungsansatz:** Zweiter Qdrant-Search ohne `pattern_id`-Filter nach dem normalen Dedup-Pass.
**Impact:** HOCH | **Effort:** MITTEL
### 7. Automatische Regressionstests (Golden Set)
**Problem:** Keine systematische Qualitaetsmessung nach Pipeline-Aenderungen.
**Loesungsansatz:** 20-Chunk Golden Set → Control-Generation → Output-Stabilitaet pruefen.
**Impact:** HOCH | **Effort:** NIEDRIG
---
## Tier 3: Strategische Investitionen (Wochen bis Monate)
### 8. Artikel-Boundary Chunking
Eigener Splitter fuer EU-Verordnungen und deutsche Gesetze: Split an "Art.", "Artikel", "Paragraph"-Grenzen statt nach Zeichenzahl.
### 9. RAGAS Evaluation Pipeline
[RAGAS](https://docs.ragas.io/) mit Golden Dataset (50-100 manuell verifizierte Control-to-Source Mappings). Metriken: Faithfulness, Answer Relevancy, Context Precision, Context Recall.
### 10. BGE-M3 Fine-Tuning
Fine-Tuning auf Compliance-Corpus (~6.373 Control-Titel/Objective-Paare). Research zeigt +10-30% Domain-Retrieval-Verbesserung.
### 11. LLM-as-Judge
Claude Sonnet bewertet jeden generierten Control auf Faithfulness zum Quelltext (~$0.01/Control).
### 12. Active Learning aus Review-Queue
Menschliche Entscheidungen der Dedup Review-Queue nutzen, um Schwellenwerte ueber die Zeit zu optimieren.
---
## Nicht empfohlen (niedriger ROI oder Konflikte)
| Ansatz | Grund |
|--------|-------|
| Jina v3 Embeddings | **CC-BY-NC-4.0** — verletzt Open Source Policy |
| Voyage-law-2 | API-only, proprietaer — kein Self-Hosting |
| Semantic Chunking | Benchmarks zeigen keinen Vorteil gegenueber Recursive fuer strukturierte Dokumente |
| HyDE als Primaerstrategie | Latenz (+43-60%) + Halluzinationsrisiko |
| Knowledge Graph RAG | Massiver Aufwand, unklarer Gewinn bei strukturiertem Rechtskorpus |
---
## Embedding-Modell Vergleich
| Modell | MTEB Score | Multilingual | Kontext | Lizenz | Bewertung |
|--------|-----------|-------------|---------|--------|-----------|
| **BGE-M3** (aktuell) | 63.0 | 100+ Sprachen | 8192 Token | MIT | Gut, Dense+Sparse+ColBERT |
| Jina v3 | 65.5 | 89 Sprachen | 8192 Token | CC-BY-NC | Nicht nutzbar (Lizenz!) |
| E5-Mistral-7B | ~65 | Gut | 4096 Token | MIT | Gross, hoher RAM |
| Voyage-law-2 | Best Legal | EN Legal | 16K Token | Proprietaer | Nicht nutzbar (API-only) |
**Fazit:** BGE-M3 bleibt die beste Wahl fuer unseren Stack. Sparse-Vectors aktivieren und Fine-Tuning bringen mehr als ein Modellwechsel.
---
## Test-Coverage Analyse
### Pipeline-Module (567 Tests)
| Modul | Tests | Bewertung | Fehlende Tests |
|-------|-------|-----------|----------------|
| Control Generator | 110 | Exzellent | 10-15 Edge Cases |
| Obligation Extractor | 107 | Exzellent | 8-10 Edge Cases |
| Decomposition Pass | 90 | Exzellent | 5-8 Edge Cases |
| Pattern Matcher | 72 | Gut | 10-15 Edge Cases |
| Control Dedup | 56 | Exzellent | 5-8 Edge Cases |
| Control Composer | 54 | Gut | 8-10 Edge Cases |
| Pipeline Adapter | 36 | Gut | 10-15 Edge Cases |
| Citation Backfill | 20 | Moderat | 5-8 Edge Cases |
| License Gate | 12 | Minimal | 5-8 Edge Cases |
| RAG Client | 10 | Minimal | 5-8 Edge Cases |
### Kritische Luecken (fehlende Tests)
| Service | Datei | Prioritaet |
|---------|-------|------------|
| AI Compliance Assistant | `ai_compliance_assistant.py` | HOCH (25-30 Tests noetig) |
| PDF Extractor | `pdf_extractor.py` | HOCH (20-25 Tests noetig) |
| LLM Provider | `llm_provider.py` | HOCH (15-20 Tests noetig) |
| Similarity Detector | `similarity_detector.py` | MITTEL (20-25 Tests noetig) |
| Anchor Finder | `anchor_finder.py` | MITTEL |
### Test-Infrastruktur
**Fehlend:** Shared `conftest.py` mit gemeinsamen Fixtures (LLM-Mock, DB-Mock, Embedding-Mock). Aktuell sind Fixtures in jedem Test-File dupliziert.
---
## Quellen
- [NAACL 2025 Vectara Chunking Study](https://blog.premai.io/rag-chunking-strategies-the-2026-benchmark-guide/)
- [Anthropic Contextual Retrieval](https://www.anthropic.com/news/contextual-retrieval)
- [Qdrant Hybrid Search Query API](https://qdrant.tech/articles/hybrid-search/)
- [Structure-Aware Chunking for Legal (ACL 2025)](https://aclanthology.org/2025.justnlp-main.19/)
- [RAGAS Evaluation Framework](https://docs.ragas.io/)
- [BGE Reranker v2 (MIT)](https://huggingface.co/BAAI/bge-reranker-v2-m3)
- [LegalGPT / CALLM Framework](https://www.emergentmind.com/topics/compliance-alignment-llm-callm)

View File

@@ -0,0 +1,223 @@
# RAG Pipeline: Lessons Learned & Hardening
## Übersicht
Dieses Dokument beschreibt die Erkenntnisse aus dem Aufbau der RAG-Pipeline und die daraus abgeleiteten Maßnahmen zur Härtung. Es dient als Referenz für zukünftige Ingestion-Runs und Pipeline-Erweiterungen.
## Architektur: Wann brauchen wir RAG vs. Direct PDF?
### RAG ist nötig für:
| Use Case | Warum RAG? |
|---|---|
| **Compliance Advisor (Chat)** | Semantische Suche über 38+ Dokumente in Echtzeit |
| **Cross-Regulation Mapping** | "Zeige alle Anforderungen zu Verschlüsselung" über alle Quellen |
| **Customer Scope-Filtering** | Nur Chunks aus relevanten Regulations für den Kunden |
| **Inkrementelle Updates** | Neues Dokument → nur neue Chunks verarbeiten |
### RAG ist NICHT nötig für:
| Use Case | Besser: Direct PDF |
|---|---|
| **Control-Generierung (Batch)** | PDF → PyMuPDF → Strukturparser → Artikel-Index → API |
| **PDF-QA/Verifizierung** | Substring-Match direkt im PDF (schneller, exakter) |
| **Artikel/§-Extraktion** | Regex-basierte Extraktion aus PDF-Text |
### Hybrid-Ansatz (Empfehlung)
```
Control-Generierung: PDF → Strukturparser → Artikel-Index → Anthropic API
(KEIN RAG nötig, direkt aus PDF)
Runtime-Betrieb: Qdrant-RAG für semantische Suche, Chat, Scope-Analyse
(RAG mit angereicherten Chunks + Struktur-Metadaten)
```
## Fehler und Root Causes
### 1. Doppelte Ingestion = Doppelte Controls
**Problem:** Gleiche PDFs unter verschiedenen Namen ingestiert (z.B. "Maschinenverordnung" und "Verordnung (EU) 2023/1230") → unterschiedliche Chunks (anderes Chunking) → anderer Hash → doppelt verarbeitet → doppelte Controls.
**Root Cause:**
- `regulation_name` aus Chunk-Metadaten statt aus kanonischer Quelle
- UNIQUE-Constraint nur `(chunk_hash, collection, document_version)` — nicht global
- Kein Check ob `regulation_code` bereits in einer Collection existiert
**Fix (implementiert):**
- `REGULATION_LICENSE_MAP` enthält jetzt kanonische `name`-Werte die den DB-Einträgen entsprechen
- `source_citation.source` wird aus `REGULATION_LICENSE_MAP.name` genommen, NICHT aus `chunk.regulation_name`
- Phase 5 Cleanup: 3.301 Duplikate hart gelöscht
**Fix (noch offen):**
- Chunk-Hash UNIQUE Constraint global machen: `(chunk_hash, document_version)` statt `(chunk_hash, collection, document_version)`
- Vor Ingestion: Check ob `regulation_code` bereits in einer Collection existiert
### 2. Chunks verlieren Strukturinformation
**Problem:** Chunks werden mitten im Absatz abgeschnitten. § und Artikelnummern fehlen in den Chunk-Metadaten. Kontext des Kapitels/Abschnitts geht verloren.
**Root Cause:**
- `chunk_strategy=recursive` mit `chunk_size=512, chunk_overlap=50` — zu kleine Chunks
- Chunking beachtet keine Dokumentstruktur (Artikel-/Paragraphengrenzen)
- Keine Einleitung/Kapitelkontext als Prefix
**Empfehlung für Re-Ingestion:**
- **Strukturiertes Chunking:** Chunks an Artikel-/Paragraphengrenzen schneiden
- **Kontext-Prefix:** Kapiteleinleitung und übergeordnete Struktur mitliefern
- **Metadaten anreichern:** `article`, `paragraph`, `article_type`, `section_hierarchy`
- **Größere Chunks:** Mindestens 1024 Tokens, besser volle Artikel/Paragraphen
### 3. Cross-Collection-Duplikate
**Problem:** `nist_csf_2_0` in `bp_compliance_ce` (67 Chunks) UND `bp_compliance_datenschutz` (162 Chunks). EU-Verordnungen sowohl in `bp_compliance_ce` als auch `bp_compliance_gesetze`.
**Root Cause:** Keine Collection-Zuordnungsregeln. Manuelle Zuweisung bei Ingestion.
**Fix:** `cleanup-qdrant-duplicates.py` Script bereinigt Cross-Collection-Duplikate.
**Empfehlung:** Klare Collection-Zuordnungsregeln:
- `bp_compliance_ce` = EU-Verordnungen + internationale Standards
- `bp_compliance_gesetze` = Deutsche + österreichische Gesetze (NUR nationale Gesetze)
- `bp_compliance_datenschutz` = EDPB/WP29 Leitlinien + Privacy Frameworks
### 4. OWASP Multilingual Controls
**Problem:** 324 OWASP Top 10 Controls in ZH, AR, ID, FR, ES, PT — Übersetzungen derselben 10 Kategorien. Kein Mehrwert, aber 324 doppelte Controls generiert.
**Root Cause:** Multilingual PDFs/GitHub-Quellen ohne Spracherkennung ingestiert.
**Fix:** 324 als `duplicate` markiert und gelöscht.
**Empfehlung:** Bei Ingestion Spracherkennung + Deduplizierung. Nur DE + EN behalten.
### 5. Fehlende Artikel/Paragraph-Extraktion
**Problem:** Chunks haben `article` und `paragraph` oft leer oder falsch. Die LLM-basierte Extraktion bei der Control-Generierung ist unzuverlässig.
**Root Cause:** Ingestion-Pipeline extrahiert keine Strukturinformation aus dem PDF.
**Fix (implementiert):** PDF-QA-Pipeline (`pdf_qa_all.py`) matched `source_original_text` gegen Original-PDFs und extrahiert korrekte Artikel/Paragraphen — 86% Match-Rate.
**Empfehlung:** Bei Re-Ingestion direkt in den Chunk-Metadaten speichern.
### 6. Job-Tracking nicht persistent
**Problem:** Generation-Jobs laufen als Background-Tasks. Kein Logging, welche Chunks verarbeitet, Status nur über API abfragbar. Bei API-Timeout oder Restart geht der Fortschritt verloren.
**Root Cause:** `asyncio.create_task()` hat keinen Recovery-Mechanismus.
**Fix (teilweise):** `canonical_generation_jobs` Tabelle trackt Jobs. `canonical_processed_chunks` markiert verarbeitete Chunks.
**Empfehlung:**
- Job-Log in DB persistieren (nicht nur stdout)
- Fortschritt in `canonical_generation_jobs.progress` als JSONB speichern
- Chunk-Level-Status: verarbeitet / übersprungen / Fehler
- Recovery-Fähigkeit: Job kann von letztem Checkpoint fortgesetzt werden
## Empfohlene Metadaten für Re-Ingestion
### Chunk-Level Metadaten (Qdrant Payload)
```json
{
"chunk_text": "...",
"regulation_code": "eu_2016_679",
"regulation_name_de": "DSGVO (EU) 2016/679",
"regulation_name_en": "GDPR (EU) 2016/679",
"article": "25",
"article_title": "Datenschutz durch Technikgestaltung und datenschutzfreundliche Voreinstellungen",
"article_type": "article",
"paragraph": "1",
"section_hierarchy": ["Kapitel IV", "Abschnitt 2", "Artikel 25"],
"chapter_context": "Kapitel IV — Verantwortlicher und Auftragsverarbeiter",
"pages": [45, 46],
"effective_date": "2018-05-25",
"publication_date": "2016-04-27",
"document_version": "2016-04-27",
"source_language": "de",
"source_url": "https://eur-lex.europa.eu/...",
"celex": "32016R0679",
"license": "EU_LAW",
"license_rule": 1,
"source_type": "law",
"category": "datenschutz",
"chunk_position": 42,
"total_chunks": 423
}
```
### Dokument-Level Metadaten (Corpus Version)
```json
{
"regulation_code": "eu_2016_679",
"canonical_name_de": "DSGVO (EU) 2016/679",
"canonical_name_en": "GDPR (EU) 2016/679",
"document_type": "eu_regulation",
"effective_date": "2018-05-25",
"publication_date": "2016-04-27",
"supersedes": null,
"superseded_by": null,
"source_pdf": "gdpr_regulation_eu_2016_679.pdf",
"source_pdf_sha256": "abc123...",
"total_articles": 99,
"total_recitals": 173,
"total_annexes": 0,
"ingestion_date": "2026-03-20",
"ingestion_version": "v2"
}
```
## Pipeline-Härtung Checkliste
### Vor Ingestion
- [ ] Prüfen ob `regulation_code` bereits in einer Collection existiert
- [ ] PDF-SHA256 gegen bekannte PDFs prüfen (Duplikat-Erkennung)
- [ ] `regulation_name` aus `REGULATION_LICENSE_MAP` verwenden, NICHT aus Chunk-Metadaten
- [ ] Spracherkennung: Nur DE + EN ingestieren
- [ ] Dokument-Metadaten (effective_date, publication_date) recherchieren
### Während Ingestion
- [ ] Strukturiertes Chunking an Artikel-/Paragraphengrenzen
- [ ] Kontext-Prefix mit Kapiteleinleitung
- [ ] Chunk-Metadaten anreichern (article, paragraph, article_type, section_hierarchy)
- [ ] Fortschritt in DB loggen
### Nach Ingestion
- [ ] Chunk-Count pro `regulation_code` prüfen (Sanity Check)
- [ ] PDF-QA gegen Original-PDF laufen lassen
- [ ] Cross-Collection-Duplikat-Check
- [ ] Corpus-Version in DB eintragen
### Control-Generierung
- [ ] `source_citation.source` aus `REGULATION_LICENSE_MAP.name`, NICHT aus Chunk-Metadaten
- [ ] Harmonisierung: Threshold 0.85 für Duplikate innerhalb gleicher `regulation_code`
- [ ] Cross-Regulation-Harmonisierung bei ähnlichen Themen (z.B. DSGVO Art. 25 ↔ NIS2 Art. 21)
- [ ] Job-Fortschritt persistent in DB speichern
## Workflow: Mac Mini → Production Sync
```
1. Mac Mini: PDF → Qdrant (lokal, http://macmini:6333)
2. Mac Mini: Control-Generierung → PostgreSQL (shared, 46.225.100.82:54321)
3. QA: PDF-Match, Dedup, Source-Normalisierung
4. Qdrant Migration: macmini:6333 → qdrant-dev.breakpilot.ai (scripts/migrate-qdrant.py)
5. Deploy: git push gitea → Coolify Build + Deploy
```
**WICHTIG:** PostgreSQL ist SHARED — Änderungen auf Mac Mini sind sofort in Production sichtbar. Qdrant hat getrennte Instanzen (lokal + production) und muss manuell synchronisiert werden.
## Scripts
| Script | Beschreibung |
|---|---|
| `scripts/ingest-phase-h.sh` | Haupt-Ingestion: 38 Dokumente → Qdrant |
| `scripts/cleanup-qdrant-duplicates.py` | Qdrant Duplikat-Cleanup (8 Schritte) |
| `scripts/migrate-qdrant.py` | Qdrant Migration: lokal → production |
| `scripts/qa/phase5_normalize_and_cleanup.py` | DB Normalisierung + Hard Delete |
| `scripts/qa/pdf_qa_all.py` | PDF-Match QA |