mindnet/docs/AUDIT_WP25B_CODE_REVIEW.md

15 KiB
Raw Permalink Blame History

Code-Prüfung WP25b: Lazy-Prompt-Orchestration

Datum: 2026-01-02
Version: WP25b (Lazy Prompt Integration)
Prüfer: Auto (AI Code Review)


📋 Übersicht

Diese Prüfung analysiert die WP25b-Änderungen auf:

  • Schwachstellen (Security, Error Handling, Robustness)
  • Inkonsistenzen (Naming, Patterns, Architecture)
  • Verbesserungspotenzial (Performance, Maintainability, Code Quality)

🔴 KRITISCHE SCHWACHSTELLEN

1. Fehlende Validierung von prompt_key in llm_service.py

Datei: app/services/llm_service.py:169-176

Problem:

if prompt_key:
    template = self.get_prompt(prompt_key, model_id=target_model, provider=target_provider)
    try:
        current_prompt = template.format(**(variables or {}))
    except Exception as e:
        logger.error(f"❌ Prompt formatting failed for key '{prompt_key}': {e}")
        current_prompt = template # Sicherheits-Fallback

Risiko:

  • Wenn prompt_key nicht existiert, gibt get_prompt() einen leeren String zurück
  • Leerer Prompt wird an LLM gesendet → unerwartetes Verhalten
  • Keine Warnung/Fehlerbehandlung für fehlende Keys

Empfehlung:

if prompt_key:
    template = self.get_prompt(prompt_key, model_id=target_model, provider=target_provider)
    if not template or not template.strip():
        logger.error(f"❌ Prompt key '{prompt_key}' not found or empty. Available keys: {list(self.prompts.keys())}")
        raise ValueError(f"Invalid prompt_key: '{prompt_key}'")
    # ... rest of code

Schweregrad: 🔴 Hoch (kann zu stillem Fehlverhalten führen)


2. Inkonsistenter Fallback in decision_engine.py

Datei: app/core/retrieval/decision_engine.py:258-261

Problem:

return await self.llm_service.generate_raw_response(
    prompt=f"Beantworte: {query}\n\nKontext:\n{fallback_context}",
    system=system_prompt, priority="realtime", profile_name=profile
)

Risiko:

  • Fallback verwendet prompt= statt prompt_key= → inkonsistent mit WP25b-Architektur
  • Keine Lazy-Loading-Vorteile (modell-spezifische Prompts werden ignoriert)
  • Hardcodierter Prompt-String statt konfigurierbarer Template

Empfehlung:

return await self.llm_service.generate_raw_response(
    prompt_key="fallback_synthesis",  # In prompts.yaml definieren
    variables={"query": query, "context": fallback_context},
    system=system_prompt, priority="realtime", profile_name=profile
)

Schweregrad: 🟡 Mittel (funktional, aber architektonisch inkonsistent)


3. Zu permissives Error-Handling in ingestion_validation.py

Datei: app/core/ingestion/ingestion_validation.py:77-80

Problem:

except Exception as e:
    logger.warning(f"⚠️ Validation error for {target_id} using {profile_name}: {e}")
    # Im Zweifel (Timeout/Fehler) erlauben wir die Kante, um Datenverlust zu vermeiden
    return True

Risiko:

  • Alle Fehler führen zu return True → ungültige Kanten werden akzeptiert
  • Keine Unterscheidung zwischen transienten Fehlern (Timeout) und permanenten Fehlern (Invalid Config)
  • Kann zu Graph-Verschmutzung führen

Empfehlung:

except Exception as e:
    error_type = type(e).__name__
    # Transiente Fehler (Timeout, Network) → erlauben
    if any(x in str(e).lower() for x in ["timeout", "connection", "network"]):
        logger.warning(f"⚠️ Transient error for {target_id}: {e}. Allowing edge.")
        return True
    # Permanente Fehler (Config, Validation) → ablehnen
    logger.error(f"❌ Permanent validation error for {target_id}: {e}")
    return False

Schweregrad: 🟡 Mittel (kann Graph-Qualität beeinträchtigen)


4. Fehlende Validierung von YAML-Konfigurationen

