Files
odoo-at-payroll/pv-agent/agent/README.md
T
fegger b3e8e47cdd Prompt v2 + Kontext-Section-Prioritaet: Fehlverweigerungen behoben (D8)
Diagnose: q-008 bekam die 'Verweise'-Navigationssektion von lb-end-03 als
Kontext (BM25-Langennormalisierung bevorzugt duenne Chunks) -> inhaltlose
Bloecke -> korrekte Verweigerung nach Regel 4. q-031 enthaelt eine falsche
Praemisse (kein 'Mindestlohngesetz' in Oesterreich; Wissensbasis sagt:
kein gesetzlich betraglich festgelegtes Mindestentgelt).

Fixes: (a) retrieve.py _representative_chunk - pro Eintrag beste Inhalts-
sektion (Zusammenfassung > Kernwerte > Rechtsgrundlagen > Payroll >
sonstige > Verweise), Section-Swap aus dem Index falls nur 'Verweise'
rangiert; (b) generate.py Prompt v2 - Regel 4 erlaubt Teilantworten,
Regel 8 verlangt Prämisse-Korrektur mit Muster-Beispiel.

Bestaetigungslauf qwen3.8:27b (35 Fragen): Zitier-Praezision 100 % (8
Regenerierungen, alle geheilt), Verweigerung korrekt 94,3 %, erwartete
Quelle 83,9 % (v1 80,6 %), mean 34 s. q-008 + q-031 behoben; verbleibender
bekannter Fall q-029 (breite Survey-Frage, sicherer Fehlermodus).
Retrieval-Metriken unveraendert (Recall@8 0,952). 41 Tests gruen.
2026-09-15 00:22:41 +02:00

142 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PV RAG Agent
Lokaler RAG-Agent für österreichische Personalverrechnung: beantwortet
Fragen **ausschließlich** aus der kuratierten Wissensbasis (Layer 2,
`wissensbasis/`, 601 Einträge) — 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_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).
## 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
tests/ 41 Tests (offline, Fake-Ollama)
data/ index.db (gitignored)
```
## Tests
```bash
python -m pytest -q # 41 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.