SUM-04: Zusammenfassungs-Qualitaetsbewertung #240
No files matched your search
@@ -0,0 +1,69 @@
|
||||
# SUM-04 – Prüfprotokoll
|
||||
|
||||
Umsetzung: `ai/internal/summaryquality` — Qualitätsbewertung von Zusammenfassungen
|
||||
(SUM-01 Dokumente, SUM-02 Threads) über Stichproben und eine einfache
|
||||
Kennzahl (Belegtheit/Groundedness: Anteil der Wörter der Zusammenfassung,
|
||||
die auch im Originaltext vorkommen).
|
||||
|
||||
Testset: 3 Stichproben (`ai/internal/summaryquality/summaryquality_test.go`),
|
||||
je zwei Typen (`document`, `thread`) und ein quellentreuer sowie ein
|
||||
erfundener Fall pro Kategorie.
|
||||
|
||||
## Prüfung 1 – Kennzahl an Testset berechnet und dokumentiert
|
||||
|
||||
Zielwert: `AggregateByType` liefert je Zusammenfassungstyp (document/thread)
|
||||
eine eigene `TypeMetrics` mit Anzahl, mittlerer Belegtheit und Anzahl
|
||||
markierter Ausreißer.
|
||||
|
||||
Ergebnis (Test `TestAggregateByType_PerTypeMetric`, ausgeführt auf
|
||||
root@192.168.1.204):
|
||||
- Typ `document`, 2 Stichproben, quellentreue Zusammenfassung
|
||||
("Der Kunde bestellt Aktenordner für das Archiv.") gegen Original
|
||||
("Der Kunde bestellt zehn Aktenordner für das Archiv am Standort Nord."):
|
||||
Belegtheit 100 % (alle 6 Wörter der Zusammenfassung kommen im Original vor).
|
||||
- Typ `thread`, 1 Stichprobe, erfundene Zusammenfassung
|
||||
("Rakete Mondbasis Ufo Zauberer Drache Einhorn.") gegen dasselbe Original:
|
||||
Belegtheit 0 %.
|
||||
|
||||
Status: bestanden.
|
||||
|
||||
## Prüfung 2 – Auffälliger Testfall im Test markiert
|
||||
|
||||
Zielwert: Eine Zusammenfassung mit erfundenem Inhalt (Wörter, die nicht im
|
||||
Original vorkommen) wird als `Flagged = true` markiert; eine quellentreue
|
||||
Zusammenfassung bleibt unmarkiert. Schwelle: `FlagThreshold = 0.6`
|
||||
(Belegtheit < 60 %).
|
||||
|
||||
Ergebnis (Test `TestEvaluate_FlagsFabricatedSummary`, ausgeführt auf
|
||||
root@192.168.1.204): erfundene Zusammenfassung ("Rakete Mondbasis Ufo
|
||||
Zauberer Drache Einhorn.") → Belegtheit 0 %, `Flagged = true`.
|
||||
Quellentreue Zusammenfassung → Belegtheit 100 %, `Flagged = false`.
|
||||
Zusätzlich geprüft (`TestEvaluate_FlagsEmptySummary`): leere Zusammenfassung
|
||||
wird ebenfalls markiert.
|
||||
|
||||
Status: bestanden.
|
||||
|
||||
## Prüfung 3 – Nachvollziehbarkeit der Bewertung bestätigt
|
||||
|
||||
Zielwert: Jedes `Metric`-Ergebnis enthält ein `Reason`-Feld, das die
|
||||
Bewertung in Textform erklärt (Prozentwert der Belegtheit, Beispiele
|
||||
unbelegter Wörter, Hinweis wenn die Zusammenfassung nicht kürzer als das
|
||||
Original ist).
|
||||
|
||||
Ergebnis (Test `TestEvaluate_ReasonExplainsScore`, ausgeführt auf
|
||||
root@192.168.1.204): `Reason` für die erfundene Zusammenfassung lautet
|
||||
sinngemäß "auffällig: 0% der Wörter der Zusammenfassung kommen im Original
|
||||
vor (nicht belegt u.a.: rakete, mondbasis, ufo)" — nennt konkrete
|
||||
unbelegte Wörter, nicht nur einen Zahlenwert.
|
||||
|
||||
Status: bestanden.
|
||||
|
||||
## Build/Test-Lauf
|
||||
|
||||
```
|
||||
cd ai && go build ./... && go vet ./internal/summaryquality/... && go test ./internal/summaryquality/... -v
|
||||
```
|
||||
Alle 4 Tests bestanden, Build und Vet ohne Befund. Ausgeführt auf
|
||||
root@192.168.1.204 (Testhost), in frischem Klon unter
|
||||
`/root/sum04-work-<timestamp>` — kein Zugriff auf `/root/nexarch-code` oder
|
||||
Host 131/archivdms.
|
||||
@@ -0,0 +1,155 @@
|
||||
// Package summaryquality bewertet erzeugte Zusammenfassungen (SUM-01,
|
||||
// SUM-02) anhand von Stichproben, damit erfundene oder auffällig schlechte
|
||||
// Zusammenfassungen nicht unbemerkt bleiben (SUM-04).
|
||||
package summaryquality
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Zusammenfassungstypen, wie sie summaryapi bedient.
|
||||
const (
|
||||
TypeDocument = "document"
|
||||
TypeThread = "thread"
|
||||
)
|
||||
|
||||
// FlagThreshold ist die Mindest-Belegtheit, unterhalb derer eine
|
||||
// Zusammenfassung als auffällig gilt (AK2).
|
||||
const FlagThreshold = 0.6
|
||||
|
||||
var wordPattern = regexp.MustCompile(`\p{L}+`)
|
||||
|
||||
// Sample ist eine Stichprobe: Original und dazugehörige Zusammenfassung
|
||||
// eines bestimmten Typs.
|
||||
type Sample struct {
|
||||
Type string
|
||||
Source string
|
||||
Summary string
|
||||
}
|
||||
|
||||
// Metric ist das Bewertungsergebnis einer einzelnen Stichprobe.
|
||||
type Metric struct {
|
||||
Sample Sample
|
||||
|
||||
// GroundednessScore ist der Anteil der Wörter aus der Zusammenfassung,
|
||||
// die auch im Original vorkommen (0..1) — die einfache Kennzahl für
|
||||
// Quellentreue (Produkt-DNA: keine Zusammenfassung ohne Rückbezug zum
|
||||
// Original).
|
||||
GroundednessScore float64
|
||||
// Flagged markiert eine auffällig schlechte Zusammenfassung (AK2).
|
||||
Flagged bool
|
||||
// Reason erklärt in Textform, wie es zur Bewertung kam (AK3).
|
||||
Reason string
|
||||
}
|
||||
|
||||
// Evaluate bewertet eine einzelne Stichprobe.
|
||||
func Evaluate(s Sample) Metric {
|
||||
summaryWords := uniqueWords(s.Summary)
|
||||
if len(summaryWords) == 0 {
|
||||
return Metric{
|
||||
Sample: s,
|
||||
Flagged: true,
|
||||
Reason: "zusammenfassung ist leer",
|
||||
}
|
||||
}
|
||||
|
||||
sourceWords := wordSet(s.Source)
|
||||
matched := 0
|
||||
var unmatched []string
|
||||
for _, w := range summaryWords {
|
||||
if sourceWords[w] {
|
||||
matched++
|
||||
} else {
|
||||
unmatched = append(unmatched, w)
|
||||
}
|
||||
}
|
||||
score := float64(matched) / float64(len(summaryWords))
|
||||
|
||||
flagged := score < FlagThreshold
|
||||
reason := reasonFor(score, unmatched, len(s.Summary), len(s.Source))
|
||||
|
||||
return Metric{
|
||||
Sample: s,
|
||||
GroundednessScore: score,
|
||||
Flagged: flagged,
|
||||
Reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
func reasonFor(score float64, unmatched []string, summaryLen, sourceLen int) string {
|
||||
var b strings.Builder
|
||||
if score < FlagThreshold {
|
||||
b.WriteString("auffällig: nur ")
|
||||
} else {
|
||||
b.WriteString("unauffällig: ")
|
||||
}
|
||||
fmt.Fprintf(&b, "%.0f%%", score*100)
|
||||
b.WriteString(" der Wörter der Zusammenfassung kommen im Original vor")
|
||||
if len(unmatched) > 0 {
|
||||
limit := unmatched
|
||||
if len(limit) > 5 {
|
||||
limit = limit[:5]
|
||||
}
|
||||
b.WriteString(" (nicht belegt u.a.: ")
|
||||
b.WriteString(strings.Join(limit, ", "))
|
||||
b.WriteString(")")
|
||||
}
|
||||
if sourceLen > 0 && summaryLen >= sourceLen {
|
||||
b.WriteString("; zusammenfassung ist nicht kürzer als das original")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func uniqueWords(s string) []string {
|
||||
set := map[string]bool{}
|
||||
var out []string
|
||||
for _, w := range wordPattern.FindAllString(strings.ToLower(s), -1) {
|
||||
if !set[w] {
|
||||
set[w] = true
|
||||
out = append(out, w)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func wordSet(s string) map[string]bool {
|
||||
set := map[string]bool{}
|
||||
for _, w := range wordPattern.FindAllString(strings.ToLower(s), -1) {
|
||||
set[w] = true
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// TypeMetrics fasst die Bewertungen aller Stichproben eines
|
||||
// Zusammenfassungstyps zusammen (AK1).
|
||||
type TypeMetrics struct {
|
||||
Type string
|
||||
Count int
|
||||
AverageGroundedness float64
|
||||
FlaggedCount int
|
||||
}
|
||||
|
||||
// AggregateByType berechnet je Zusammenfassungstyp die Kennzahl über alle
|
||||
// übergebenen Metriken (AK1).
|
||||
func AggregateByType(metrics []Metric) map[string]TypeMetrics {
|
||||
result := map[string]TypeMetrics{}
|
||||
sums := map[string]float64{}
|
||||
for _, m := range metrics {
|
||||
t := m.Sample.Type
|
||||
agg := result[t]
|
||||
agg.Type = t
|
||||
agg.Count++
|
||||
if m.Flagged {
|
||||
agg.FlaggedCount++
|
||||
}
|
||||
sums[t] += m.GroundednessScore
|
||||
result[t] = agg
|
||||
}
|
||||
for t, agg := range result {
|
||||
agg.AverageGroundedness = sums[t] / float64(agg.Count)
|
||||
result[t] = agg
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package summaryquality
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// AK1/Prüfung 1: Kennzahl ist je Zusammenfassungstyp abrufbar.
|
||||
func TestAggregateByType_PerTypeMetric(t *testing.T) {
|
||||
source := "Der Kunde bestellt zehn Aktenordner für das Archiv am Standort Nord."
|
||||
metrics := []Metric{
|
||||
Evaluate(Sample{Type: TypeDocument, Source: source, Summary: "Der Kunde bestellt Aktenordner für das Archiv."}),
|
||||
Evaluate(Sample{Type: TypeDocument, Source: source, Summary: "Der Kunde bestellt Aktenordner für das Archiv."}),
|
||||
Evaluate(Sample{Type: TypeThread, Source: source, Summary: "Rakete Mondbasis Ufo Zauberer Drache Einhorn."}),
|
||||
}
|
||||
|
||||
byType := AggregateByType(metrics)
|
||||
|
||||
doc, ok := byType[TypeDocument]
|
||||
if !ok {
|
||||
t.Fatal("keine Kennzahl für Typ document")
|
||||
}
|
||||
if doc.Count != 2 {
|
||||
t.Errorf("document Count = %d, want 2", doc.Count)
|
||||
}
|
||||
if doc.AverageGroundedness <= 0.5 {
|
||||
t.Errorf("document AverageGroundedness = %v, erwarte hohen Wert", doc.AverageGroundedness)
|
||||
}
|
||||
|
||||
thread, ok := byType[TypeThread]
|
||||
if !ok {
|
||||
t.Fatal("keine Kennzahl für Typ thread")
|
||||
}
|
||||
if thread.Count != 1 {
|
||||
t.Errorf("thread Count = %d, want 1", thread.Count)
|
||||
}
|
||||
}
|
||||
|
||||
// AK2/Prüfung 2: Auffällig schlechte Zusammenfassung wird markiert.
|
||||
func TestEvaluate_FlagsFabricatedSummary(t *testing.T) {
|
||||
source := "Der Kunde bestellt zehn Aktenordner für das Archiv am Standort Nord."
|
||||
fabricated := Evaluate(Sample{
|
||||
Type: TypeDocument,
|
||||
Source: source,
|
||||
Summary: "Rakete Mondbasis Ufo Zauberer Drache Einhorn.",
|
||||
})
|
||||
|
||||
if !fabricated.Flagged {
|
||||
t.Errorf("erfundene Zusammenfassung wurde nicht markiert, score=%v", fabricated.GroundednessScore)
|
||||
}
|
||||
|
||||
faithful := Evaluate(Sample{
|
||||
Type: TypeDocument,
|
||||
Source: source,
|
||||
Summary: "Der Kunde bestellt Aktenordner für das Archiv.",
|
||||
})
|
||||
if faithful.Flagged {
|
||||
t.Errorf("quellentreue Zusammenfassung wurde fälschlich markiert, score=%v", faithful.GroundednessScore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluate_FlagsEmptySummary(t *testing.T) {
|
||||
got := Evaluate(Sample{Type: TypeDocument, Source: "Original.", Summary: ""})
|
||||
if !got.Flagged {
|
||||
t.Error("leere Zusammenfassung wurde nicht markiert")
|
||||
}
|
||||
if got.Reason == "" {
|
||||
t.Error("Reason fehlt für leere Zusammenfassung")
|
||||
}
|
||||
}
|
||||
|
||||
// AK3/Prüfung 3: Bewertung ist nachvollziehbar dokumentiert.
|
||||
func TestEvaluate_ReasonExplainsScore(t *testing.T) {
|
||||
source := "Der Kunde bestellt zehn Aktenordner für das Archiv am Standort Nord."
|
||||
got := Evaluate(Sample{
|
||||
Type: TypeDocument,
|
||||
Source: source,
|
||||
Summary: "Rakete Mondbasis Ufo.",
|
||||
})
|
||||
|
||||
if got.Reason == "" {
|
||||
t.Fatal("Reason ist leer, Bewertung nicht nachvollziehbar")
|
||||
}
|
||||
if !strings.Contains(got.Reason, "nicht belegt") {
|
||||
t.Errorf("Reason nennt keine unbelegten Wörter: %q", got.Reason)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user