Files
pv-agent/agent/README.md
T
fegger 062e010d7b Stufe 1: komplexe Fragen via Query-Planer, Per-Query-Slots, Scope-Filter (D12, M6)
- Query-Planer (agent/query_planner.py): Heuristik-Gate (Jahr, Vergleichs-/
  Aggregationsmarker, Laenge) entscheidet ueber einen kleinen LLM-Call,
  der komplexe Fragen in 1-3 Sub-Queries zerlegt (JSON, temp 0; Fehler ->
  Original als Einzel-Query). Je Sub-Query optional stand_year und scope.
- Multi-Query-Retrieval: je Sub-Query BM25+Dense mit RRF-Summe;
  Per-Query-Slots (2 je Sub-Query) sichern jeden Frageaspekt im Kontext
  (sonst dominieren Eintraege, die in mehreren Sub-Queries mittelgut
  matchen — q-113-Befund). Scope-Filter: 'gesetz' (nur Lexis/WIKU/RIS)
  und 'kv' (nur Branchen-KV) mit Fallback auf unscoped bei leerem
  Ergebnis. Temporal-Intent: stand_year + temporal_boost (default 0,
  FTS-jahr-Tag-Signal reichte).
- Grounding unveraendert: eine Retrieved-Menge (Union), eine Antwort,
  Post-Validierung ueber die Union, Verweigerungspflicht unveraendert.
- Goldset 42 -> 46: q-110 (Temporal 2023; 2024er-Lohnordnung existiert
  im Korpus nicht - Mantelvertrag ohne Lohntabelle, korrekt verweigert),
  q-111 (2025), q-112 (Abfertigung-Vergleich), q-113 (Gesetz+KV).
- Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 %
  (Gate >94,3 % erfuellt), erwartete Quelle 90,2 %, Latenz mean 33,5 s.
- Tests 50 -> 57. Reports lokal: data/eval-qwen38-stage1*.json.
  API-first festgehalten (D13-Vorbereitung): Odoo bleibt duenner Client;
  Lohndaten-Zugriff in M4 erfordert Privacy-Neubewertung.
2026-09-15 09:58:25 +02:00

9.2 KiB
Raw Blame History

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_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-110113 (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).

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.