[!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:
- Welches Team ist für die Regel verantwortlich?
- Welche Sprache verwenden die Fachexperten dafür?
- Innerhalb welcher Grenze hat dieser Begriff eine eindeutige Bedeutung?
- Welche Zustandsänderungen müssen konsistent zusammenbleiben?
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.
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:
- Ein Kontext kann intern mehrere Aufrufe von Model oder spezialisierte Agents verwenden.
- Ein Agent, der von mehreren Kontexten Spans wird, erfordert eine explizite Übersetzung sowie entsprechende Berechtigungen für jeden dieser Kontexte.
- Orchestration ist eine Anwendungsfrage; dadurch ändert sich nicht das Eigentumsverhältnis am jeweiligen Domänenbereich.
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:
- Kernbereich: Die Fähigkeit, einen differenzierten Wert zu schaffen
- Unterbereich zur Unterstützung: Notwendige, branchenspezifische Aufgaben, die nicht zum Alleinstellungsmerkmal gehören
- Allgemeiner Unterbereich: Eine bereits gelöste Funktionalität wie z. B. Identitätsmanagement oder E-Mail-Zustellung
Bei einem Aufgabenhilfsprogramm kann die Aufgabenverwaltung Kernfunktion sein, während die Zeitplanung Unterstützungsfunktion und die Benachrichtigungsübermittlung allgemeine Funktionen darstellen.
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:
- Eine bereits abgeschlossene Aufgabe kann nicht erneut abgeschlossen werden.
- Ein Verantwortlicher darf keine doppelten, offenen Erinnerungen für denselben Tag haben.
- Eine Rückerstattung darf den verbleibenden rückerstattbaren Betrag nicht überschreiten.
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.
Die Grenze umfasst vier Schritte:
- Einschränken und parsen: Es wird ein typisierter Ausgabevertrag erforderlich.
- Normalisieren: Daten zu Datumsangaben, Einheiten, Identifikatoren sowie der Lokalisation werden mithilfe eines zuverlässigen Kontextes aufbereitet.
- Autorisieren: Es wird entschieden, ob dieser Akteur die Anfrage ausführen darf.
- 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:
- Duplikataufgaben werden abgelehnt.
- Gültige Aufgaben lösen das erwartete Ereignis aus.
- Offengelegte Sammlungen dürfen den internen Zustand nicht verändern.
- Übergangsregeln bleiben bei wiederholten Operationen gültig.
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:
- Genauigkeit bei der Extraktion von Intenten und Feldern
- Auflösung relativer Datumsangaben unter Berücksichtigung des bereitgestellten Zeitzone-Kontexts
- Ablehnung oder Nachfrage bei fehlenden erforderlichen Informationen
- Widerstandsfähigkeit gegenüber Prompt Injection innerhalb zitierten Aufgabentexten
- Anteil an schema-konformen, aber semantisch unbrauchbaren Vorschlägen
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
- Eric Evans, Referenzwerk zu Domain-Driven Design — Definitionen strategischer und taktischer Muster Martin Fowler, Begrenzter Kontext — warum ein Model nicht Span jede Bedeutung eines Begriffs abbilden sollte
- Martin Fowler, Repository — Abstraktion der Persistenzsammlung Chris Richardson, Transaktionale Ausgangskorrespondenz — Veröffentlichung nach einer Datenbanktransaktion ohne Verlust von Ereignissen Pydantic-Dokumentation — Validierung des eingegebenen Vorschlags