# PV RAG Agent Lokaler RAG-Agent für österreichische Personalverrechnung: beantwortet Fragen **ausschließlich** aus der kuratierten Wissensbasis (Layer 2, `wissensbasis/`, **1274 Einträge**: 601 lb/wk-Kuratierung + 614 WKO-KV- Dokumente + 59 RIS-Gesetze) — mit ID- und Stand-Beleg, ohne Trainingswissen, ohne Web-Zugriff. Verbindliche Regeln: `.agents/skills/pv-rag-agent/SKILL.md`, Plan: `planung.md`. ## Architektur (Kurzfassung) ``` wissensbasis/dokumente/*.md ──ingest──▶ data/index.db ├─ chunks (FTS5, BM25, Umlaut-Folding) ├─ vectors (bge-m3, Content-Hash-Cache) └─ Metadaten (stand, topic, tags, …) Frage ──retrieve──▶ Hybrid BM25+Dense (RRF) + cross_ref-Erweiterung ──generate──▶ Ollama (Systemprompt, Zitierpflicht) ──validate──▶ zitierte IDs ⊆ Retrieved-Set? sonst 1× regenerieren, dann verweigern ``` - **Nur Layer 2** als Korpus (kuratiert, lizenzkonform). Layer-1-Volltexte (`.lexis360/`, `.wiku/`) bleiben außen vor (offener Lizenzpunkt). - **Kein Ausweg nach außen:** keine Tools, kein Browsing — der einzige HTTP-Client spricht mit Ollama. - **Verweigerungspflicht:** leeres/schwaches Retrieval → deterministische Antwort „Dazu enthält die Wissensbasis keine Aussage." (kein LLM-Call). ## Schnellstart ```bash pip install -r requirements.txt # 1) Index bauen (mit Embeddings, wenn Ollama erreichbar) python -m agent.cli ingest # --no-embed erzwingt BM25-only # 2) Frage im Terminal python -m agent.cli ask "Wie hoch ist die AMS-Ersatzquote bei geblockter Altersteilzeit?" # 3) Goldset-Evaluation (offline: Retrieval-Metriken) python -m agent.cli eval # inkl. Antworten + Verweigerungsfälle (benötigt Ollama): python -m agent.cli eval --answers --json-out data/eval-report.json # 4) HTTP-API + Test-Chat python -m agent.cli serve # http://127.0.0.1:8080 (/ask, /health, /reindex) ``` ## Konfiguration (Umgebungsvariablen) | Variable | Default | Bedeutung | |---|---|---| | `OLLAMA_URL` | `http://100.103.83.12:11435` | Ollama-Ziel-Instanz — Remote-GPU-Maschine im Tailscale-Netz (nicht localhost:11434 — das ist ein anderer, lokaler Ollama) | | `PV_ANSWER_MODEL` | `qwen3.8:27b` | Antwortmodell (provisorisch bis Bake-off M3) | | `PV_EMBED_MODEL` | `bge-m3` | Embedding-Modell | | `PV_DB_PATH` | `data/index.db` | SQLite-Index | | `PV_KB_DIR` | `wissensbasis` | Wissensbasis-Verzeichnis | | `PV_THINK` | `false` | Thinking per Request (qwen3.8: default an) | | `PV_EMBED_OFF` | `false` | `true` = BM25-only | | `PV_CANDIDATE_POOL` | `150` | Kandidaten je Liste vor der Fusion (KV/RIS-Erweiterung: Longtail-Spezialisten in der Kandidatur halten) | | `PV_RRF_K` | `20` | RRF-Konstante (erweiterter Korpus: Top-Ränge dominant) | | `PV_DENSE_WEIGHT` | `2.0` | RRF-Gewicht der Dense-Liste relativ zu BM25 (BM25 ist durch KV-§-Titel-Matches inflationiert) | | `PV_CONTEXT_BLOCKS` | `8` | Kontextblöcke im Prompt | | `PV_PORT` | `8080` | API-Port | ## Deployment auf dem Host (Ollama-Maschine) ```bash # Ollama-Ziel-Instanz prüfen (Custom-Port! Achtung: auf dem Host läuft # zusätzlich eine fast leere Instanz auf 11434 — nicht verwexseln) curl http://100.103.83.12:11435/api/tags # qwen3.8:27b, bge-m3, Bake-off-Feld installiert # Vollständiger Index (BM25 + Dense) python -m agent.cli ingest python -m agent.cli eval python -m agent.cli eval --answers --json-out data/eval-report.json # Zitier-Präzision, Verweigerungen, Latenz ``` ## Baseline (2026-09-14, Hybrid BM25 + bge-m3, Ollama :11435) **Retrieval** (Goldset, 31 Fragen): Hit-Rate 0,968 · **Recall@8 0,952** · MRR 0,690 — M1-Ziel >0,9 erreicht (BM25-only war 0,855; die vier BM25-Fehltreffer behebt die Dense-Suche alle). **Antworten** (Bake-off-Sieger qwen3.8:27b, Thinking aus, Temperatur 0,1): **Zitier-Präzision 100 %** · Verweigerung korrekt 94,3 % · erwartete Quelle zitiert **83,9 %** · Latenz mean 34 s / p95 54 s. **Prompt v2 + Kontext-Section-Priorität (2026-09-14):** Teilantworten bei unvollständiger Deckung erlaubt (Regel 4), Prämisse-Korrektur statt Verweigerung (Regel 8), pro Eintrag beste Inhaltssektion als Kontextblock (Zusammenfassung > Kernwerte > … > Verweise zuletzt — Navigations-Chunks lösen keine Fehlverweigerungen mehr aus). v1→v2: q-008 + q-031 behoben, erwartete Quelle 80,6 % → 83,9 %, Zitier-Präzision unverändert 100 %. **Modell-Bake-off (M3, 2026-09-14)** — Entscheidung: **qwen3.8:27b** (Protokoll: Zitier-Präzision → Verweigerungskorrektheit → Latenz): | Kandidat | Zitier-Präz. | Verweig. korrekt | Erw. Quelle | mean/p95 | |---|---|---|---|---| | **qwen3.8:27b** | **100 %** | **94,3 %** | **80,6 %** | 32 s / 53 s | | gemma4:26b | 100 % | 91,4 % | 67,7 % | **7,9 s** / 12 s | | gemma4:12B | 94,3 % | 91,4 % | 77,4 % | 21 s / 40 s | | qwen3.6:27B | 94,3 % | 85,7 % | 80,6 % | 43 s / 89 s | | muse-glimmer:latest | 100 % | 77,1 % | 74,2 % | 37,5 s / 51 s | | mistral-small3.1:24b | 100 % | 71,4 % | 58,1 % | 20 s / 43 s | `gemma4:26b` bleibt als dokumentierter Latenz-Kandidat für späteres interaktives Tuning. Bekannte Fehlverweigerungen: q-008 (Abfertigung Verfügungsmöglichkeiten), q-031 (Mindestlohngesetz) — breite Fragen, Retrieval erfolgreich, Modell verweigert trotzdem (sicheres Versagensmuster; M3-Prompt-Tuning-Kandidat). Latenz-Hebel für M3: weniger Kontextblöcke, schnellere Kandidaten (gemma4:12B, MoE). **KV/RIS-Erweiterung + Retrieval-Kalibrierung (2026-09-15):** Korpus 601 → 1274 Einträge (kv-*: 614 WKO-KV-Dokumente quellentreu, ris-*: 59 RIS-Gesetze, nur die im Lexis360-Bestand zitierten §-Auschnitte); 14 984 Chunks. Kalibrierung per Goldset-Sweep (dichte Gewichtung, RRF-k, Pool): Hit-Rate 0,865 → 0,973 · Recall@8 0,851 → **0,923** (>0,9 ✓) · MRR 0,621 → 0,667. Antwortmodus (qwen3.8:27b): Zitier-Präzision 95,2 % (2 Verletzungen), Verweigerung korrekt 90,5 % (4 Fehlverweige- rungen, u.a. q-022/q-029), erwartete Quelle zitiert 83,8 %, Latenz mean 31 s / p95 53 s — **unter den M3-Gates** (100 % / 94,3 %): das Fehlverweigerungs-Tuning (Regel 4/8 + Kontextblöcke) ist weiteres Thema, Prompt bleibt beim laufenden Arbeitsstand eingefroren. ## Dateien ``` agent/ config.py Env-Konfiguration kb.py Layer-2-Parsing + kb.json-Gate normalize.py Umlaut-Folding, FTS-Query-Bau ingest.py Index-Bau (chunks + FTS5 + Vektoren-Cache) retrieve.py Hybrid-Retrieval (BM25 + Dense, RRF, cross_refs) ollama_client.py Ollama-HTTP (embed + chat, think-Fallback) generate.py Systemprompt, Post-Validierung, Verweigerung api.py FastAPI (/ask, /health, /reindex) cli.py ingest | ask | eval | serve eval/ goldset.yaml + evaluate.py web/index.html Minimaler Test-Chat tools/ Intake + Registry (build_registry.py, ingest_sources.py) tests/ 49 Tests (offline, Fake-Ollama) data/ index.db (gitignored) ``` ## Tests ```bash python -m pytest -q # 49 Tests, alle offline ``` ## Lizenz-Disziplin `.lexis360/`, `.wiku/`, `.firecrawl/`, `.ris/` sind lokal und unversioniert (`.gitignore`). Der Index enthält ausschließlich Layer-2-Kuratierung; Layer-1-Prompts wären ein Lizenzverstoß und sind im Code nicht vorgesehen.