[!NOTE] Tradução automática Este artigo foi traduzido automaticamente a partir da versão original em inglês.

Design Orientado a Domínio para Agentes AI: Contextos Delimitados, Ferramentas e Regras de Negócio

Os projetos de agente tornam-se difíceis de modificar quando prompts, o código e os processos de negócio utilizam termos diferentes. A conformidade exige uma “verificação de política”, enquanto a implementação revela process_data(). O nome vago oculta qual regra está a ser aplicada, quem é o seu responsável e onde deve ser efetuada uma alteração.

O Domain-Driven Design (DDD) coloca essa linguagem de negócio e a responsabilidade associada no centro do processo. No caso de um agente, o modelo pode interpretar um pedido e propor um comando tipado, mas um serviço de aplicação fornece o contexto confiável, sendo que o modelo de domínio aceita ou rejeita a alteração de estado. Este guia relaciona o vocabulário e os trabalhos de delimitação com esse caminho de execução.

TL;DR. Utilize o DDD quando um agente altera o estado de negócio num domínio que dispõe de linguagem, responsabilidades e regras bem definidas. Um esquema valida a estrutura de uma proposta de modelo; o domínio, por sua vez, garante a sua correta interpretação. Não confunda agentes com contextos delimitados, nem JSON gerados com decisões de negócio válidas.


O verdadeiro problema é a propriedade das regras

Os sistemas de agentes distribuem frequentemente uma única regra por um system prompt, uma descrição de ferramenta, um manipulador API e uma restrição de base de dados. As cópias dessas regras acabam por divergir. Quando o limite de reembolso é alterado, um prompt continua a utilizar valores obsoletos, e um tool call sintaticamente válido acaba a ser aplicado a uma política incorreta.

O DDD começa por fazer perguntas diferentes:

Essas questões são úteis quando um fluxo de trabalho é suficientemente importante para exigir políticas e um ciclo de vida definido. Um chatbot simples, de leitura apenas, pode não precisar de agregados, repositórios nem eventos. Utilize o DDD para gerir a complexidade do domínio, e não para decorar cada chamada a LLM.

Design estratégico antes do código

Construir uma linguagem onipresente

Uma linguagem onipresente é o vocabulário partilhado por especialistas em determinada área e desenvolvedores dentro de um contexto delimitado. Se as equipas de suporte disserem RefundRequest, approval limit, e settlementEsses termos devem ser incluídos nos requisitos, no código, nos contratos de ferramentas e nas avaliações.

Isto vai além da simples escolha de nomes descritivos para métodos. Os termos exigem definições e exemplos concretos. Será que “aprovado” significa que um gestor clicou num botão, que o processador de pagamentos aceitou a transferência, ou ambos? Descobrir ambiguidades num glossário é mais barato do que encontrá-las no rasto de atividades de um agente.

Defina contextos delimitados em torno dos modelos e das suas responsabilidades de propriedade

O mesmo substantivo pode significar coisas diferentes em contextos distintos. “Produto” pode ser uma unidade de controle de estoque em Gestão de Inventário, uma linha com preço definido em Faturação e um compromisso de entrega em Gestão de Pedidos.

A palavra “produto” é modelada de forma diferente em contextos delimitados.

Um contexto delimitado gere o seu próprio modelo e realiza a tradução nas suas fronteiras. Não se trata automaticamente de um microsserviço, repositório, agente ou equipa, embora essas fronteiras muitas vezes coincidam.

Esta distinção é importante no projeto de agentes:

Comece com um mapa de contexto antes de desenhar um grafo de agente. Caso contrário, o grafo tende a refletir a disponibilidade das ferramentas em vez dos objetivos de negócio.

Classificar os subdomínios

DDD separa normalmente:

Num assistente de tarefas, a gestão de tarefas pode ser o elemento central, enquanto o agendamento e a entrega de notificações são funcionalidades genéricas.

Gestão de tarefas, agendamento e notificações como contextos distintos

A classificação orienta os investimentos. Isso não significa que cada categoria precise obrigatoriamente de um LLM.


Os padrões táticos definem os limites de estado

Entidades e objetos de valor

Uma entidade possui identidade e um ciclo de vida. Uma tarefa continua a ser a mesma tarefa mesmo após a alteração da sua descrição. Um objecto de valor é definido pelos seus valores e costuma ser imutável: um endereço de e-mail, um montante monetário ou um intervalo de tempo.

Agregados e invariantes

Um aggregate é uma fronteira de consistência. A sua raiz expõe as operações que podem alterar os membros e protege invariantes como:

Um agregado não se torna seguro apenas porque existe uma lista em Python por trás dele. add_task() Método; o código externo não deve receber uma referência mutável que contorne esse método. A persistência também requer controle de concorrência, caso contrário, duas solicitações válidas podem violar uma invariante quando salvas simultaneamente.

Repositórios e serviços de aplicação

Um repositório carrega e guarda agregados sem que as questões relacionadas com a base de dados se propaguem para o domínio. Um serviço de aplicação coordena um caso de uso: carregar o estado, invocar a operação do domínio, guardar os dados com a versão esperada e publicar os eventos resultantes.

O domínio não deve chamar um LLM, cliente HTTP ou ORM. Trata‑se de adaptadores que rodeiam o caso de uso.

Os eventos de domínio são factos, e não um barramento de mensagens

