[!NOTE] Automatische vertaling Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.
Domain-Driven Design voor AI Agents: Begrensde contexten, hulpmiddelen en bedrijfsregels
Wanneer prompts, code en bedrijfsprocessen verschillende terminologie gebruiken, worden Agent projecten moeilijk aan te passen. Voor compliance is een “policy check” vereist, terwijl de implementatie dit probleem juist blootlegt. process_data(). De vage benaming verhult voor welk regelgevingsprincipe er wordt gegrepen, wie daar verantwoordelijk voor is, en waar een wijziging thuishoort.
Domain-Driven Design (DDD) plaatst die bedrijfsstaal en eigenaarschap centraal. Voor een agent kan de model een verzoek interpreteren en een getypeerde opdracht voorstellen, maar een applicatieservice levert betrouwbare context, waarna de domein model de staatswijziging accepteert of afwijst. Deze gids brengt het vocabulaire en de grensdefinitie in verband met die uitvoeringsweg.
Kort samengevat. Gebruik DDD wanneer een agent de bedrijfsstate in een domein verandert, waarbij sprake is van betekenisvolle taal, eigenaarschap en regels. Een schema controleert de vorm van een voorstel voor een model; het domein zorgt ervoor dat de betekenis wordt nageleefd. Stel agents niet gelijk aan begrensde contexten, en beschouw gegenereerde JSON niet als een geldige bedrijfsbeslissing.
Het echte probleem is de eigendomsverhouding van regels
Agent-systemen verdelen vaak één regel over een system prompt, een beschrijving van een hulpprogramma, een API-handler en een databasebeperking. De kopieën komen uit elkaar te liggen. Wanneer de limiet voor restituties verandert, blijft één prompt achter met oude waarden, terwijl een syntactisch geldige tool call wordt toegepast op de verkeerde beleidsregels.
DDD begint met het stellen van verschillende vragen:
- Welk team is verantwoordelijk voor de regel?
- Welke taal gebruiken domeinexperts hiervoor?
- Binnen welke grenzen heeft die term één enkele betekenis?
- Welke toestandsveranderingen moeten samen consistent blijven?
Die vragen zijn nuttig wanneer een workflow belangrijk genoeg is om beleidsregels en een levenscyclus te hebben. Een eenvoudige alleen-lezen chatbot heeft mogelijk geen aggregaten, repositories of events nodig. Gebruik DDD om de complexiteit van het domein te beheren, en niet om elke LLM-aanroep te versieren.
Strategisch ontwerp vóór de code
Een alomtegenwoordige taal bouwen
Een universele taal is een woordenschat die wordt gedeeld door domeinexperts en ontwikkelaars binnen één afgebakend context. Als de supportafdeling zegt RefundRequest, approval limit, en settlementDie termen moeten voorkomen in de specificaties, de code, de contracten voor tools en in de evaluaties.
Dit gaat verder dan het kiezen van beschrijvende methoondnamen. Termen vereisen definities en voorbeelden. Betekent “goedgekeurd” dat een manager op een knop heeft gedrukt, dat de betalingsprocessor de overboeking heeft geaccepteerd, of beide? Ambigiteit die al in een glossary wordt vastgesteld, is goedkoper dan ambigiteit die pas later bij agent trace naar voren komt.
Teken begrensde contexten rondom models en eigendom
Hetzelfde zelfstandig naamwoord kan in verschillende contexten verschillende betekenissen hebben. “Product” kan een voorraadhoudende eenheid zijn in Inventory, een geprijsde lijst in Billing, en een leveringsverplichting in Order Management.
Een begrensd context beheert zijn eigen model en voert vertaling uit aan de grenzen daarvan. Het is niet automatisch een microservice, repository, agent of team, hoewel deze grenzen vaak overeenkomen.
Deze onderscheiding is belangrijk bij het ontwerpen van agent:
- Eén context kan intern meerdere model-aanroepen of gespecialiseerde agents-objecten gebruiken.
- Een agent die door meerdere contexten spans wordt gebruikt, vereist een expliciete vertaling en toestemming voor elke context.
- orchestration valt onder de verantwoordelijkheid van de applicatie; dit doet niets af aan het domeinbeheer.
Begin met een contextkaart voordat je een agent-grafiek tekenen. Anders heeft de grafiek de neiging om de beschikbaarheid van tools weer te geven in plaats van de bedrijfsactiviteiten.
Classificeer de subdomeinen
DDD scheidt doorgaans:
- Core domein: de capaciteit die een differentieerende waarde creëert
- Ondersteunend subdomein: noodzakelijke, bedrijfsspecifieke werkzaamheden die niet als differentiator dienen
- Algemeen subdomein: een reeds opgeloste capaciteit, zoals identiteitsbeheer of e-maillevering
Voor een taakassistent kan taakbeheer kernfunctie zijn, terwijl planningondersteuning en notificatieretour generiek van aard zijn.
De classificatie stuurt de investeringen richting. Dat betekent echter niet dat elke categorie een LLM nodig heeft.
Tactische patronen bepalen de grens van de toestand
Entiteiten en waardeobjecten
Een entity beschikt over een identiteit en een levenscyclus. Een taak blijft dezelfde taak, zelfs wanneer de beschrijving ervan verandert. Een value object wordt gedefinieerd door zijn waarden en is doorgaans onveranderlijk: bijvoorbeeld een e-mailadres, een bedrag of een tijdsinterval.
Agregaten en invarianten
Een aggregate is een consistentiegrens. De wortel ervan biedt toegang tot de operaties die leden kunnen wijzigen en beschermt invarianten zoals:
- Een voltooid taak kan niet opnieuw worden afgerond
- Een eigenaar mag geen dubbele, openstaande herinneringen voor dezelfde dag hebben
- Een teruggave mag het nog resterende teruggavebedrag niet overschrijden
Een aggregate wordt niet automatisch veilig alleen omdat er een Python-list achter zit add_task() Methoden: externe code mag geen mutabele referentie ontvangen die deze methoden omzeilt. Voor persistente gegevens is bovendien controle op gelijktijdige toegang noodzakelijk, aangezien twee geldige verzoeken bij gelijktijdige opslag een invariant kunnen schenden.
Repositoren en applicatieservices
Een repository laadt en slaat aggregaten op, zonder dat databasegerelateerde zorgen worden overgedragen naar het domein. Een applicatieservice coördineert één gebruiksscenario: het laden van de staat, het uitvoeren van de domeinoperatie, het opslaan met de verwachte versie, en het publiceren van de resulterende gebeurtenissen.
Het domein mag geen LLM, HTTP-client of ORM aanroepen. Dit zijn adapters die dienen ter ondersteuning van de betreffende use case.
Domeingebeurtenissen zijn feiten, geen berichtbus
TaskAdded Het betreft een feit uit het verleden dat door het domein wordt gegenereerd. De applicatie kan dit feit samen met de samenvattende update opslaan in een outbox, en vervolgens een integratie‑event publiceren nadat de commit heeft plaatsgevonden. Het direct verzenden naar een broker vanuit een entiteit brengt het risico met zich mee dat er een event voor een transactie wordt gepubliceerd die later faalt.
Gebeurtenissen kunnen agents coördineren, maar ze zorgen op zichzelf niet voor betrouwbare coördinatie. Semantiek rondom levering, idempotentie, ordening en geversioneerde contracten blijven onderdeel van de infrastructuurontwikkeling.
Behandel de uitvoer van model als een onbetrouwbare voorstel.
Een integratie van LLM fungeert als een anticorruptielayer: het vertaalt een externe, probabilistische weergave naar termen die door het desbetreffende domein begrepen worden. Deze analogie is bruikbaar zolang validatie en beleidsregels gescheiden blijven.
De grens bestaat uit vier stappen:
- Beperken en analyseren: er moet een getypeerd uitvoercontract worden vereist.
- Normaliseren: datummen, eenheden, identificatoren en de lokale instellingen worden opgelost aan de hand van betrouwbare contextinformatie.
- Autoriseren: bepalen of deze actor toestemming heeft om de operatie aan te vragen.
- Uitvoeren: een aggregatemethode wordt aangeroepen die de invarianten handhaaft.
Pydantic kan een ontbrekend veld of een ongeldige enum weigeren. Het kan niet bepalen of ‘morgen’ overeenkomt met de juiste datum, of dat de gebruiker eigenaar is van de taaklijst, of dat er al een soortgelijke taak openstaat.
Voorbeeld: een taak veilig toevoegen
1. Definieer het voorstel dat gericht is op model
Houd het voorstel zo dicht mogelijk bij wat de model kan afleiden. Vraag het niet om database-ID’s of identificatoren van betrouwbare eigenaren te verzinnen.
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"
Wanneer de gebruiker ‘morgen’ zegt, moet de applicatie aan de model een expliciete lokale datum geven of de relatieve expressie verwerken met een geteste datumparser. Gebruik nooit de klok van de inference-server als impliciete bedrijfscontext.
2. Plaats de invariant in het aggregaat
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
Het voorbeeld laat dit weg. TaskAdded Definitie voor de kortheid: in een compleet domeinmodule zou dit een immutable value object zijn. Het aggregate biedt een tuple-weergave in plaats van zijn mutable dictionary, zodat aanroepers geen elementen kunnen toevoegen. add_task().
De detectie van duplicaten is hier opzettelijk eenvoudig. Echte regels vereisen mogelijk normalisatie die rekening houdt met de lokale context, semantiek rondom recurrencie, of een uniekheidsbeperking in de database als definitieve veiligheidsmaatregel tegen race conditions.
3. Definieer de poort van het repository
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: ...
De infrastructuuradapter kan optimistische concurrentie realiseren met behulp van een versienummerkolom. Het domeincontract specificeert wat belangrijk is, zonder afhankelijk te zijn van SQLAlchemy of een bepaalde database.
4. Coördineren van het gebruikscase
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
Een opmerking over één transactie is essentieel: het opslaan in de repository en het invoegen in de outbox moeten ofwel samen slagen, ofwel samen falen. Een abstractie van een eenheid van werk kan die transactie beheren wanneer de concrete repository en de outbox toegang hebben tot dezelfde database.
De model is afwezig in deze service. Eén adapter kan deze wel verkrijgen. AddTaskProposal Een komt van een LLM, een andere van een HTTP-formulier, en tests kunnen het direct opbouwen. Het bedrijfsbehaviour blijft identiek.
Hernoem hulpmiddelen naar applicatiecommando’s
Agent Tools moeten gebruiksscenario’s blootleggen, en niet de primitieven van een database. Gebruik liever:
add_task(description, due_date, priority)
complete_task(task_id)
reschedule_task(task_id, due_date)
Over:
insert_row(table, values)
update_record(table, id, patch)
De eerste set spreekt de domeinstaal en biedt de applicatie een manier om regels te autoriseren en af te dwingen. De tweede set stelt model in staat om willekeurige mutaties in de persistente gegevens te beschrijven.
Een tool result dient fouten te onderscheiden waarop de agent kan ingrijpen: een ongeldige voorstel, een onbevoegde actor, een domeinconflict, gelijktijdige updates en onbeschikbare infrastructuur. Zorg ervoor dat niet alle fouten worden samengevoegd tot één string, waardoor blinde herproberingen worden uitgelokt.
De grens op verschillende lagen testen
Domeintests
Test aggregaten zonder een model, netwerk of database:
- Dubbele taken worden afgewezen
- Geldige taken triggeren het verwachte evenement
- Exposeerde collecties mogen de interne toestand niet wijzigen
- Overgangsregels blijven van kracht bij herhaalde operaties
Toepassingstests
Gebruik valse repositories en autorisatoren om het laden, de volgorde van autorisatie, het opslaan van de verwachte versie, het gedrag van de uitgaande berichtenlijst en de foutmapping te verifiëren.
Model-evaluatie van contracten
Evalueer de probabilistische adapter apart:
- Precisie van intentie- en veldextractie
- Resolutie van relatieve data met de opgegeven tijdszonecontext
- Weigering of verduidelijking wanneer benodigde informatie ontbreekt
- Bestendigheid tegen prompt injection in geciteerde taakteksten
- Aantal schema-goede, maar semantisch onbruikbare voorstellen
Een end-to-end-test dient vervolgens te bevestigen dat slechte voorstellen nooit de dezelfde domeinmethoden omzeilen die door betrouwbare interfaces worden gebruikt.
Wanneer het ontwerp goed functioneert
Je moet in staat zijn om de model-provider te wijzigen zonder een domeintest aan te passen. Een beleidswijziging dient één geaggregeerd of domeinservice te modificeren in plaats van meerdere prompts. Een trace moet termen gebruiken die door het beheerteam worden herkend. Een ongeldige of niet-geautoriseerde voorstel moet al vóór opslag worden afgewezen, en een gelijktijdige opslag moet falen in plaats van de staat stilletjes te overschrijven.
Dat is de praktische waarde van DDD voor agents. Het maakt een model niet deterministisch. Het zorgt er juist voor dat de grenzen met betrekking tot de autoriteit, het taalgebruik en de consistentie van het systeem voldoende expliciet zijn, zodat een model daartoe niet nodig is.
Referenties
- Eric Evans, Referentieboek Domain-Driven Design — Definities van strategische en tactische patronen Martin Fowler, Begrensd context — waarom één model niet span alle betekenissen van een term mag hebben
- Martin Fowler, Repository — abstractie van persistente collecties Chris Richardson, Transactionele uitgaande map — publiceren na een databasetransactie zonder gebeurtenissen te verliezen Documentatie van Pydantic — validatie van het getypte voorstel