[!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:

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.

La palabra producto se modela de forma distinta en contextos delimitados.

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:

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:

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.

Gestión de tareas, programación y notificaciones como contextos independientes

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:

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.

La salida del modelo se traduce a un comando de dominio y se verifica mediante reglas deterministas.

El límite consta de cuatro pasos:

  1. Restringir y analizar: se exige un contrato de salida tipado.
  2. Normalizar: resolver fechas, unidades, identificadores y configuración regional mediante un contexto fiable.
  3. Autorizar: determinar si este agente puede solicitar la operación.
  4. 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:

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:

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