TaskAdded Trata‑se de um facto no passado, indicado pelo domínio. A aplicação pode armazená‑lo numa caixa de saída juntamente com a atualização agregada e, em seguida, publicar um evento de integração após o commit. Enviar diretamente para um broker a partir de uma entidade corre o risco de publicar um evento relativo a uma transação que posteriormente falhará.

Os eventos podem coordenar agentes, mas por si só não tornam a coordenação fiável. As semânticas de entrega, a idempotência, a ordenação e os contratos versionados continuam a ser tarefas de infraestrutura.


Trate a saída do modelo como uma proposta não confiável

Uma integração com LLM assemelha‑se a uma camada de anti‑corrupção: ela traduz uma representação externa e probabilística para termos que o domínio consegue compreender. Esta analogia permanece útil desde que a validação e as políticas se mantenham separadas.

A saída do modelo é traduzida para um comando de domínio e verificada mediante regras determinísticas.

A fronteira possui quatro etapas:

  1. Restringir e analisar: exigir um contrato de saída tipado.
  2. Normalizar: resolver datas, unidades, identificadores e configurações regionais com base em contexto fidedigno.
  3. Autorizar: determinar se este agente pode solicitar a operação.
  4. Executar: invocar um método agregado que garanta a invariante.

O Pydantic pode rejeitar um campo em falta ou um valor inválido no enum. Não consegue determinar se “amanhã” corresponde à data correta, se o utilizador é o proprietário da lista de tarefas ou se já existe uma tarefa semelhante aberta.

Exemplo prático: adicionar uma tarefa de forma segura

1. Defina a proposta voltada para o modelo

Mantenha a proposta o mais próxima possível do que o modelo consegue inferir. Não peça que ele crie identificadores de base de dados ou identificadores de proprietário confiável.

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"

Se o utilizador indicar “amanhã”, a aplicação deve fornecer ao modelo uma data local explícita ou resolver a expressão relativa com um analisador de datas testado. Nunca se deve utilizar o relógio do servidor de inferência como contexto de negócio implícito.

2. Incorporar a invariante no 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

O exemplo omite o TaskAdded Definição de forma concisa. Num módulo de domínio completo, trata‑se de um objeto de valor imutável. O agregado expõe uma visão em forma de tupla em vez do seu dicionário mutável, pelo que os utilizadores não podem adicionar elementos posteriormente. add_task().

A deteção de duplicados aqui é deliberadamente simples. As regras reais podem exigir normalização adaptada ao contexto local, semântica de recorrência ou uma restrição de unicidade na base de dados como medida de segurança final contra concorrências.

3. Defina a porta do repositório

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: ...

O adaptador de infraestrutura pode implementar concorrência otimista através de uma coluna de versão. O contrato de domínio define os requisitos essenciais, sem depender do SQLAlchemy ou de um banco de dados específico.

4. Coordenar o 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

O comentário relativo a uma transação é essencial: o salvamento no repositório e a inserção na fila de saída devem ter sucesso ou falhar em conjunto. Uma abstração de unidade de trabalho pode gerir essa transação quando o repositório concreto e a fila de saída partilham a mesma base de dados.

O modelo está ausente neste serviço. Um adaptador pode obtê-lo. AddTaskProposal Um é proveniente de um LLM, outro de um formulário HTTP, e os testes podem criá‑lo diretamente. O comportamento de negócio permanece idêntico.


Mapear ferramentas de mapeamento para comandos da aplicação

As ferramentas de agente devem expor casos de uso, e não primitivas de base de dados. Prefira:

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)

O primeiro conjunto fala a linguagem do domínio e fornece à aplicação um local para autorizar e aplicar regras. O segundo permite que o modelo descreva mutações de persistência arbitrárias.

Um tool result deve permitir a distinção entre falhas sobre as quais o agente pode intervir: proposta inválida, ator não autorizado, conflito de domínio, atualização concorrente e infraestrutura indisponível. Não se deve consolidar todas as falhas num único valor de string, o que poderia levar a tentativas de recuperação cegas.

Testar os limites em camadas

Testes de domínio

Teste agregados sem modelo, rede ou base de dados:

Testes de aplicação

Utilize repositórios e autorizadores fictícios para validar o processo de carregamento, a ordem de autorização, a gravação da versão esperada, o comportamento da fila de saída e a mapeação de erros.

Avaliações de contrato de modelo

Avalie o adaptador probabilístico separadamente:

Um teste end-to-end deve, então, confirmar que as propostas inválidas nunca contornam os mesmos métodos de domínio utilizados pelas interfaces confiáveis.

Quando o design está a funcionar

Deve ser possível alterar o fornecedor do modelo sem ter de modificar um teste de domínio. Uma alteração de política deve afetar um serviço agregado ou de domínio inteiro, e não vários prompts. Um registo de rastreio deve utilizar termos reconhecidos pela equipa responsável. Uma proposta mal formatada ou não autorizada deve ser rejeitada antes mesmo de ser persistida, e uma operação de salvamento simultânea deve falhar em vez de sobrescrever silenciosamente o estado atual.

Esse é o valor prático do DDD para agentes. Ele não torna um modelo determinístico. Em vez disso, torna as fronteiras de autoridade, linguagem e consistência do sistema suficientemente explícitas, de modo que o modelo não precise sê-lo.

Referências