Home Fp Basics Python Artikel 5
Teil 5 Funktionale Programmierung & Kategorientheorie

Warum FP und AI-Agents ein Dream-Team sind

Bild: KI-generiert


Warum FP und AI-Agents ein Dream-Team sind

Wie funktionale Prinzipien AI-generierten Code vorhersagbar, testbar und komponierbar machen – und ein Systemprompt, der alles zusammenfasst.


1. Das Problem: AI ist nondeterministisch, Code muss es nicht sein

AI-Agents – ob ChatGPT, Claude, Copilot oder ein eigener Agent – haben eine fundamentale Eigenschaft: Sie sind nondeterministisch.

"An LLM is nondeterministic — there is no sure or consistent output for a given input. Although the output is based on probabilities and isn't purely random, the user doesn't get to see what those probabilities are." — Jay Alammar & Maarten Grootendorst: Common Sense Guide to AI Engineering, S. 34

Derselbe Prompt kann verschiedene Code-Varianten erzeugen. Diese Variabilität ist bei Prosa wünschenswert – bei Code ist sie gefährlich.

"An LLM may simply not listen to instructions. This is a very different experience from writing 'regular' code." — Jay Alammar & Maarten Grootendorst: Common Sense Guide to AI Engineering, S. 39

Wenn der AI-Agent nondeterministisch ist, muss der Code, den er aufruft und generiert, umso deterministischer sein. FP liefert genau das.

2. Pure Functions: Der perfekte Vertrag mit der AI

  1. Gleicher Input → gleicher Output – immer
  2. Keine Seiteneffekte – sie verändert nichts außerhalb ihrer selbst

❌ Impure: Der Agent kann nicht reasonen

class UserServiceImpure:
    _users: dict[str, dict] = {}
    _audit_log: list[str] = []

    @classmethod
    def create_user(cls, name: str, email: str) -> dict:
        if name in cls._users:
            return {"error": f"User {name} existiert bereits"}
        user = {"name": name, "email": email.lower(), "active": True}
        cls._users[name] = user
        cls._audit_log.append(f"created:{name}")
        return user

Ruft ein AI-Agent create_user("Alice", "alice@example.com") zweimal auf, bekommt er beim zweiten Mal ein anderes Ergebnis – weil versteckter State sich geändert hat.

✅ Pure: Vorhersagbar für Mensch und Maschine

@dataclass(frozen=True)
class UserState:
    users: tuple[User, ...] = ()
    audit_log: tuple[str, ...] = ()

def create_user(state: UserState, name: str, email: str
) -> tuple[UserState, User | str]:
    existing = next((u for u in state.users if u.name == name), None)
    if existing is not None:
        return state, f"User {name} existiert bereits"
    user = User(name=name, email=email.lower())
    new_state = UserState(
        users=state.users + (user,),
        audit_log=state.audit_log + (f"created:{name}",),
    )
    return new_state, user

Alles explizit: Die Signatur (UserState, str, str) → (UserState, User | str) zeigt, was reingeht und was rauskommt.

"Referentially transparent functions are easier to test and understand in isolation. Furthermore, we can compose, call, and assemble functions that are referentially transparent without losing this property." — Chris Eidhof, Florian Kugler & Wouter Swierstra: Functional Programming in Swift, S. 80

→ Vollständiger Code: src/ai_pure_vs_impure.py

3. Type Signatures als AI-Tool-Definitionen

Wenn ein AI-Agent Tools benutzt (Function Calling), braucht er eine Beschreibung jedes Tools: Name, Parameter, Rückgabetyp. Das ist exakt, was eine FP-Funktionssignatur liefert.

"Understanding and using type signatures is a critical part of coding." — Scott Wlaschin: Domain Modeling Made Functional, S. 72

def validate_email(email: str) -> str:
    """Validiert eine E-Mail-Adresse und gibt sie normalisiert zurück."""
    ...

def calculate_shipping(weight_kg: float, country: str) -> float:
    """Berechnet Versandkosten basierend auf Gewicht und Zielland."""
    ...

Aus diesen Signaturen lassen sich automatisch Tool-Definitionen generieren – direkt in das JSON-Schema für OpenAI Function Calling übersetzbar. Die Signatur A → B ist gleichzeitig Dokumentation, Vertrag und Tool-Definition.

→ Vollständiger Code: src/ai_tool_signatures.py

4. Immutability: Sicher vor versehentlicher Mutation

Ein AI-Agent kann subtile Bugs einführen, wenn er mutierbaren State verwendet:

# AI generiert Code, der eine Liste verändert...
def add_premium_badge(users: list[dict]) -> list[dict]:
    for user in users:
        user["badge"] = "premium"  # Mutation des Originals!
    return users

Mit immutablen Datenstrukturen ist das unmöglich:

@dataclass(frozen=True)
class User:
    name: str
    badge: str = ""

def add_premium_badge(users: tuple[User, ...]) -> tuple[User, ...]:
    return tuple(replace(u, badge="premium") for u in users)

"A large class of software bugs boil down to one section of code modifying data in another section in an unexpected way. By making our data immutable, we can avoid this class of bugs altogether." — Stuart Sierra & Luke VanderHart: Functional Patterns, S. 27

5. Komposition: Kleine Bausteine statt monolithischer Blöcke

AI-Agents sind besonders gut darin, kleine, klar definierte Aufgaben zu lösen:

"Composition is the essence of all things categorical. The key insight is that the sum of two parts is no more complex than the parts themselves." — Boris Marinov: Category Theory Illustrated, S. 29

  1. Kleine Funktionen sind leichter zu generieren
  2. Kleine Funktionen sind leichter zu testen
  3. Komposition ist mechanischbind und map folgen festen Regeln

