[!NOTE] Automatische Übersetzung Dieser Artikel wurde automatisch aus der englischen Originalversion übersetzt.

Domain-Driven Design für AI Agents: Begrenzte Kontexte, Werkzeuge und Geschäftsregeln

Agent-Projekte werden schwierig zu ändern, wenn Prompts, der Code sowie die Geschäftsprozesse unterschiedliche Begriffe verwenden. Die Konformitätsanforderungen erfordern eine „Richtlinienprüfung“, während die Implementierung tatsächlich Probleme aufweist. process_data(). Der vage Name verschleiert, welche Regel angewendet wird, wer für sie verantwortlich ist und wohin eine Änderung gehören sollte.

Domain-Driven Design (DDD) stellt diese Geschäftsprache sowie die zugehörige Verantwortung in den Mittelpunkt. Bei einem Agent kann der Model eine Anfrage interpretieren und einen typisierten Befehl vorschlagen, wobei ein Anwendungsservice einen zuverlässigen Kontext bereitstellt und das Domänen-Model den Zustandswechsel entweder akzeptiert oder ablehnt. Diese Anleitung verknüpft das entsprechende Vokabular sowie die Grenzdefinitionen mit diesem Ausführungspfad.

Zusammenfassung. Verwenden Sie DDD, wenn ein Agent den Geschäftsstatus in einem Domänenbereich mithilfe von sinnvoller Sprache, klaren Verantwortlichkeiten und festgelegten Regeln ändert. Ein Schema überprüft die Struktur eines Vorschlags für einen Model; die Domäne stellt hingegen sicher, dass dieser Vorschlag die richtige Bedeutung hat. Verwechseln Sie Agents nicht mit begrenzten Kontexten und betrachten Sie generierte JSON nicht als gültige Geschäftsentscheidungen.


Das eigentliche Problem ist das Eigentumsverhältnis der Regeln

Agent-Systeme verteilen oft dieselbe Regel auf einen System Prompt, eine Tool-Beschreibung, einen API-Handler sowie eine Datenbankbeschränkung. Dadurch entstehen Abweichungen zwischen den Kopien. Wenn sich beispielsweise die Rückerstattungsgrenze ändert, bleibt eine Prompt weiterhin veraltet, wodurch ein syntaktisch korrekter Tool Call auf die falsche Richtlinie angewendet wird.

DDD beginnt damit, unterschiedliche Fragen zu stellen:

Diese Fragen sind nützlich, wenn ein Workflow wichtig genug ist, um über eigene Richtlinien sowie einen definierten Lebenszyklus zu verfügen. Ein einfacher, nur zum Lesen geeigneter Chatbot benötigt möglicherweise weder Aggregaten noch Repositorien oder Ereignisse. Nutzen Sie DDD, um die Komplexität des Domänenbereichs zu bewältigen – und nicht dazu, jeden Aufruf von LLM mit überflüssigen Strukturen zu versehen.

Strategische Gestaltung vor dem Code

Eine allgegenwärtige Sprache entwickeln

Eine allgegenwärtige Sprache ist ein Wortschatz, der von Fachexperten und Entwicklern innerhalb eines begrenzten Kontexts geteilt wird. Wenn die Support-Abteilungen sagen RefundRequest, approval limit, und settlementDiese Begriffe müssen in den Anforderungen, im Code, in den Tool-Kontrakten sowie bei den Bewertungen enthalten sein.

Es geht hierbei um mehr als nur die Auswahl beschreibender Methodennamen – die Begriffe benötigen klare Definitionen sowie konkrete Beispiele. Bedeutet „genehmigt“ etwa, dass ein Manager einen Button geklickt hat, der Zahlungsdienstleister den Überweisungsvorgang akzeptiert hat, oder beides? Eine Unklarheit, die bereits im Glossar entdeckt wird, ist kostengünstiger als eine Unklarheit, die erst in Agent Trace auftritt.

Zeichnen Sie begrenzte Kontexte um Models sowie die Eigentumsverhältnisse herum.

Derselbe Substantivbegriff kann je nach Kontext unterschiedliche Bedeutungen haben. „Produkt“ kann in der Inventur eine Lagerhaltungseinheit sein, in der Rechnungsstellung eine preisgegebene Position und in der Auftragsverwaltung ein Lieferversprechen.

Das Wort „Produkt“ wird in unterschiedlichen begrenzten Kontexten unterschiedlich modelliert.

