[!NOTE] Traducción automática Este artículo se tradujo automáticamente a partir de la versión original en inglés.
Diseño Orientado a Dominios para agentes AI: Contextos delimitados, herramientas y reglas de negocio
Los proyectos de agente se vuelven difíciles de modificar cuando prompts, el código y los procesos empresariales emplean términos diferentes. La normativa exige una “verificación de políticas”, mientras que la implementación revela process_data(). El nombre vago oculta qué regla se está aplicando, quién es su responsable y dónde debe realizarse la modificación.
El Diseño Impulsado por el Dominio (DDD) sitúa ese lenguaje empresarial y la responsabilidad correspondiente en el centro del proceso. En el caso de un agente, el modelo puede interpretar una solicitud y proponer una orden tipada, pero un servicio de aplicación proporciona el contexto fiable necesario, y el modelo del dominio decide si acepta o rechaza el cambio de estado. Esta guía relaciona el vocabulario y los trabajos relacionados con los límites del dominio con dicho camino de ejecución.
TL;DR. Se debe emplear el DDD cuando un agente modifica el estado del negocio en un dominio que cuenta con un lenguaje, una responsabilidad de gestión y reglas bien definidos. Un esquema sirve para validar la estructura de una propuesta de modelo, mientras que el dominio se encarga de garantizar su significado semántico. No se debe equiparar a los agentes con contextos acotados, ni a los JSON generados con una decisión de negocio válida.
El problema real es la propiedad de las reglas
Los sistemas de agentes suelen distribuir una misma regla entre un system prompt, una descripción de herramienta, un manejador API y una restricción en la base de datos. Las copias de dicha regla acaban desviándose. Cuando cambia el límite de reembolso, alguna prompt sigue siendo la versión antigua, y un tool call sintácticamente válido termina aplicándose a la política incorrecta.
DDD comienza planteando diferentes preguntas:
- ¿Qué equipo es responsable de la regla?
- ¿Qué lenguaje utilizan los expertos en el dominio para ella?
- ¿Dentro de qué marco ese término tiene un único significado?
- ¿Qué cambios de estado deben mantenerse coherentes entre sí?
Esas preguntas resultan útiles cuando un flujo de trabajo es lo suficientemente importante como para requerir políticas y un ciclo de vida definido. Un chatbot sencillo de solo lectura podría no necesitar agregados, repositorios ni eventos. Utilice el enfoque DDD para gestionar la complejidad del dominio, y no para recargar cada llamada a LLM.
Diseño estratégico antes del código
Construir un lenguaje ubicuo
Un lenguaje ubicuo es el vocabulario compartido por los expertos en un dominio y los desarrolladores dentro de un contexto delimitado. Si el equipo de soporte técnico dice RefundRequest, approval limit, y settlementDichos términos deben aparecer en los requisitos, el código, los contratos de las herramientas y las evaluaciones.
Se trata de algo más que simplemente elegir nombres descriptivos para los métodos. Los términos requieren definiciones y ejemplos concretos. ¿Significa “aprobado” que un administrador hizo clic en un botón, que el procesador de pagos aceptó la transferencia, o ambas cosas? Descubrir ambigüedades en un glosario es mucho menos costoso que encontrarlas en los registros de actividad de un agente.
Dibujar contextos delimitados alrededor de los modelos y sus responsables de gestión
El mismo sustantivo puede referirse a cosas diferentes en contextos distintos. “Product” puede ser una unidad de control de existencias en Inventario, una línea con precio en Facturación, y un compromiso de entrega en Gestión de Pedidos.
Un contexto delimitado posee su propio modelo y se encarga de la traducción en sus bordes. No es necesariamente un microservicio, un repositorio, un agente ni un equipo, aunque dichos límites suelen coincidir.
Esta distinción es importante en el diseño de agentes:
- Un contexto puede utilizar varias llamadas al modelo o agentes especializados de forma interna.
- Un agente que abarca varios contextos requiere una traducción explícita y autorización para cada uno de ellos.
- La orquestación es una cuestión propia de la aplicación; no elimina la responsabilidad del dominio correspondiente.
Comience con un mapa de contexto antes de dibujar el grafo del agente. De lo contrario, el grafo tiende a reflejar la disponibilidad de herramientas en lugar de los objetivos empresariales.
Clasificar los subdominios
DDD suele separar:
- Dominio principal: la capacidad que genera un valor diferenciador.
- Subdominio de soporte: tareas necesarias específicas del negocio que no constituyen el factor diferenciador.
- Subdominio genérico: una capacidad ya resuelta, como la gestión de identidades o la entrega de correos electrónicos.
En el caso de un asistente de tareas, la gestión de tareas puede ser funcionalidad central, mientras que la planificación y la entrega de notificaciones son componentes genéricos.
La clasificación orienta las inversiones. Esto no implica que cada categoría necesariamente requiera un LLM.
Los patrones tácticos definen el límite de estado
Entidades y objetos de valor
Una entity posee identidad y un ciclo de vida propio. Una tarea sigue siendo la misma tarea independientemente de los cambios en su descripción. Un value object, por su parte, está definido por sus valores y, por lo general, es inmutable: por ejemplo, una dirección de correo electrónico, una cantidad monetaria o un intervalo de tiempo.
Agregados e invariantes
Un agregado es un límite de consistencia. Su raíz expone las operaciones que pueden modificar sus miembros y protege invariantes como:
- Una tarea ya completada no puede volver a completarse.
- Un responsable no puede tener recordatorios abiertos duplicados para el mismo día.
- Un reembolso no puede superar la cantidad restante reembolsable.
Un agregado no se vuelve seguro simplemente porque haya una lista de Python detrás de él. add_task()
Repositorios y servicios de aplicación
Un repositorio carga y guarda agregados sin que los problemas relacionados con la base de datos afecten al dominio. Un servicio de aplicación coordina un caso de uso específico: carga el estado, invoca la operación del dominio, guarda los datos con la versión esperada y publica los eventos resultantes.
El dominio no debe hacer llamadas a un LLM, cliente HTTP ni ORM. Estos componentes constituyen adaptadores que rodean el caso de uso.
Los eventos de dominio son hechos, no un bus de mensajes
TaskAdded Se trata de un hecho en tiempo pasado detectado por el dominio. La aplicación puede conservarlo en una bandeja de salida junto con la actualización global y, posteriormente, publicar un evento de integración tras realizar el commit. Enviarlo directamente a un broker desde una entidad conlleva el riesgo de publicar un evento correspondiente a una transacción que más tarde fallará.
Los eventos pueden coordinar a los agentes, pero por sí solos no garantizan una coordinación fiable. La semántica de entrega, la idempotencia, el ordenamiento y los contratos versionados siguen siendo tareas propias de la infraestructura.
Tratar la salida del modelo como una propuesta no fiable
Una integración con LLM funciona como una capa de anti-corrupción: convierte una representación externa y probabilística en términos que el dominio puede comprender. Esta analogía resulta útil siempre y cuando la validación y las políticas se mantengan separadas.
El límite consta de cuatro pasos:
- Restringir y analizar: se exige un contrato de salida tipado.
- Normalizar: resolver fechas, unidades, identificadores y configuración regional mediante un contexto fiable.
- Autorizar: determinar si este agente puede solicitar la operación.
- Ejecutar: invocar un método agregado que garantice la invariante.
Pydantic puede rechazar un campo faltante o un valor de enum inválido. No está en condiciones de determinar si “mañana” corresponde a la fecha correcta, si el usuario es el propietario de la lista de tareas, o si ya existe una tarea similar abierta.
Ejemplo práctico: agregar una tarea de forma segura
1. Definir la propuesta orientada al modelo
Mantén la propuesta lo más cercana posible a lo que el modelo pueda inferir. No le pidas que invente identificadores de base de datos ni identificadores de propietario de confianza.
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"
Si el usuario indica “mañana”, la aplicación debe proporcionar al modelo una fecha local explícita o resolver la expresión relativa mediante un analizador de fechas validado. Nunca se debe utilizar el reloj del servidor de inferencia como contexto empresarial implícito.
2. Incluir la invariante en el agregado
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
El ejemplo omite los TaskAdded Definición por brevedad. En un módulo de dominio completo, se trataría de un objeto de valor inmutable. El agregado expone una vista en forma de tupla en lugar de su diccionario mutable, por lo que los llamantes no pueden realizar operaciones de inserción adicional. add_task().
La detección de duplicados aquí se ha diseñado intencionadamente de forma sencilla. Las reglas reales podrían requerir una normalización sensible al contexto lingüístico, semántica de recurrencia, o una restricción de unicidad en la base de datos como medida de seguridad adicional para evitar conflictos en entornos concurrentes.
3. Definir el puerto del repositorio
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: ...
El adaptador de infraestructura puede implementar concurrencia optimista mediante una columna de versión. El contrato del dominio define qué elementos son relevantes, sin depender de SQLAlchemy ni de una base de datos concreta.
4. Coordinar el caso de uso
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
El comentario relativo a una transacción es fundamental: el guardado en el repositorio junto con la inserción en la cola de envío deben tener éxito o fallar de forma simultánea. Una abstracción de unidad de trabajo puede gestionar dicha transacción cuando el repositorio concreto y la cola de envío comparten una misma base de datos.
El modelo no está presente en este servicio. Es posible que un adaptador lo obtenga. AddTaskProposal Una proviene de un LLM, otra de un formulario HTTP, y las pruebas pueden generarla directamente. El comportamiento del negocio se mantiene idéntico.
Asignar herramientas de mapeo a comandos de la aplicación
Las herramientas de agente deben exponer casos de uso, y no primitivas de base de datos. Se prefiere:
add_task(description, due_date, priority)
complete_task(task_id)
reschedule_task(task_id, due_date)
sobre:
insert_row(table, values)
update_record(table, id, patch)
El primer conjunto habla el lenguaje del dominio y proporciona a la aplicación un lugar donde autorizar y hacer cumplir las reglas. El segundo permite al modelo describir mutaciones de persistencia arbitrarias.
Un tool result debe permitir distinguir los fallos sobre los cuales el agente puede tomar medidas: propuesta inválida, actor no autorizado, conflicto de dominio, actualización concurrente e infraestructura no disponible. No se deben agrupar todos los fallos en una única cadena de texto que incentive intentos de reintentado ciegos.
Probar los límites en capas
Pruebas de dominio
Pruebe los agregados sin modelo, red ni base de datos:
- Las tareas duplicadas son rechazadas.
- Las tareas válidas generan el evento esperado.
- Las colecciones expuestas no pueden modificar el estado interno.
- Las reglas de transición siguen vigentes en operaciones repetidas.
Pruebas de aplicación
Utilice repositorios y autorizadores ficticios para comprobar la carga, el orden de autorización, los guardados de la versión esperada, el comportamiento del buzón de salida y la asignación de errores.
Evaluaciones de contrato de modelo
Evalúe el adaptador probabilístico por separado:
- precisión en la extracción de intención y campos
- resolución de fechas relativas con el contexto de zona horaria proporcionado
- rechazo o solicitud de aclaración cuando falta información necesaria
- resistencia a prompt injection dentro del texto de la tarea entre comillas
- tasa de propuestas válidas según el esquema pero semanticamente inutilizables
Una prueba de extremo a extremo debe confirmar, por lo tanto, que las propuestas no válidas nunca evadan los mismos métodos del dominio utilizados por las interfaces de confianza.
Cuando el diseño funciona correctamente
Debería ser posible cambiar el proveedor del modelo sin tener que modificar las pruebas de dominio. Cualquier cambio de política debe afectar a un servicio agregado o de dominio único, en lugar de a varios prompts. Un rastro de ejecución debe utilizar términos que el equipo responsable reconozca. Una propuesta mal formada o no autorizada debe fallar antes de que se realice su persistencia, y un intento de guardado simultáneo debe fracasar en lugar de sobrescribir silenciosamente el estado actual.
Ese es el valor práctico del DDD para los agentes. No convierte al modelo en determinista. Hace que los límites de autoridad, lenguaje y consistencia del sistema sean lo suficientemente explícitos como para que el modelo no necesite serlo.
Referencias
- Eric Evans, Referencia sobre Diseño Impulsado por Dominios — definiciones de patrones estratégicos y tácticos Martin Fowler, Contexto Acotado — por qué un único modelo no debe abarcar todos los significados de un término
- Martin Fowler, Repositorio — abstracción de colección de persistencia Chris Richardson, buzón de salida transaccional — publicar tras una transacción de base de datos sin perder eventos Documentación de Pydantic — validación de propuestas escritas