ADR-0061: KI-Mittelsmann als Pipeline mit Backoffice-Freigabe, nicht als freier Agent
Kontext
Rückmeldungen aus dem Feld sind kurz, umgangssprachlich, oft Sprachnachrichten: „Schacht 7 fertig, bei 9 liegt noch ein Kabel quer, brauch ne Verlängerung“. Jemand muss das auf Issues abbilden, abhaken, Probleme eskalieren und dem Chef abends eine Einschätzung geben. Das soll ein LLM vorbereiten — lokal über das vorhandene Ollama — ohne zu halluzinieren und ohne ungeprüft in Jira zu schreiben.
Die Entscheidung über Aufgabenerledigung und Dokumente liegt beim Backoffice: Die KI bereitet vor, Backoffice-Mitarbeiter genehmigen, passen an, weisen ab oder schicken mit Kommentar an den Feldmitarbeiter zurück. Der Feldkontakt (ADR-0060) hat keine Schreibrechte und soll auch nicht der letzte Prüfer sein.
Entscheidung
- Pipeline statt Agent. Jede eingehende Nachricht durchläuft feste Schritte mit Structured Output (JSON-Schema). Kein offener Chat-Loop, kein Gedächtnis über die Conversation hinaus.
- Schritte:
- Transkription (Audio → Whisper) und Medien-Ablage (ADR-0062).
- Projektauflösung — Code, nicht LLM (ADR-0060 Punkt 6).
- Intent ∈
done | problem | need_material | status_update | question | unrelated, mitconfidence. - Issue-Matching — das Modell sieht nur die offenen Issues dieser Person in diesem Projekt (Key, Summary, Status, Fälligkeit) und wählt
issue_key | null, mitconfidence. - Aktionsvorschlag ∈
transition | comment | attach | notify_manager | ask_clarification | none— als Entwurf mit Kommentartext, Zielstatus, Dateizuordnung. - Policy-Gate — Code: entscheidet je Aktion zwischen
auto,field_confirm,review(Backoffice) undclarify(Tabelle unten). - Freigabe (Human in the loop) — Backoffice bearbeitet die Queue; erst danach Ausführung über typisierte Tools und Antwort an den Kontakt.
- Tools (Pydantic-Schemas, vom Code ausgeführt, Scope auf Projekt + Person geprüft):
list_my_open_issues,get_issue,add_comment,transition_issue,attach_file,notify_manager,ask_clarification. Später:search_project_documents(RAG). - Policy-Gate (Default):
| Aktion | Default-Gate | Bemerkung |
|---|---|---|
transition_issue (Erledigung) |
review |
Backoffice genehmigt; kein automatischer Statuswechsel |
attach_file (Fotos, Dokumente) |
review |
Backoffice prüft Zuordnung und Inhalt, bevor Tracker/Nextcloud geschrieben wird |
add_comment |
auto bei Match-Confidence ≥ 0,7, sonst review |
Kommentar ist reversibel und sichtbar; trotzdem je Projekt auf review umstellbar |
notify_manager |
auto bei problem / need_material |
Eskalation darf nicht in der Queue warten |
ask_clarification |
auto bei Intent- oder Match-Confidence < 0,5 |
Rückfrage an den Kontakt mit Issue-Liste |
| alles | blocked ohne Opt-in oder bei Widerruf |
keine Verarbeitung |
Gate-Modus je Aktion ist Konfiguration pro Projekt (projects.assistant_policy JSONB, Fallback Mandanten-Default in integration_connections.config). field_confirm (Button beim Kontakt) bleibt als Modus verfügbar, ist aber nicht Default.
5. Freigabe-Queue. Jeder Vorschlag mit Gate review ist eine pending_actions-Zeile im Zustand open. Der Kontakt bekommt sofort eine Eingangsbestätigung („Danke, ist in Prüfung“). Backoffice-Mitarbeiter (Permission operations.review) haben vier Aktionen:
| Aktion | Wirkung |
|---|---|
| Genehmigen | Entwurf wird unverändert ausgeführt (Tool-Calls), Kontakt bekommt Bestätigung mit Issue-Key |
| Anpassen + Genehmigen | Reviewer ändert Issue, Zielstatus, Kommentartext oder Dateizuordnung; payload_final ≠ payload_draft; dann wie Genehmigen |
| Abweisen | Nichts wird in den Tracker geschrieben; Grund Pflicht; Kontakt erhält keine oder eine neutrale Nachricht (konfigurierbar) |
| Zurückschicken | Reviewer-Kommentar geht als Nachricht an den Kontakt („Bitte Foto vom Typenschild nachreichen“); Zeile bleibt sent_back, Antwort des Kontakts wird an dieselbe Zeile gehängt und erneut vorgeschlagen |
Keine Ablauffrist mit stillem Verwerfen: offene Zeilen werden nach konfigurierbarer Zeit (Default 4 h) als überfällig markiert und im Tagesbericht gezählt. Eine zweite Freigabe (Vier-Augen) ist nicht vorgesehen.
6. Tagesbericht (Chef): Aggregation aus tracker_issues, messages, agent_runs, pending_actions ist Code; das LLM schreibt nur summary + risks[] aus dieser Aggregation (Structured Output). Keine Zahlen aus dem Modell. Optional Gate review vor dem Versand (Default auto).
7. Provider-Abstraktion assistant/provider/ (LLMProvider: complete_structured, transcribe), portiert aus poliju. Modell pro Schritt konfigurierbar (kleines Modell für Intent/Match, größeres für den Bericht). Ollama zuerst; API-Provider möglich.
8. Vollständiges Audit: jeder Lauf in agent_runs mit Eingabe, Prompt-Version, Tool-Calls, Entscheidung, Confidence, Dauer; jede Freigabe mit Reviewer, Zeitpunkt, Änderung (payload_draft vs. payload_final) und Grund. Sichtbar unter /operations/agent-log. Abweichungen zwischen Entwurf und Freigabe sind die Messgröße für Prompt-Qualität.
9. Celery: Queue assistant, task_time_limit 300 s, eigener Worker-Container, damit Sync nicht blockiert.
Datenmodell (Tenant-DB)
CREATE TABLE agent_runs (
run_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
message_id uuid REFERENCES messages(message_id), -- NULL bei Tagesbericht
conversation_id uuid REFERENCES conversations(conversation_id),
project_id uuid REFERENCES projects(project_id),
kind text NOT NULL, -- 'inbound' | 'daily_report'
prompt_version text NOT NULL,
model text NOT NULL,
intent text,
intent_confidence numeric(3,2),
matched_issue_id uuid REFERENCES tracker_issues(issue_id),
match_confidence numeric(3,2),
proposed_action text,
gate_decision text NOT NULL, -- 'auto' | 'confirm' | 'clarify' | 'blocked'
tool_calls jsonb NOT NULL DEFAULT '[]',
steps jsonb NOT NULL DEFAULT '[]', -- Ein-/Ausgabe je Schritt
duration_ms integer,
error text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON agent_runs (project_id, created_at DESC);
ALTER TABLE projects ADD COLUMN assistant_policy jsonb NOT NULL DEFAULT '{}';
-- z. B. {"transition": "review", "attach": "review", "comment": "auto", "comment_min_confidence": 0.7}
CREATE TABLE pending_actions (
action_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
run_id uuid NOT NULL REFERENCES agent_runs(run_id),
conversation_id uuid NOT NULL REFERENCES conversations(conversation_id),
project_id uuid NOT NULL REFERENCES projects(project_id),
issue_id uuid REFERENCES tracker_issues(issue_id), -- NULL: KI fand kein Issue, Reviewer ordnet zu
action text NOT NULL, -- 'transition' | 'comment' | 'attach'
gate text NOT NULL, -- 'review' | 'field_confirm'
payload_draft jsonb NOT NULL, -- KI-Entwurf: to_status, comment_text, attachment_ids
payload_final jsonb, -- nach Anpassung durch Reviewer
state text NOT NULL DEFAULT 'open', -- 'open' | 'sent_back' | 'approved' | 'rejected' | 'executed' | 'failed'
is_overdue boolean NOT NULL DEFAULT false,
reviewer_user_id uuid, -- app_users.user_id (Control-DB, schwache Referenz)
review_comment text, -- Grund bei Abweisung / Text bei Zurückschicken
reviewed_at timestamptz,
sent_back_message_id uuid REFERENCES messages(message_id), -- ausgehende Rückfrage
executed_at timestamptz,
execution_error text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON pending_actions (project_id, state, created_at);
-- Antworten des Kontakts auf ein Zurückschicken hängen an derselben Zeile
ALTER TABLE messages ADD COLUMN pending_action_id uuid REFERENCES pending_actions(action_id);
CREATE TABLE daily_reports (
report_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(project_id),
for_date date NOT NULL,
aggregation jsonb NOT NULL, -- done, blocked, overdue, silent, pending
summary text NOT NULL,
risks jsonb NOT NULL DEFAULT '[]',
run_id uuid REFERENCES agent_runs(run_id),
sent_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (project_id, for_date)
);
Begründung
Die Fehlerkosten sind asymmetrisch: ein falsch abgehaktes Issue kostet mehr als eine Rückfrage. Eine Pipeline mit Kandidatenliste macht Halluzinationen strukturell unmöglich — das Modell kann keinen Issue-Key erfinden, der nicht in seiner Eingabe stand, und es führt nichts aus; das tut der Code nach dem Gate. 7–8B-Modelle klassifizieren und matchen aus kurzen Listen zuverlässig; für Freitext-Reasoning über das ganze Projekt wären sie es nicht.
Das Backoffice als Prüfinstanz ist die richtige Stelle: Es kennt Projekt und Kunde, sieht den Tracker ohnehin und trägt die Verantwortung für den Projektstand. Der Feldmitarbeiter kann seine eigene Meldung bestätigen, aber nicht beurteilen, ob „fertig“ auch im Sinne des Auftrags fertig ist. Die KI spart dem Backoffice das Tippen und Zuordnen, nicht das Entscheiden — und jede Korrektur im Review ist zugleich Trainingsmaterial für die Prompts. Das Audit ist nicht optional: Ohne nachvollziehbare Entscheidungen akzeptiert kein PM das System.
Verworfene Alternativen
| Alternative | Warum verworfen |
|---|---|
| Freier Tool-Calling-Agent mit Chat-Historie | Nicht reproduzierbar, Halluzinationsrisiko, Prompt-Injection durch Nachrichteninhalt |
| RAG über Jira-Issues statt Kandidatenliste | Unnötig: die Menge pro Person/Projekt ist klein; RAG später nur für Projektdokumente |
| Automatische Transitionen ab Confidence X | Falsch-Positive sind im Tracker sichtbar und teuer; Erledigung ist eine fachliche Entscheidung |
| Feldmitarbeiter bestätigt per Button als einzige Prüfung | Bestätigt nur die eigene Aussage; kein Blick auf Auftrag, Kunde, Dokumentqualität. Bleibt als Modus field_confirm für Projekte mit hohem Vertrauen |
| Stilles Verwerfen nach Ablauffrist | Meldung ginge verloren; stattdessen „überfällig“ sichtbar im Tagesbericht |
| Vier-Augen-Freigabe | Backoffice ist klein; zweite Freigabe verdoppelt die Queue ohne erkennbaren Nutzen |
| Review in Jira (KI schreibt Kommentar, PM wechselt Status) | Kunden sehen KI-Entwürfe in ihren Projekten; Dokumente lägen vor Prüfung am Issue |
| Nur Buttons, kein LLM | Freitext und Sprachnachrichten sind die Realität im Feld; Buttons bleiben als Fallback |
| Cloud-LLM als Default | Mitarbeiter-Nachrichten und Kundenprojekte verlassen das Haus; Ollama ist vorhanden |
| Confidence-Schwellen im Prompt | Nicht konfigurierbar, nicht testbar |
Folgen
- Prompts sind versioniert (
prompt_version) und liegen im Repo (assistant/prompts/); Änderungen sind Code-Review. - Testbarkeit:
FakeLLMProvidermit Fixture-Antworten; Pipeline-Tests ohne Ollama. Eine Golden-Set-Datei mit echten (anonymisierten) Nachrichten aus Phase 2 misst Intent-/Match-Qualität. - Compose:
ollama(Server) oderOLLAMA_BASE_URLauf bestehende Instanz (Dev);celery-assistant-Worker. - Settings:
OLLAMA_BASE_URL,ASSISTANT_MODEL_FAST,ASSISTANT_MODEL_REPORT,WHISPER_URL. - Neue Permission
operations.reviewim Katalog (ADR-0053); Default für Org-Admin und Mitarbeiter, nicht für Gast. - Routen
/operations/review(Freigabe-Queue: Entwurf links, Issue + Chatverlauf + Dateien rechts, vier Aktionen; Filter offen / überfällig / zurückgeschickt),/operations/agent-log,/operations/reports. Offene Freigaben als Zähler im Header-Bereich Betrieb. - Kontakt-Nachrichten für Eingang, Genehmigung, Abweisung und Zurückschicken sind Templates bzw. Freitext im 24-Stunden-Fenster (ADR-0060); Texte sind Konfiguration, nicht Prompt-Output.
- Tagesbericht zählt offene und überfällige Freigaben je Projekt.