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
- Gleicher Input → gleicher Output – immer
- 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
- Kleine Funktionen sind leichter zu generieren
- Kleine Funktionen sind leichter zu testen
- Komposition ist mechanisch –
bindundmapfolgen 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
- Jay Alammar & Maarten Grootendorst: Common Sense Guide to AI Engineering, S. 34, 39
- Scott Wlaschin: Domain Modeling Made Functional, S. 72, 166
- Stuart Sierra & Luke VanderHart: Functional Patterns, S. 27, 189
- Chris Eidhof, Florian Kugler & Wouter Swierstra: Functional Programming in Swift, S. 80
- Boris Marinov: Category Theory Illustrated, S. 29
- Dave Thomas & Bruce Tate: Functional Anthology, S. 115
Code-Beispiele
src/ai_pure_vs_impure.py– Pure vs. Impure: Vergleich mit State-Managementsrc/ai_tool_signatures.py– FP-Funktionen als AI-Agent-Tools mit automatischer Registrysrc/fp_system_prompt.py– Der FP-Systemprompt als testbare Datenstruktur mit Code-Checks