Wie funktionale Prinzipien den Code, den KI-Agents generieren, aufrufen und kombinieren, vorhersagbar und sicher machen.
1. Das Problem: Nondeterminismus trifft auf Code
KI-Agents – ob ChatGPT, Claude, Copilot oder ein eigener Agent – haben eine fundamentale Eigenschaft: Sie sind nondeterministisch. Derselbe Prompt kann verschiedene Code-Varianten erzeugen.
"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
Das bedeutet: Manchmal generiert der Agent eine Klasse, manchmal eine Funktion, manchmal globalen State. 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
Die Lösung: Wenn der Agent nondeterministisch ist, muss der Code, den er aufruft und generiert, umso deterministischer sein. Functional Programming liefert genau das.
2. Pure Functions: Der perfekte Vertrag mit der KI
Eine Pure Function hat zwei Eigenschaften:
- Gleicher Input → gleicher Output – immer
- Keine Seiteneffekte – sie verändert nichts außerhalb ihrer selbst
Für einen KI-Agent bedeutet das: Wenn er eine Pure Function aufruft, kann er sich auf das Ergebnis verlassen.
❌ Impure: Der Agent kann nicht reasonen
class TaskManagerImpure:
_tasks: dict[str, dict] = {}
_history: list[str] = []
@classmethod
def add_task(cls, task_id: str, title: str) -> dict:
if task_id in cls._tasks:
return {"error": f"Task {task_id} existiert bereits"}
task = {"id": task_id, "title": title, "done": False}
cls._tasks[task_id] = task
cls._history.append(f"added:{task_id}")
return task
Wenn der Agent add_task("t1", "Test") zweimal aufruft, bekommt er beim zweiten Mal ein anderes Ergebnis – nicht weil die Funktion anders arbeitet, sondern weil versteckter State sich geändert hat.
✅ Pure: Vorhersagbar für Mensch und Maschine
@dataclass(frozen=True)
class Task:
id: str
title: str
done: bool = False
@dataclass(frozen=True)
class TaskState:
tasks: tuple[Task, ...] = ()
history: tuple[str, ...] = ()
def add_task(state: TaskState, task_id: str, title: str
) -> tuple[TaskState, Task | str]:
existing = next((t for t in state.tasks if t.id == task_id), None)
if existing is not None:
return state, f"Task {task_id} existiert bereits"
task = Task(id=task_id, title=title)
new_state = TaskState(
tasks=state.tasks + (task,),
history=state.history + (f"added:{task_id}",),
)
return new_state, task
Jetzt ist alles explizit: Der Agent sieht an der Signatur (TaskState, str, str) → (TaskState, Task | str), was reingeht und was rauskommt. Der State ist ein Parameter, kein Geheimnis.
"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
Und das Entscheidende: Gleicher Input → gleicher Output, immer. Der Agent kann add_task(state, "t1", "Test") mit demselben state beliebig oft aufrufen und bekommt immer dasselbe Ergebnis.
→ Vollständiger Code: src/agent_pure_functions.py
3. Immutability: Schutz vor versehentlicher Mutation
KI-Agents können subtile Bugs einführen, wenn sie mutierbaren State verwenden. Das klassische Aliasing-Problem wird mit Agents noch gefährlicher:
❌ Mutable: Versteckte Seiteneffekte
def add_tag_mutable(items: list[dict], tag: str) -> list[dict]:
for item in items:
item["tags"] = item.get("tags", []) + [tag]
return items
Der Agent ruft diese Funktion auf und die Original-Daten werden verändert. Jeder weitere Zugriff auf items sieht die Mutation – ohne es zu wissen.
✅ Immutable: Sicher für Agents
@dataclass(frozen=True)
class Item:
name: str
tags: tuple[str, ...] = ()
def add_tag(items: tuple[Item, ...], tag: str) -> tuple[Item, ...]:
return tuple(replace(item, tags=item.tags + (tag,)) for item in items)
Das Original bleibt immer unverändert. Der Agent kann diese Funktion beliebig oft aufrufen, ohne Seiteneffekte.
"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
Immutable Workflow-State
Dasselbe Prinzip gilt für Agent-Workflows: Jeder Schritt erzeugt einen neuen State, statt den alten zu mutieren:
@dataclass(frozen=True)
class WorkflowState:
steps: tuple[WorkflowStep, ...] = ()
current_index: int = 0
def complete_current_step(state: WorkflowState, result: str
) -> WorkflowState:
# ... erzeugt neuen State, Original bleibt unverändert
Der Agent kann jeden Zwischenstand nachvollziehen – und bei Fehlern einfach zum vorherigen State zurückkehren.
→ Vollständiger Code: src/agent_immutability.py
4. Explizite Signaturen: Die Sprache zwischen Mensch und Agent
Type Signatures sind für KI-Agents besonders wertvoll. Sie sind gleichzeitig Dokumentation, Vertrag und Maschinenlesbare Spezifikation.
"Understanding and using type signatures is a critical part of coding." — Scott Wlaschin: Domain Modeling Made Functional, S. 72
Vergleiche:
# Implizit: Was kommt rein? Was kommt raus? Kann es fehlschlagen?
def process(data):
...
# Explizit: Alles steht in der Signatur
def process(data: TaskState) -> tuple[TaskState, Task | str]:
...
Für einen KI-Agent ist die explizite Signatur ein Vertrag: Er sieht genau, welche Typen erwartet werden und was zurückkommt. Implizite Kontrakte – globaler State, versteckte Exceptions, undokumentierte Nebeneffekte – sind für Agents unsichtbar.
Wenn der Agent Tools per Function Calling aufruft, braucht er eine Beschreibung: Name, Parameter, Rückgabetyp. Die FP-Signatur A → B liefert genau das – automatisch.
5. Die FP-Checkliste für KI-Code
| Eigenschaft | Warum gut für KI-Agents? |
|---|---|
| Pure Functions | Vorhersagbares Verhalten – der Agent kann das Ergebnis antizipieren |
| Immutability | Kein versehentliches Überschreiben – kein versteckter State |
| Type Signatures | Selbstdokumentierende Tool-Definitionen – die Signatur ist der Vertrag |
| Expliziter State | Der Agent sieht alle Abhängigkeiten in den Parametern |
| Keine Seiteneffekte | Der Agent kann Funktionen isoliert testen und kombinieren |
Die gemeinsame Idee:
"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
Wenn Code als Datentransformation organisiert ist – kleine Funktionen, die Input in Output verwandeln – dann kann ein KI-Agent jeden einzelnen Schritt verstehen, generieren, testen und kombinieren.
Selbst ausprobieren
Genau das, was diesen Artikel zusammenhält, lässt sich in wenigen Zeilen direkt im Browser ausprobieren: ein TaskState aus frozen-Dataclasses und eine reine add_task-Funktion, die (state, task_id, title) → (TaskState, Task | str) abbildet. Kein globaler State, keine Seiteneffekte – derselbe Input ergibt immer denselben Output, und das Original bleibt unverändert.
from dataclasses import dataclass
@dataclass(frozen=True)
class Task:
id: str
title: str
done: bool = False
@dataclass(frozen=True)
class TaskState:
tasks: tuple = ()
history: tuple = ()
def add_task(state: TaskState, task_id: str, title: str):
"""Reine Funktion: gibt (neuer_state, ergebnis) zurück."""
if any(t.id == task_id for t in state.tasks):
return state, f"Task {task_id} existiert bereits"
task = Task(id=task_id, title=title)
new_state = TaskState(
tasks=state.tasks + (task,),
history=state.history + (f"added:{task_id}",),
)
return new_state, task
s0 = TaskState()
s1, r1 = add_task(s0, "t1", "Spec schreiben")
s2, r2 = add_task(s1, "t2", "Tests grün machen")
s3, r3 = add_task(s2, "t1", "doppelt einfügen") # absichtlich Konflikt
print("r1:", r1)
print("r2:", r2)
print("r3:", r3)
print("Original s0 unverändert:", s0)
print("Anzahl Tasks in s2:", len(s2.tasks), "| in s3:", len(s3.tasks))
Erweiterungs-Idee: Schreibe analog complete_task(state, task_id) → (TaskState, Task | str), die den Task immutabel auf done=True setzt – die Signatur bleibt der Vertrag, den der Agent zur Komposition braucht.
Ausblick
Diese fünf Eigenschaften sind nicht zufällig – sie haben ein formales Fundament in der Kategorientheorie. Im nächsten Artikel zeigen wir, was Kategorien, Funktoren und natürliche Transformationen sind – und warum sie für KI-Agent-Architektur relevant sind.
Quellen
- Jay Alammar & Maarten Grootendorst: Common Sense Guide to AI Engineering, S. 34, 39
- Chris Eidhof, Florian Kugler & Wouter Swierstra: Functional Programming in Swift, S. 80
- Stuart Sierra & Luke VanderHart: Functional Patterns, S. 27
- Scott Wlaschin: Domain Modeling Made Functional, S. 72
- Dave Thomas & Bruce Tate: Functional Anthology, S. 115
Code-Beispiele
src/agent_pure_functions.py– Pure vs. Impure: Task-Manager mit explizitem Statesrc/agent_immutability.py– Immutable Datenstrukturen und Workflow-State