Ein begrenzter Kontext besitzt seine eigene Model und führt die Übersetzung an seiner Grenze durch. Er ist nicht automatisch ein Microservice, ein Repository, eine Agent oder ein Team – obwohl sich diese Grenzen häufig überschneiden.

Diese Unterscheidung ist bei der Gestaltung von Agent von Bedeutung:

Beginnen Sie mit einer Kontextkarte, bevor Sie ein Agent-Grafik erstellen. Andernfalls spiegelt das Grafikmodell in der Regel eher die Verfügbarkeit der Tools wider als die tatsächlichen Geschäftsprozesse.

Klassifizieren Sie die Subdomänen

DDD trennt in der Regel auf folgende Weise ab:

Bei einem Aufgabenhilfsprogramm kann die Aufgabenverwaltung Kernfunktion sein, während die Zeitplanung Unterstützungsfunktion und die Benachrichtigungsübermittlung allgemeine Funktionen darstellen.

Aufgabenverwaltung, Zeitplanung und Benachrichtigungen als eigenständige Kontexte

Die Klassifizierung leitet die Investitionen. Das bedeutet jedoch nicht, dass jedes Kategoriefeld einen LLM erfordert.


Taktische Muster definieren die Zustandsgrenze

Entitäten und Wertobjekte

Eine Entity verfügt über eine Identität sowie ein Lebenszyklus. Eine Aufgabe bleibt weiterhin derselbe Auftrag, auch wenn sich ihre Beschreibung ändert. Ein Value Object wird durch seine Werte definiert und ist in der Regel unveränderlich – beispielsweise eine E-Mail-Adresse, eine Geldsumme oder ein Zeitfenster.

Aggregate-Werte und Invarianten

Ein Aggregate ist eine Konsistenzgrenze. Seine Wurzel stellt die Operationen zur Verfügung, mit denen Mitglieder geändert werden können, und schützt dabei Invarianten wie:

Ein Aggregat wird nicht automatisch sicher, nur weil eine Python-Liste dahinterliegt. add_task() Methoden: Externer Code darf keine veränderbare Referenz erhalten, die es ihm ermöglicht, diese Methode zu umgehen. Auch bei der Persistenz ist ein Konkurrenzmanagement erforderlich, da zwei gültige Anfragen beim gleichzeitigen Speichern eine Invarianz verletzen könnten.

Repositorien und Anwendungs-Dienste

Ein Repository lädt und speichert Aggregatdaten, ohne dass Datenbankaspekte in den Domänenbereich durchsickern. Ein Anwendungsservice koordiniert einen bestimmten Anwendungsfall: Er lädt den Zustand, ruft die Operation der Domäne auf, speichert die Daten unter Berücksichtigung der erwarteten Version und veröffentlicht die resultierenden Ereignisse.

Der Domain darf weder einen LLM, einen HTTP-Client noch ein ORM aufrufen. Es handelt sich dabei um Adapter, die die jeweiligen Anwendungsfälle abdecken.

Domänenereignisse sind Fakten – keine Message-Bus-Struktur

TaskAdded Es handelt sich um einen im Vergangenheit liegenden Sachverhalt, der vom jeweiligen Domänenobjekt bereitgestellt wird. Die Anwendung kann diesen Zustand zusammen mit den übrigen Aktualisierungen in einem Ausgangspostfach speichern und anschließend nach der Commit-Vorgangs ein Integrationsevent veröffentlichen. Ein direkter Versand an einen Broker seitens einer Entität birgt das Risiko, ein Event für eine Transaktion zu senden, die später fehlschlägt.

Ereignisse können Agents koordinieren, sorgen aber allein nicht für zuverlässige Koordination. Semantiken zur Zustellung, Idempotenz, Sortierung sowie versionierte Verträge gehören weiterhin zum Aufgabenbereich der Infrastruktur.


Behandeln Sie die Ausgabe von Model als einen unzuverlässigen Vorschlag

Eine Integration mit LLM ähnelt einer Antikorruptionsschicht: Sie wandelt eine externe, probabilistische Repräsentation in Begriffe um, die vom jeweiligen Domänenkontext verstanden werden können. Diese Analogie bleibt sinnvoll, solange Validierung und Regelverwaltung weiterhin getrennt voneinander erfolgen.

