[!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:
- Qual é a equipa responsável pela regra?
- Que linguagem são utilizados pelos especialistas no domínio para ela?
- Dentro de que limite esse termo tem um único significado?
- Quais as alterações de estado que devem manter-se consistentes entre si?
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.
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:
- Um contexto pode utilizar várias chamadas a modelos ou agentes especializados internamente
- Um agente que abrange vários contextos necessita de uma tradução explícita e de autorização para cada um deles
- A orquestração é uma questão relacionada com a aplicação; ela não elimina a responsabilidade pelo domínio específico
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:
- Domínio principal: a capacidade que gera valor diferenciador
- Subdomínio de suporte: tarefas necessárias e específicas do negócio que não constituem o fator diferenciador
- Subdomínio genérico: uma capacidade já resolvida, como gestão de identidades ou entrega de e-mails
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.
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:
- uma tarefa concluída não pode ser concluída novamente
- um responsável não pode ter lembretes abertos duplicados para o mesmo dia
- um reembolso não pode exceder o montante remanescente reembolsável
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 fronteira possui quatro etapas:
- Restringir e analisar: exigir um contrato de saída tipado.
- Normalizar: resolver datas, unidades, identificadores e configurações regionais com base em contexto fidedigno.
- Autorizar: determinar se este agente pode solicitar a operação.
- 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:
- As tarefas duplicadas são rejeitadas
- As tarefas válidas disparam o evento esperado
- As coleções expostas não podem modificar o estado interno
- As regras de transição permanecem válidas em operações repetidas
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:
- precisão na extração de intenção e campos
- resolução de datas relativas com o contexto de fuso horário fornecido
- recusa ou solicitação de esclarecimentos quando informações necessárias faltam
- resistência a prompt injection dentro de textos de tarefas entre aspas
- taxa de propostas que cumprem os padrões de schema, mas são semanticamente inviáveis
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
- Eric Evans, Referência em Design Orientado a Domínio — definições de padrões estratégicos e táticos Martin Fowler, Contexto Delimitado — por que um modelo não deve abranger todos os significados de um termo
- Martin Fowler, Repositório — abstração de coleção de persistência Chris Richardson, Caixa de Saída Transacional — publicação após uma transação de base de dados sem perda de eventos Documentação Pydantic — validação de proposta digitada