Datei: app/core/retrieval/decision_engine.py:38-51

Problem:

def _load_engine_config(self) -> Dict[str, Any]:
    path = os.getenv("MINDNET_DECISION_CONFIG", "config/decision_engine.yaml")
    if not os.path.exists(path):
        logger.error(f"❌ Decision Engine Config not found at {path}")
        return {"strategies": {}}
    try:
        with open(path, "r", encoding="utf-8") as f:
            config = yaml.safe_load(f) or {}
            logger.info(f"⚙️ Decision Engine Config loaded (v{config.get('version', 'unknown')})")
            return config
    except Exception as e:
        logger.error(f"❌ Failed to load decision_engine.yaml: {e}")
        return {"strategies": {}}

Risiko:

  • Keine Schema-Validierung → fehlerhafte YAML wird stillschweigend akzeptiert
  • Fehlende Pflichtfelder (z.B. strategies, streams_library) führen zu Runtime-Fehlern
  • Keine Warnung bei unbekannten Keys

Empfehlung:

def _load_engine_config(self) -> Dict[str, Any]:
    # ... existing code ...
    try:
        with open(path, "r", encoding="utf-8") as f:
            config = yaml.safe_load(f) or {}
            
            # Schema-Validierung
            required_keys = ["strategies", "streams_library"]
            missing = [k for k in required_keys if k not in config]
            if missing:
                logger.error(f"❌ Missing required keys in config: {missing}")
                return {"strategies": {}, "streams_library": {}}
            
            # Warnung bei unbekannten Top-Level-Keys
            known_keys = {"version", "settings", "strategies", "streams_library"}
            unknown = set(config.keys()) - known_keys
            if unknown:
                logger.warning(f"⚠️ Unknown keys in config: {unknown}")
            
            return config
    except yaml.YAMLError as e:
        logger.error(f"❌ YAML syntax error in {path}: {e}")
        return {"strategies": {}, "streams_library": {}}

Schweregrad: 🟡 Mittel (kann zu Runtime-Fehlern führen)


🟡 INKONSISTENZEN

5. Mix aus Deutsch und Englisch in Logs

Problem:

  • Logs enthalten sowohl deutsche als auch englische Nachrichten
  • Inkonsistente Emoji-Nutzung (manchmal, manchmal nicht)

Beispiele:

  • logger.info(f"🎯 [ROUTING] Parsed Intent: '{intent}'...") (Englisch)
  • logger.warning(f"⚠️ Profil '{profile_name}' nicht in llm_profiles.yaml gefunden!") (Deutsch)

Empfehlung:

  • Einheitliche Sprache wählen (empfohlen: Deutsch, da User-Regel "Always respond in German")
  • Oder: Englisch für technische Logs, Deutsch für User-sichtbare Nachrichten

Schweregrad: 🟢 Niedrig (Wartbarkeit)


6. Unterschiedliche Config-Loading-Patterns

Problem:

  • decision_engine.py: Lädt Config bei jedem Aufruf (kein Cache)
  • chat.py: Nutzt globalen Cache (_DECISION_CONFIG_CACHE)
  • llm_service.py: Lädt Prompts/Profiles einmalig im __init__

Empfehlung:

  • Einheitliches Pattern: Lazy Loading mit Cache
  • Cache-Invalidierung bei Datei-Änderungen (optional, aber wünschenswert)

Schweregrad: 🟢 Niedrig (Performance-Optimierung)


7. Private Methoden werden extern genutzt

Datei: app/routers/chat.py:138

Problem:

intent = await llm.decision_engine._determine_strategy(query)

Risiko:

  • Nutzung von _determine_strategy() (private Methode) von außen
  • Verletzt Encapsulation-Prinzip
  • Kann bei Refactoring brechen

Empfehlung:

# In decision_engine.py:
async def determine_strategy(self, query: str) -> str:  # Public
    return await self._determine_strategy(query)

# In chat.py:
intent = await llm.decision_engine.determine_strategy(query)

Schweregrad: 🟡 Mittel (Wartbarkeit)


8. Inkonsistente Prompt-Formatierung