6. Die fünf Gründe im Überblick

Eigenschaft Warum gut für AI-Agents?
Pure Functions Vorhersagbares Verhalten – der Agent kann das Ergebnis antizipieren
Immutability Kein versehentliches Überschreiben – kein versteckter State
Type Signatures Selbstdokumentierende Tool-Definitionen
Komposition Kleine, testbare Bausteine
Result-Monade Explizite Fehlerbehandlung im Rückgabetyp

"Programming is not about data. It's about transforming data. Every program we write takes some input and transforms it into some output." — Dave Thomas & Bruce Tate: Functional Anthology, S. 115

7. Der FP-Systemprompt

Alles aus fünf Artikeln, destilliert in einen Systemprompt für AI-Agents:

## FP-Regeln für Code-Generierung

### Regel 1: Schreibe Pure Functions
Gleicher Input  gleicher Output. Keine Seiteneffekte. Alle Abhängigkeiten als Parameter.

### Regel 2: Nutze immutable Datenstrukturen
@dataclass(frozen=True), tuple statt list. Neue Objekte statt Mutation.

### Regel 3: Nutze sprechende Type Signatures
Type Hints für alle Parameter und Rückgabewerte. A  Result[B] für fehlbare Operationen.

### Regel 4: Komponiere kleine Funktionen
Komplexe Logik in unabhängig testbare Funktionen zerlegen. Verbinden mit bind, map, Pipelines.

### Regel 5: Verwende Result statt Exceptions für erwartbare Fehler
Ok/Err als Rückgabetyp. Exceptions nur für unerwartete Fehler.

### Regel 6: Deklarativ statt imperativ
map, filter, Comprehensions, Pattern Matching statt Schleifen und if/else-Ketten.

### Regel 7: Higher-Order Functions für Konfiguration
Konfigurierte Funktionen mit Closures statt Parameter-Schleifen.

Der Prompt ist selbst als immutable Datenstruktur modelliert – natürlich mit @dataclass(frozen=True). Er ist testbarer, komponierbarer Code, keine magische Zeichenkette.

→ Vollständiger Code: src/fp_system_prompt.py

Selbst ausprobieren

Eine Mini-Tool-Registry, wie ein AI-Agent sie nutzen würde: Pure Functions als Tools, Type-Signaturen werden automatisch in eine JSON-Schema-artige Beschreibung übersetzt, ein simulierter Agent-Call führt das Tool deterministisch aus.

import inspect
import json
from dataclasses import dataclass

# 1. Pure Tools – gleicher Input → gleicher Output
def add(a: int, b: int) -> int:
    """Addiert zwei ganze Zahlen."""
    return a + b

def greet(name: str, formal: bool) -> str:
    """Begrüßt einen Nutzer formell oder informell."""
    return f"Guten Tag, {name}." if formal else f"Hi, {name}!"

# 2. Automatische Tool-Definition aus der Signatur
PY_TO_JSON = {int: "integer", float: "number", str: "string", bool: "boolean"}

def tool_schema(fn):
    sig = inspect.signature(fn)
    params = {
        name: {"type": PY_TO_JSON.get(p.annotation, "string")}
        for name, p in sig.parameters.items()
    }
    return {
        "name": fn.__name__,
        "description": (fn.__doc__ or "").strip(),
        "parameters": {"type": "object", "properties": params,
                       "required": list(params)},
    }

REGISTRY = {fn.__name__: fn for fn in [add, greet]}

# 3. Simulierter Agent-Call (deterministisch, weil Tools pure sind)
@dataclass(frozen=True)
class ToolCall:
    name: str
    args: dict

def dispatch(call: ToolCall):
    return REGISTRY[call.name](**call.args)

print(json.dumps([tool_schema(fn) for fn in REGISTRY.values()], indent=2, ensure_ascii=False))
print("---")
for call in [ToolCall("add", {"a": 2, "b": 3}),
             ToolCall("greet", {"name": "Alice", "formal": True}),
             ToolCall("greet", {"name": "Bob", "formal": False})]:
    print(f"{call.name}{call.args} → {dispatch(call)!r}")

Erweiterungs-Idee: Füge ein drittes Tool multiply(a: int, b: int) -> int hinzu – die Registry und das Schema erweitern sich automatisch, ohne dass anderer Code geändert werden muss.

Zusammenfassung

Artikel Konzept Werkzeug
1 – FP-Grundlagen Pure Functions, Immutability, Komposition lambda, map, filter, reduce
2 – Algebraische Datentypen Product Types, Sum Types, Pattern Matching @dataclass(frozen=True), match/case
3 – Funktor, Applicative, Monad Werte im Kontext transformieren und verketten Maybe, Result, Validation
4 – Komplexität verlagern Pipeline statt if/else bind, map, Komposition
5 – FP und AI-Agents FP als Fundament für AI-generierten Code Systemprompt, Tool-Registry, Code-Checks

Die zentrale Erkenntnis

Ein AI-Agent ist nondeterministisch. Der Code, den er aufruft und generiert, sollte es nicht sein. Funktionale Programmierung liefert vorhersagbare Bausteine, die ein Agent zuverlässig kombinieren kann.


Weiterführend: FP & Kategorientheorie für KI-Agents

Diese Artikelserie hat die FP-Grundlagen gelegt. Wer tiefer einsteigen möchte – insbesondere in die Kategorientheorie hinter den Abstraktionen und wie sie konkret für KI-Agent-Architekturen genutzt werden können – findet das in der Folgeserie:

FP for AI Agents – Vier Artikel darüber, wie Funktoren, natürliche Transformationen und Monaden das universelle Steuerungsprotokoll für KI-Agent-Workflows bilden.


Quellen

Code-Beispiele

📄 Als PDF