Model ausgegebene Werte werden als Domänenbefehl übersetzt und mithilfe deterministischer Regeln überprüft.

Die Grenze umfasst vier Schritte:

  1. Einschränken und parsen: Es wird ein typisierter Ausgabevertrag erforderlich.
  2. Normalisieren: Daten zu Datumsangaben, Einheiten, Identifikatoren sowie der Lokalisation werden mithilfe eines zuverlässigen Kontextes aufbereitet.
  3. Autorisieren: Es wird entschieden, ob dieser Akteur die Anfrage ausführen darf.
  4. Ausführen: Eine Aggregatmethode wird aufgerufen, die die Invarianz gewährleistet.

Pydantic kann ein fehlendes Feld oder eine ungültige Enum ablehnen. Es ist jedoch nicht in der Lage zu beurteilen, ob „morgen“ auf das korrekte Datum verweist, ob der Benutzer die Aufgabenliste besitzt, oder ob bereits eine ähnliche Aufgabe offen ist.

Beispielanwendung: Eine Aufgabe sicher hinzufügen

1. Definieren des vorgeschlagenen Ansatzes für den Model-Kontakt

Stellen Sie den Vorschlag so dar, dass er dem, was der Model ableiten kann, möglichst nahekommt. Verlangen Sie nicht von ihm, Datenbank-IDs oder Identifikatoren für vertrauenswürdige Eigentümer zu erfinden.

from datetime import date
from typing import Literal

from pydantic import BaseModel, Field


class AddTaskProposal(BaseModel):
    description: str = Field(min_length=1, max_length=200)
    due_date: date | None = None
    priority: Literal["low", "normal", "high"] = "normal"

Falls der Benutzer „morgen“ angibt, muss die Anwendung dem Model einen expliziten lokalen Datumswert zuweisen oder den relativen Ausdruck mithilfe eines erprobten Datuparsers auflösen. Man darf niemals die Uhr des Inference Server als impliziten Geschäftskontext verwenden.

2. Fügen Sie die Invariante dem Aggregat hinzu.

from dataclasses import dataclass, field
from datetime import date
from uuid import UUID, uuid4


@dataclass(frozen=True)
class Task:
    task_id: UUID
    description: str
    due_date: date | None
    priority: str


@dataclass
class TaskList:
    owner_id: UUID
    version: int
    _tasks: dict[UUID, Task] = field(default_factory=dict)
    _events: list[object] = field(default_factory=list)

    @property
    def tasks(self) -> tuple[Task, ...]:
        return tuple(self._tasks.values())

    def pull_events(self) -> tuple[object, ...]:
        events = tuple(self._events)
        self._events.clear()
        return events

    def add_task(
        self,
        description: str,
        due_date: date | None,
        priority: str,
    ) -> Task:
        normalized = " ".join(description.casefold().split())
        duplicate = any(
            " ".join(task.description.casefold().split()) == normalized
            and task.due_date == due_date
            for task in self._tasks.values()
        )
        if duplicate:
            raise ValueError("A matching task already exists for that date")

        task = Task(uuid4(), description.strip(), due_date, priority)
        self._tasks[task.task_id] = task
        self._events.append(TaskAdded(task.task_id, self.owner_id))
        return task

Das Beispiel lässt diesen Ausdruck aus. TaskAdded Definition zur Kürze: In einem vollständigen Domänenmodul handelt es sich dabei um ein unveränderliches Wertobjekt. Das Aggregat stellt einen Tupel-Zugriff bereit anstelle seines veränderlichen Dictionarys, sodass Aufrufer keine Elemente hinzufügen können. add_task().

Die Duplikatenerkennung ist hier absichtlich einfach gehalten. Reale Regeln könnten eine lokalisierte Normalisierung, rekursive Semantik oder eine Eindeutigkeitsbeschränkung in der Datenbank als letzte, race-sichere Absicherung erfordern.

3. Definieren Sie den Port des Repositoriums

from typing import Protocol
from uuid import UUID


class ConcurrentUpdate(Exception):
    pass


class TaskListRepository(Protocol):
    def get(self, owner_id: UUID) -> TaskList: ...

    def save(self, task_list: TaskList, expected_version: int) -> None: ...

Der Infrastrukturadapter kann optimistische Konkurrenzverarbeitung mithilfe einer Versionsspalte umsetzen. Der Domänenvertrag definiert die relevanten Aspekte unabhängig von SQLAlchemy oder einer bestimmten Datenbank.