Problem:

  • decision_engine.py:248-250: Manuelle prepend_instruction-Anhängung
  • Sollte eigentlich über variables im Template gehandhabt werden

Empfehlung:

  • prepend_instruction als Variable in prompts.yaml Templates integrieren
  • Entfernung der manuellen Anhängung

Schweregrad: 🟢 Niedrig (Code-Qualität)


🟢 VERBESSERUNGSPOTENZIAL

9. Fehlende Type Hints

Problem:

  • Viele Funktionen haben unvollständige oder fehlende Type Hints
  • Any wird zu häufig verwendet

Beispiele:

  • app/routers/chat.py:116: async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]:
  • app/services/llm_service.py:125: Viele Optional[...] aber auch Any

Empfehlung:

  • Vollständige Type Hints für alle öffentlichen Methoden
  • Verwendung von TypedDict für Config-Strukturen
  • from __future__ import annotations für Forward References

Schweregrad: 🟢 Niedrig (Code-Qualität, IDE-Support)


10. Code-Duplikation bei Config-Loading

Problem:

  • Ähnliche YAML-Loading-Logik in mehreren Dateien:
    • llm_service.py:_load_prompts()
    • llm_service.py:_load_llm_profiles()
    • decision_engine.py:_load_engine_config()
    • chat.py:_load_decision_config()
    • embeddings_client.py:_load_embedding_profile()

Empfehlung:

# app/core/config_loader.py
def load_yaml_config(path: Path, required_keys: List[str] = None) -> Dict[str, Any]:
    """Zentrale YAML-Loading-Logik mit Validierung."""
    # ... unified implementation ...

Schweregrad: 🟢 Niedrig (DRY-Prinzip)


11. Fehlende Retry-Logik für Embedding-Requests

Datei: app/services/embeddings_client.py:83-96

Problem:

  • Embedding-Requests haben keine Retry-Logik
  • Bei transienten Fehlern (Network, Timeout) wird sofort [] zurückgegeben

Empfehlung:

async def _request_embedding_with_client(self, client: httpx.AsyncClient, text: str, max_retries: int = 2) -> List[float]:
    for attempt in range(max_retries + 1):
        try:
            # ... existing code ...
        except (httpx.TimeoutException, httpx.NetworkError) as e:
            if attempt < max_retries:
                await asyncio.sleep(2 ** attempt)
                continue
            raise

Schweregrad: 🟢 Niedrig (Resilienz)


12. Potenzielle Race Condition bei Background-Semaphore

Datei: app/services/llm_service.py:39-42

Problem:

  • _background_semaphore wird als Klassen-Variable initialisiert
  • Bei mehreren LLMService-Instanzen könnte es zu Race Conditions kommen

Empfehlung:

  • Thread-safe Initialisierung mit asyncio.Lock
  • Oder: Singleton-Pattern für LLMService

Schweregrad: 🟢 Niedrig (Edge Case)


13. Fehlende Validierung von variables in Prompt-Formatierung

Datei: app/services/llm_service.py:173

Problem:

current_prompt = template.format(**(variables or {}))

Risiko:

  • Wenn Template Variablen erwartet, die nicht in variables sind → KeyError
  • Keine Warnung über fehlende Variablen

Empfehlung:

if variables:
    # Validierung: Prüfe ob alle benötigten Variablen vorhanden sind
    import string
    required_vars = set(string.Formatter().parse(template))
    required_vars = {v[1] for v in required_vars if v[1] is not None}
    missing = required_vars - set(variables.keys())
    if missing:
        logger.warning(f"⚠️ Missing variables in prompt '{prompt_key}': {missing}")
    current_prompt = template.format(**(variables or {}))
else:
    current_prompt = template

Schweregrad: 🟢 Niedrig (Debugging-Hilfe)


14. Ineffiziente String-Operationen

Datei: app/core/retrieval/decision_engine.py:248-250

Problem:

if prepend and prepend not in response[:len(prepend)+50]:

Risiko:

  • String-Slicing bei jeder Antwort (auch wenn prepend leer ist)
  • Ineffizient für lange Antworten

Empfehlung:

