M1+M2: RAG-Pipeline mit verbindlichem Grounding

agent/-Paket: Ingest (601 Layer-2-Eintraege -> 3005 Chunks, FTS5-BM25 +
Vektoren-Cache), Hybrid-Retrieval (RRF, Stand-Boost, cross_ref-Erweiterung),
Ollama-Client (embed/chat, think-Flag-Fallback, kurzes Connect-Budget),
Systemprompt mit Zitierpflicht, Post-Validierung (zitierte IDs gemaess
Retrieved-Set, 1x Regenerierung, dann Verweigerung), FastAPI (/ask, /health,
/reindex), CLI, Goldset (31 Fragen, IDs gegen kb.json verifiziert, inkl.
ATZ-Konfliktfall + 4 Verweigerungsfaelle), Eval-Suite, Test-Chat.

41 Offline-Tests gruen. Baseline BM25-only: Hit-Rate 0,871 / Recall@8 0,855 /
MRR 0,476. Hybrid-Messung, Antwortmodus-Eval und Modell-Bake-off (M3) auf
dem Host ausstaendig (Ollama aus der Zed-Sandbox nicht erreichbar).

MEMORY.md und planung.md Umsetzungsstand aktualisiert.
This commit is contained in:
2026-09-14 16:53:04 +02:00
parent bf8191b013
commit 2cba72aeb0
26 changed files with 2574 additions and 1 deletions
+69
View File
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PV RAG Agent — Test-Chat</title>
<style>
:root { color-scheme: light dark; }
body { font-family: system-ui, sans-serif; max-width: 780px; margin: 2rem auto; padding: 0 1rem; }
h1 { font-size: 1.2rem; }
#log { display: flex; flex-direction: column; gap: 0.8rem; margin: 1.5rem 0; }
.msg { border: 1px solid rgba(128,128,128,.35); border-radius: 8px; padding: .7rem .9rem; white-space: pre-wrap; }
.q { background: rgba(128,128,128,.12); }
.src { font-size: .78rem; margin-top: .4rem; display: flex; flex-wrap: wrap; gap: .3rem; }
.chip { border-radius: 999px; padding: .1rem .5rem; background: rgba(128,128,128,.15); }
.warn { border-color: #c08020; }
form { display: flex; gap: .5rem; }
input { flex: 1; padding: .5rem; border-radius: 6px; border: 1px solid rgba(128,128,128,.5); }
button { padding: .5rem 1rem; border-radius: 6px; cursor: pointer; }
.meta { font-size: .75rem; opacity: .7; }
</style>
</head>
<body>
<h1>PV RAG Agent — Wissensbasis Personalverrechnung</h1>
<p class="meta">Antworten ausschließlich aus der kuratierten Wissensbasis, mit ID- und Stand-Beleg.</p>
<div id="log"></div>
<form id="f">
<input id="q" placeholder="Frage zur österreichischen Personalverrechnung …" autocomplete="off" required>
<button>Senden</button>
</form>
<script>
const log = document.getElementById("log");
function add(cls, html) {
const d = document.createElement("div");
d.className = "msg " + cls;
d.innerHTML = html;
log.appendChild(d);
}
document.getElementById("f").addEventListener("submit", async (e) => {
e.preventDefault();
const q = document.getElementById("q").value.trim();
if (!q) return;
add("q", q);
document.getElementById("q").value = "";
try {
const r = await fetch("/ask", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question: q }),
});
if (!r.ok) {
add("warn", "Fehler " + r.status + ": " + (await r.text()));
return;
}
const d = await r.json();
const chips = (d.sources || []).map(s =>
`<span class="chip">${s.id} · ${s.stand || ""}</span>`).join("");
add(d.refused ? "warn" : "",
`${d.answer.replace(/</g, "&lt;")}` +
(d.sources && d.sources.length ? `<div class="src">${chips}</div>` : "") +
`<div class="meta">Modell: ${d.model} · ${d.latency_ms} ms · ` +
`${d.verified ? "zitiergeprüft" : "UNVERIFIZIERT"}${d.refused ? " · verweigert" : ""}</div>`);
} catch (err) {
add("warn", "Netzwerkfehler: " + err);
}
});
</script>
</body>
</html>