4. Koordination des Anwendungsfalles

from uuid import UUID


class AddTaskService:
    def __init__(
        self,
        repository: TaskListRepository,
        authorizer: TaskAuthorizer,
        outbox: Outbox,
    ) -> None:
        self.repository = repository
        self.authorizer = authorizer
        self.outbox = outbox

    def execute(
        self,
        actor_id: UUID,
        owner_id: UUID,
        proposal: AddTaskProposal,
    ) -> Task:
        self.authorizer.require_add_permission(actor_id, owner_id)

        task_list = self.repository.get(owner_id)
        expected_version = task_list.version
        task = task_list.add_task(
            description=proposal.description,
            due_date=proposal.due_date,
            priority=proposal.priority,
        )

        # Implement both writes in one database transaction.
        self.repository.save(task_list, expected_version)
        self.outbox.add_all(task_list.pull_events())
        return task

Der Kommentar zur Transaktion ist von entscheidender Bedeutung: Der Speichervorgang im Repository zusammen mit dem Einfügen in den Outbox muss entweder gemeinsam erfolgreich sein oder gemeinsam fehlschlagen. Eine Abstraktion der Arbeitseinheit kann diese Transaktion verwalten, sofern das konkrete Repository und der Outbox denselben Datenbankserver nutzen.

Der Model fehlt in diesem Service. Ein Adapter kann ihn jedoch abrufen. AddTaskProposal Eines stammt von einem LLM, ein weiteres aus einem HTTP-Formular, und die Tests können es direkt erstellen. Das Geschäftsverhalten bleibt unverändert.


Tools für die Zuordnung zu Anwendungsbefehlen

Agent Werkzeuge sollten Anwendungsfallbeispiele bereitstellen und nicht direkt Datenbankprimitiven. Es wird empfohlen:

add_task(description, due_date, priority)
complete_task(task_id)
reschedule_task(task_id, due_date)

über:

insert_row(table, values)
update_record(table, id, patch)

Die erste Komponente spricht die Domänsprache und bietet der Anwendung eine Plattform zur Autorisierung sowie Durchsetzung von Regeln. Die zweite ermöglicht es dem Model, beliebige Persistenzänderungen vorzunehmen.

Ein Tool Result muss fehlerhafte Zustände unterscheiden, auf die der Agent reagieren kann: ungültige Vorschläge, nicht autorisierte Akteure, Domänenkonflikte, gleichzeitige Aktualisierungen sowie nicht verfügbare Infrastrukturen. Es darf nicht alle Fehler in einen einzigen String zusammengefasst werden, der blinde Wiederholungsversuche provoziert.

Die Grenze schichtweise testen

Domänentests

Testen Sie Aggregaten ohne Model, Netzwerk oder Datenbank:

Anwendungstests

Verwenden Sie falsche Repositorien sowie Autorisierungsmechanismen, um das Laden, die Reihenfolge der Autorisierung, das Speichern der erwarteten Versionen, das Verhalten des Ausgabepuffers sowie die Fehlerkennzeichnung zu überprüfen.

Model – Vertragsbewertungen

Bewerten Sie den probabilistischen Adapter getrennt voneinander:

Ein End-to-End-Test sollte anschließend sicherstellen, dass fehlerhafte Vorschläge niemals die gleichen Domänenmethoden umgehen, die von zuverlässigen Schnittstellen genutzt werden.

Wenn das Design funktioniert

Sie sollten in der Lage sein, den Model-Anbieter zu wechseln, ohne einen Domain-Test ändern zu müssen. Eine Richtlinienänderung sollte lediglich einen Aggregat- oder Domaindienst beeinflussen und nicht mehrere Prompts. Ein Trace sollte Begriffe verwenden, die vom zuständigen Team verstanden werden. Eine fehlerhafte oder nicht autorisierte Vorschlag sollte bereits vor der Speicherung abgelehnt werden, und ein gleichzeitiger Speichervorgang sollte fehlschlagen, anstatt den Zustand stillschweigend zu überschreiben.

Das ist der praktische Nutzen von DDD für Agents. Es macht einen Model nicht deterministisch. Stattdessen macht es die Grenzen bezüglich der Autorität des Systems, seiner Sprache sowie seiner Konsistenz ausreichend explizit, sodass eine Model gar nicht erforderlich ist.

Referenzen