if prepend and prepend.strip():
    # Prüfe nur ersten Teil der Antwort
    check_length = min(len(response), len(prepend) + 100)
    if prepend not in response[:check_length]:
        logger.info(" Adding prepend_instruction manually (not found in response).")
        response = f"{prepend}\n\n{response}"

Schweregrad: 🟢 Niedrig (Performance-Mikrooptimierung)


📊 ZUSAMMENFASSUNG

Kritische Schwachstellen: 4

  • 🔴 Hoch: Fehlende prompt_key-Validierung
  • 🟡 Mittel: Inkonsistenter Fallback, Permissives Error-Handling, Fehlende YAML-Validierung

Inkonsistenzen: 4

  • 🟡 Mittel: Private Methoden-Nutzung
  • 🟢 Niedrig: Sprach-Mix, Config-Patterns, Prompt-Formatierung

Verbesserungspotenzial: 6

  • 🟢 Niedrig: Type Hints, Code-Duplikation, Retry-Logik, Race Conditions, Variable-Validierung, String-Optimierung

PRIORISIERTE EMPFEHLUNGEN

Sofort (vor Merge):

  1. Prompt-Key-Validierung hinzufügen (llm_service.py)
  2. Fallback konsistent machen (decision_engine.py)
  3. YAML-Schema-Validierung implementieren

Kurzfristig (nächste Iteration):

  1. Error-Handling in ingestion_validation.py differenzieren
  2. Public API für _determine_strategy() erstellen
  3. Zentrale Config-Loader implementieren

Langfristig (Refactoring):

  1. Type Hints vervollständigen
  2. Code-Duplikation reduzieren
  3. Retry-Logik für Embeddings

🎯 POSITIVE ASPEKTE

Gute Architektur:

  • Saubere Trennung von Concerns (Lazy Loading, MoE, Fallback-Kaskade)
  • Modulare Struktur mit klaren Verantwortlichkeiten

Robustheit:

  • Umfassende Fallback-Mechanismen
  • Background-Semaphore für Rate-Limiting

Dokumentation:

  • Ausführliche Code-Kommentare
  • Versions-Tracking in Datei-Headern

Konsistenz:

  • Einheitliche Verwendung von prompt_key + variables (WP25b)
  • Klare Profil-Steuerung über llm_profiles.yaml

Status: 🟡 Bedingt genehmigt - Kritische Fixes sollten vor Merge implementiert werden.


🔧 IMPLEMENTIERTE FIXES

Fix 1: Prompt-Key-Validierung (llm_service.py)

  • Hinzugefügt: Validierung, ob prompt_key existiert und nicht leer ist
  • Hinzugefügt: Bessere Fehlermeldung mit verfügbaren Keys
  • Hinzugefügt: Spezifische Behandlung von KeyError bei fehlenden Variablen

Fix 2: Konsistenter Fallback (decision_engine.py)

  • Geändert: Fallback nutzt nun prompt_key="fallback_synthesis" statt hardcodiertem Prompt
  • Hinzugefügt: Fallback-Template in prompts.yaml (Zeile 429-447)
  • Hinzugefügt: Graceful Degradation, falls Template nicht existiert

Fix 3: YAML-Schema-Validierung (decision_engine.py)

  • Hinzugefügt: Validierung der Pflichtfelder (strategies, streams_library)
  • Hinzugefügt: Warnung bei unbekannten Top-Level-Keys
  • Hinzugefügt: Spezifische Behandlung von yaml.YAMLError

Fix 4: Differenziertes Error-Handling (ingestion_validation.py)

  • Geändert: Unterscheidung zwischen transienten (Timeout, Network) und permanenten Fehlern
  • Verbessert: Transiente Fehler → return True (Datenverlust vermeiden)
  • Verbessert: Permanente Fehler → return False (Graph-Qualität schützen)

📝 TODO (Nicht kritisch, aber empfohlen):

  • Public API für _determine_strategy() erstellen (decision_engine.py)
  • Zentrale Config-Loader-Funktion implementieren
  • Type Hints vervollständigen
  • Retry-Logik für Embedding-Requests hinzufügen

Status nach Fixes: 🟢 Genehmigt - Kritische Probleme behoben, Code ist produktionsreif.