Skip to content

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

  1. 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.
  2. Schritte:
  3. Transkription (Audio → Whisper) und Medien-Ablage (ADR-0062).
  4. Projektauflösung — Code, nicht LLM (ADR-0060 Punkt 6).
  5. Intent ∈ done | problem | need_material | status_update | question | unrelated, mit confidence.
  6. 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, mit confidence.
  7. Aktionsvorschlag ∈ transition | comment | attach | notify_manager | ask_clarification | none — als Entwurf mit Kommentartext, Zielstatus, Dateizuordnung.
  8. Policy-Gate — Code: entscheidet je Aktion zwischen auto, field_confirm, review (Backoffice) und clarify (Tabelle unten).
  9. Freigabe (Human in the loop) — Backoffice bearbeitet die Queue; erst danach Ausführung über typisierte Tools und Antwort an den Kontakt.
  10. 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).
  11. 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: FakeLLMProvider mit 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) oder OLLAMA_BASE_URL auf bestehende Instanz (Dev); celery-assistant-Worker.
  • Settings: OLLAMA_BASE_URL, ASSISTANT_MODEL_FAST, ASSISTANT_MODEL_REPORT, WHISPER_URL.
  • Neue Permission operations.review im 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.