- Planer liefert jetzt type: survey|specific. Survey-Fragen (Uebersicht ueber viele Dokumente) laufen ueber Map-Reduce: Retrieval auf survey_blocks=16 erweitert, ein Map-Call destilliert JE Block als Stichpunkte mit seiner KB-ID (MAP_SYSTEM_PROMPT), ein Reduce-Call synthetisiert die Endantwort. Grounding unveraendert: Zitier-Validierung strikt ueber die Retrieved-Union; leerer Map-Output -> Fallback auf Einzelantwort. - Systemprompt-Regel 9: haengt die Antwort wesentlich von nicht genanntem Kontext ab (Branche, Bundesland, Zeitraum), belegte allgemeine Aussage plus EINE Rueckfrage statt Verweigerung (API-first; Odoo-Chat kann die Rueckfrage als Follow-up nutzen). - Ergebnis: q-029 (WIKU-Survey, bisher hartnaeckigste Fehlverweigerung) geheilt - Teilantwort mit 5 belegten Heften; q-015 antwortet mit expliziter KV-Abhaengigkeit + Rueckfrage statt Branchen-Noise. - Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 % (Gate erfuellt), erwartete Quelle 90,2 %, Latenz mean 34,2 s (Map-Reduce nur bei Survey-Fragen, ~95 s). Report lokal data/eval-qwen38-stage2.json. - Tests 57 -> 59 (Survey-Integration, Typ-Parsing).
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
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_PER_QUERY_SLOTS |
2 |
Multi-Query: garantierte Kontext-Slots je Sub-Query (Multi-Hop-Abdeckung) |
PV_QUERY_PLANNER |
true |
Query-Planer an (Heuristik-Gate entscheidet je Frage) |
PV_PLANNER_MODEL |
leer = Antwortmodell | Modell des Planer-Calls |
PV_TEMPORAL_BOOST |
0.0 |
Bonus für kv-Einträge im gefragten Geltungsjahr |
PV_SURVEY_BLOCKS |
16 |
Map-Reduce: breiteres Retrieval für Survey-Fragen (Typ survey vom Planer) |
PV_NUM_CTX |
32768 |
Modell-Kontextfenster (KV-Chunks überschreiten 16k — Overflow trunciert den Systemprompt) |
PV_MAX_CONTEXT_CHARS |
90000 |
User-Content-Budget; niedrig gerankte Blöcke werden ganz weggelassen (trim_results) |
PV_CONTEXT_BLOCKS |
8 |
Kontextblöcke im Prompt |
PV_PORT |
8080 |
API-Port |
Deployment auf dem Host (Ollama-Maschine)
# 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.
Prompt-Budget + Kontext-Abdeckung (2026-09-15, D11): Lange KV-Chunks
überlieferten num_ctx=16384 — Ollama trunciert den Systemprompt vorn,
das Modell verliert die Zitierregeln (Fehlverweigerungen, „Block-N“-
Zitate). Fix: num_ctx 32768, trim_results (max_context_chars=90 000,
Tail-Blöcke ganz weg) und cross_ref_expand 6 (Top-3 sind oft Branchen-
KV-Blöcke mit leeren cross_refs — kuratierte Nachbarn kamen nie nach).
Alle 4 Fehlverweigerungs-Fälle geheilt; 10/10 Bestätigungsläufe OK.
Retrieval: Recall@8 0,946 (>0,9 ✓) · Hit-Rate 0,973 · MRR 0,671.
Antworten (qwen3.8:27b, 42 Fragen): Zitier-Präzision 97,6 % (1
transienter Fall) · Verweigerung korrekt 97,6 % (>94,3 %-Gate ✓) ·
erwartete Quelle zitiert 91,9 % · Latenz mean 33 s / p95 57 s ·
6 Regenerierungen. Reports: data/eval-qwen38-kvris.json (vor Tuning),
data/eval-qwen38-kvris-tuned.json. Offen: q-015 Branchen-Noise,
q-024 transiente Flakiness (leerer Draft).
Komplexe-Fragen Stufe 1 (2026-09-15, M6/D12): Query-Planer (Heuristik-
Gate → kleiner LLM-Call, 1-3 Sub-Queries als JSON; Stand-Jahr + Scope
je Sub-Query), Multi-Query-Retrieval mit Per-Query-Slots (2 je Sub-Query)
und Scope-Filter („gesetz“ = nur Lexis/WIKU/RIS, „kv“ = nur Branchen-KV;
Fallback unscoped). Grounding unverändert: eine Retrieved-Menge, eine
Antwort, Post-Validierung über die Union. Eval (46 Fragen): Zitier-
Präzision 97,8 % · Verweigerung korrekt 97,8 % (Gate ✓) · erwartete
Quelle 90,2 % · Latenz mean 33,5 s. Komplexe Goldset-Fragen: q-110–113
(Temporal 2023/2025, Abfertigung-Vergleich, Gesetz+KV-Multi-Source).
Report: data/eval-qwen38-stage1-final.json. Offen: q-029 Survey
(Stufe-2-Hebel: Map-Reduce), q-015 Branchen-Noise → API-first-Rückfrage
(Stufe 2).
Antworttyp-Routing Stufe 2 (2026-09-15, M6/D13): Planer liefert
type: survey|specific. Survey-Fragen („Welche Neuerungen …“) →
Map-Reduce: Retrieval auf survey_blocks (16) erweitert, ein Map-Call
destilliert jeden Block als Stichpunkte mit seiner KB-ID, ein Reduce-Call
synthetisiert die Endantwort — Zitier-Validierung weiterhin strikt über
die Retrieved-Union. Regel 9 (Kontext-Abhängigkeit): belegte allgemeine
Aussage + eine Rückfrage statt Verweigerung (Branche/Bundesland/Zeitraum).
Ergebnisse: q-029 geheilt (5 belegte Hefte, ~95 s Map-Reduce-Latenz),
q-015 antwortet mit KV-Abhängigkeit + Rückfrage. Eval (46 Fragen):
Zitier-Präzision 97,8 % · Verweigerung 97,8 % (Gate ✓) · erwartete
Quelle 90,2 % · Latenz mean 34,2 s. Report: data/eval-qwen38-stage2.json.
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
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.