LLM: Sektions-Pfad ab 5 Sprechern gegen Recency-Bias in Meetings
Whole-Pass versagte auf #74 (13-Sprecher-Weekly, 31k Zeichen): Summary enthielt fast nur die letzten Sprecher. analyze() routet labeled Transkripte mit >=5 Sprechern jetzt in analyzeLong; Sektions-Pfad liefert Themen-Überschriften, jede Person, Smalltalk-Dämpfung, Todos mit Verantwortlichem, Überblick-Mikro-Call, Cap 16000. Kap. 05 + Changelog aktualisiert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b33b555fa7
commit
432c4b14dd
+32
-6
@@ -19,25 +19,51 @@
|
||||
flowchart TB
|
||||
T["Transkript"] --> A{"≤ 4 800 Zeichen?"}
|
||||
A -- ja --> SINGLE["analyzeSingle:<br/>EIN Call = bereinigter Dialog + JSON-Analyse<br/>(Fallback: separater JSON-Call bei Echo/Parse-Fehler)"]
|
||||
A -- nein --> B{"≤ 32 000 Zeichen?"}
|
||||
A -- nein --> SPK{"labeled + ≥ 5 Sprecher?<br/>(MANY_SPEAKERS, countSpeakers)"}
|
||||
SPK -- "ja (Round-Robin-Meeting)" --> LONG
|
||||
SPK -- nein --> B{"≤ 32 000 Zeichen?"}
|
||||
B -- ja --> WHOLE["analyzeWhole:<br/>GANZES Gespräch in einem Analyse-Call<br/>(kohärente Summary, kleine JSON-Ausgabe)<br/>Instruktion VOR und NACH dem Transkript"]
|
||||
WHOLE --> WCHECK{"JSON parsebar +<br/>Zusammenfassung ≥ 20 Zeichen?"}
|
||||
WCHECK -- "JSON kaputt" --> REPAIR["Repair-Call: NUR der kaputte<br/>Output (klein) → gültiges JSON"]
|
||||
REPAIR --> WCHECK
|
||||
WCHECK -- "weiterhin leer/Junk" --> LONG
|
||||
WCHECK -- ja --> OUT
|
||||
B -- nein --> LONG["analyzeLong (Sektions-Konglomerat):<br/>Chunks à 4 200 Zeichen<br/>(bei 8k wird der Inhalt generisch/namenlos,<br/>bei 4k bleibt Gemma geerdet)"]
|
||||
LONG --> MAP["MAP pro Chunk, sequenziell mit ROLLING CONTEXT<br/>(1-Satz-Kurzfassungen der vorherigen Abschnitte →<br/>Änderungen erkennbar: 'jetzt X, vorher Y'):<br/>Mini-JSON zusammenfassung + eigennamen[]<br/>(reine Namens-EXTRAKTION) + teilnehmer + todos<br/>bewusst minimaler Prompt"]
|
||||
B -- nein --> LONG["analyzeLong (Sektions-Konglomerat):<br/>Chunks à 4 200 Zeichen<br/>(bei 8k wird der Inhalt generisch/namenlos,<br/>bei 4k bleibt das Modell geerdet);<br/>chunkText splittet an Zeilen = Turn-Grenzen"]
|
||||
LONG --> MAP["MAP pro Chunk, sequenziell mit ROLLING CONTEXT<br/>(1-Satz-Kurzfassungen der vorherigen Abschnitte →<br/>Änderungen erkennbar: 'jetzt X, vorher Y'):<br/>Mini-JSON thema (Überschrift ≤ 6 Wörter) +<br/>zusammenfassung (JEDE berichtende Person, beruflich;<br/>Smalltalk max. Halbsatz) + eigennamen[] (EXTRAKTION)<br/>+ teilnehmer + todos (nur beruflich, Verantwortlicher<br/>in Klammern) — bewusst minimaler Prompt"]
|
||||
MAP --> SALV{"JSON parsebar +<br/>kein Feldbeschreibungs-Echo?"}
|
||||
SALV -- nein --> RETRY["bis 3 Versuche, Prompt VARIIERT<br/>(2.: ohne Kontext, 3.: ohne Hint —<br/>manche Inhalte echoen sonst JEDES Mal)<br/>dann Feld-Salvage per Regex<br/>(zusammenfassung-String überlebt fast immer),<br/>zuletzt Prosa-Rettung (≥ 60 Zeichen, sonst verwerfen)"]
|
||||
SALV -- nein --> RETRY["bis 3 Versuche, Prompt VARIIERT<br/>(2.: ohne Kontext, 3.: ohne Hint —<br/>manche Inhalte echoen sonst JEDES Mal)<br/>dann Feld-Salvage per Regex<br/>(zusammenfassung/thema-Strings überleben fast immer),<br/>zuletzt Prosa-Rettung (≥ 60 Zeichen, sonst verwerfen)"]
|
||||
RETRY --> COLLECT
|
||||
SALV -- ja --> COLLECT["Sektionen einsammeln"]
|
||||
COLLECT --> TITLE["TITEL-Mikro-Call: 2-Satz-Kurzfassungen<br/>OHNE letzten Abschnitt (Schluss-Smalltalk-Filter)<br/>→ nur {titel} — kleines Input/Output = zuverlässig"]
|
||||
TITLE --> ASSEMBLE["KONGLOMERAT baut CODE (kein LLM-Reduce mehr):<br/>'Gesprächsverlauf in n Teilen — genannte Namen: …'<br/>+ 'Teil i/n: Summary (Namen: …)' chronologisch<br/>→ deterministisch, keine Fehlgewichtung möglich"]
|
||||
COLLECT --> TITLE["TITEL-Mikro-Call: Thema + 2-Satz-Kurzfassungen<br/>OHNE letzten Abschnitt (Schluss-Smalltalk-Filter)<br/>→ nur {titel} — kleines Input/Output = zuverlässig"]
|
||||
TITLE --> OVERVIEW["ÜBERBLICK-Mikro-Call über dieselben Kurzfassungen<br/>→ {ueberblick} 2-3 Sätze, alle Abschnitte gleich gewichtet<br/>(Key-tolerant parsen: auch 'überblick' oder<br/>irgendein langer String-Value; leer = weglassen)"]
|
||||
OVERVIEW --> ASSEMBLE["KONGLOMERAT baut CODE (kein LLM-Reduce):<br/>Überblick-Absatz + pro Sektion 'Thema:\nSummary (Namen: …)'<br/>chronologisch + 'Genannte Namen: …' am Ende<br/>→ deterministisch, keine Fehlgewichtung möglich; Cap 16 000"]
|
||||
ASSEMBLE --> OUT["Ergebnis"]
|
||||
SINGLE --> OUT
|
||||
```
|
||||
|
||||
### Round-Robin-Meetings: Sprecherzahl schlägt Länge (2026-07-17)
|
||||
|
||||
Aufnahme #74 (33-min-Weekly, 13 diarisierte Sprecher, 31k Zeichen) lag knapp UNTER `WHOLE_MAX_CHARS`
|
||||
und lief als Whole-Pass — Ergebnis: Summary fast nur aus den LETZTEN Sprechern (Gasblender, Sitas,
|
||||
Zeiterfassung), alle frühen Wochenberichte fehlten. Ein 4B kann 15+ gleichgewichtige Themen aus einem
|
||||
~10k-Token-Prompt nicht ausbalancieren — der Whole-Pass funktioniert nur für narrative Gespräche mit
|
||||
wenigen Sprechern (dort validiert, #70/#71). Deshalb routet `analyze()` **labeled Transkripte mit
|
||||
≥ 5 Sprechern (`MANY_SPEAKERS`) immer in den Sektions-Pfad**, der jede Gesprächsphase per Konstruktion
|
||||
abdeckt. Der Sektions-Pfad wurde dabei auf Copilot-Niveau ausgebaut (3 Iterationen auf #74):
|
||||
|
||||
1. **Themen-Überschrift pro Chunk** (`thema`-Feld) → Konglomerat liest sich wie Besprechungsnotizen
|
||||
(„Performanceproblem bei SAP-System:" statt „Teil 2/10:").
|
||||
2. **Jede berichtende Person MUSS vorkommen** (Prompt-Regel) — vorher fielen einzelne Sprecher weg.
|
||||
3. **Smalltalk-Dämpfung**: Privates (Essen, Familie, Auto, Urlaubserzählungen) max. ein Halbsatz;
|
||||
Todos nur beruflich, mit Verantwortlichem in Klammern (37 → 30 saubere Todos auf #74).
|
||||
4. **Überblick-Mikro-Call** (`PROMPT_OVERVIEW`) liefert den Intro-Absatz; gleiche kleine
|
||||
Input/Output-Klasse wie der Titel-Call. GOTCHA: Qwen antwortet teils mit Key `"überblick"`
|
||||
(ü!) statt `"ueberblick"` — Parser akzeptiert jeden langen String-Value.
|
||||
5. Summary-Cap 6 000 → **16 000** Zeichen: 10 Sektionen ≈ 8k, der alte Cap schnitt mitten im Satz ab.
|
||||
|
||||
Validiert auf #74: alle 13 Sprecher + alle Themen (DWP-Bank, Diakonie, SAP-GUI, Sitas, Zeiterfassung,
|
||||
Wellpass, LinkedIn, Polavis, Gasblender) abgedeckt, kein Recency-Bias mehr. Backup `llm.js.bak-standup`.
|
||||
|
||||
### Warum kein LLM-Reduce mehr (Prompt-Lab 2026-07-16)
|
||||
|
||||
Der alte Reduce (Teil-Summaries → LLM baut EINE Gesamt-Summary) war die Fehlgewichtungs-Quelle. Im
|
||||
|
||||
@@ -3,6 +3,23 @@
|
||||
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
|
||||
Was, Warum, betroffene Dateien/Backups).
|
||||
|
||||
## 2026-07-17 — Round-Robin-Meetings: Sektions-Pfad statt Whole-Pass ab 5 Sprechern (Recency-Bias-Fix)
|
||||
|
||||
- **Problem:** #74 (33-min-Weekly, 13 Sprecher, 31k Zeichen) lief knapp unter `WHOLE_MAX_CHARS` als
|
||||
Whole-Pass → Summary enthielt fast nur die letzten Sprecher. 4B kann 15+ gleichwertige Themen in
|
||||
einem ~10k-Token-Prompt nicht gewichten; Whole-Pass taugt nur für narrative Gespräche mit wenigen
|
||||
Sprechern.
|
||||
- **Fix (llm.js, `.bak-standup`):** `analyze()` routet labeled Transkripte mit ≥ 5 Sprechern
|
||||
(`MANY_SPEAKERS`, `countSpeakers()`) immer in `analyzeLong` (Sektions-Konglomerat). Sektions-Pfad
|
||||
auf Besprechungsnotizen-Niveau ausgebaut: `thema`-Überschrift pro Chunk, Pflicht zur Erwähnung
|
||||
JEDER berichtenden Person, Smalltalk-Dämpfung (Privates max. Halbsatz), Todos nur beruflich mit
|
||||
Verantwortlichem in Klammern, Überblick-Mikro-Call (`PROMPT_OVERVIEW`, Key-tolerant: Qwen schreibt
|
||||
teils `"überblick"`), Summary-Cap 6 000 → 16 000 (alter Cap schnitt 10 Sektionen mitten im Satz ab).
|
||||
- **Validierung:** 3 Iterationen per `reanalyze-llm` auf #74 — final alle 13 Sprecher + alle Themen
|
||||
abgedeckt (Intro-Absatz, 10 Themen-Sektionen, 30 saubere Todos mit Verantwortlichen), kein
|
||||
Recency-Bias. Temporärer `/api/debug/ask` fürs Overview-Prompt-Tuning wieder entfernt
|
||||
(server.js unverändert).
|
||||
|
||||
## 2026-07-17 — GUI: Aufnahme-Tab gestrichen, Record-Button in der Kopfzeile + Stille-MP3-Fix
|
||||
|
||||
- Der Reiter "Aufnahme" ist weg: Quelle (CallRecMix), HFP-Codec (auto) und Sprecherzahl (auto) waren
|
||||
|
||||
Reference in New Issue
Block a user