[!NOTE] Автоматический перевод Эта статья была автоматически переведена с оригинальной английской версии.

Проектирование, ориентированное на домен, для ИИ-агенты: ограниченные контексты, инструменты и бизнес-правила

Проекты агентов становятся крайне сложными для изменения, когда промпты, код и бизнес-процессы используют разные термины. В рамках соблюдения стандартов требуется провести «проверку политики», тогда как на практике это приводит к process_data(). Неоднозначное название скрывает информацию о том, какое правило применяется, кто является его владельцем и где следует вносить изменения.

Дизайн, ориентированный на домен (Domain-Driven Design, DDD), ставит именно этот бизнес-язык и ответственность за его использование в центр внимания. Агент может интерпретировать запрос с помощью модель и предложить команду определённого типа, однако сервис приложения обеспечивает надёжный контекст, а сам домен модель принимает или отклоняет изменение состояния. В данном руководстве терминология и работа с границами домена связываются с конкретным путём выполнения операций.

Кратко. Используйте DDD тогда, когда агент изменяет состояние бизнеса в домене, для которого существуют чётко определённые языки описания, правила управления и ответственные стороны. Схема проверяет формат предложения модель; сам домен же определяет его смысл. Не следует отождествлять агентов с ограниченными контекстами, а генерируемые JSON — с действительно корректными бизнес-решениями.


Настоящая проблема — принадлежность правил

В системах агентов часто одно правило реализуется одновременно в системный промпт, описании инструмента, обработчике API и ограничении базы данных. В результате возникает дрейф этих копий: при изменении лимита на возврат средств один промпт остаётся устаревшим, в то время как синтаксически корректный tool call начинает применяться к неправильной политике.

DDD начинается с постановки различных вопросов:

Эти вопросы становятся актуальными тогда, когда рабочий процесс имеет достаточную значимость, чтобы требовать наличия специфических правил и механизмов управления его жизненным циклом. Простой чат-бот с режимом только чтения, как правило, не нуждается в агрегатах данных, хранилищах и событиях. Для управления сложностью доменной логики следует применять подход DDD, а не пытаться использовать его для оформления каждого вызова LLM.

Стратегический проектирование перед написанием кода

Создание универсального языка

Универсальным языком считается словарный запас, общий у экспертов в конкретной области и разработчиков в рамках одного определённого контекста. Если отдел поддержки говорит RefundRequest, approval limit, и settlementЭти термины должны присутствовать в требованиях, коде, спецификациях инструментов и результатах оценки.

Речь идет не просто о выборе описательных имен методов. Для каждого термина необходимы четкие определения и примеры использования. Означает ли статус «одобрено», что менеджер нажал соответствующую кнопку, обработчик платежей принял транзакцию, или же произошло и то, и другое? Обнаружение неоднозначности в глоссарии стоит дешевле, чем ее обнаружение в самом агенте трейс.

Определите ограниченные контексты вокруг модели и модели собственности

То же существительное может обозначать разные вещи в зависимости от контекста. «Product» может быть единицей учёта запасов в системе Inventory, позицией с ценой в модуле Billing или обязательством по поставке в системе Order Management.

Термин «product», моделируемый по-разному в различных ограниченных контекстах

Ограниченный контекст владеет своим модель и осуществляет его транслейцию на своей границе. Он автоматически не является микросервисом, репозиторием, агентом или командой, хотя эти границы зачастую совпадают.

Это различие имеет важное значение при проектировании агентов:

Начните с карты контекста перед построением графа агентов. В противном случае граф будет отражать доступность инструментов, а не бизнес-логику.

Классификация поддоменов

DDD обычно разделяет на:

Для вспомогательного агента по задачам управление задачами может быть ядром функционала, планирование — вспомогательным компонентом, а доставка уведомлений — универсальной частью интерфейса.

Управление задачами, планирование и уведомления как отдельные контексты

Классификация направляет процесс инвестирования. Однако это не означает, что для каждой категории обязательно требуется LLM.


Тактические шаблоны определяют границы состояния

Энтитеты и объекты значений

Энтитет обладает идентичностью и жизненным циклом. Задача остается той же самой задачей даже при изменении её описания. Объект с значением определяется своими значениями и, как правило, является неизменяемым: речь идёт о адресе электронной почты, сумме денег или временном интервале.

Агрегаты и инварианты

Агрегат представляет собой границу консистентности. Его корневой узел обеспечивает доступ к операциям, способным изменять элементы агрегата, и защищает такие инварианты, как:

Агрегат не становится безопасным лишь потому, что за ним находится список в Python. add_task() Метод: внешний код не должен получать изменяемый ссылатель, позволяющий обойти данный метод. Для обеспечения сохранности данных также требуется контроль конкурентности, иначе два действительных запроса могут нарушить неизменяемость при одновременной записи.

Репозитории и сервисы приложений

Репозиторий загружает и сохраняет агрегаты, не позволяя проблемам базы данных влиять на домен. Служба приложения координирует один сценарий использования: загрузку состояния, вызов операции домена, сохранение с указанием ожидаемой версии и публикацию генерируемых событий.

Домен не должен обращаться к LLM, HTTP-клиенту или ORM. Это адаптеры, предназначенные для обслуживания конкретных сценариев использования.

События домена представляют собой факты, а не шину сообщений

TaskAdded Это факт прошедшего времени, выявленный доменом. Приложение может сохранить его в очереди отправки вместе с агрегированными обновлениями, а затем опубликовать событие интеграции после выполнения коммита. Отправка данных непосредственно в брокер с помощью отдельной entности создаёт риск публикации события для транзакции, которая впоследствии завершится неудачей.

События могут координировать агентов, однако сами по себе они не обеспечивают надёжности этой координации. Семантика доставки, идемпотентность, порядок выполнения операций и версионированные контракты продолжают оставаться частью инфраструктурных решений.


Рассматривайте вывод модель как ненадёжное предложение

Интеграция с LLM функционирует аналогично слою противодействия коррупции: она преобразует внешнее, вероятностное представление в формат, понятный конкретной области применения. Такая аналогия остаётся уместной при условии, что процессы валидации и формирования правил продолжают выполняться отдельно друг от друга.

результат выполнения доменной команды Модель, обработанный с использованием детерминистических правил

Граница состоит из четырёх этапов:

  1. Ограничение и парсинг: необходимо наличие контракта типизированного вывода.
  2. Нормализация: приведение дат, единиц измерения, идентификаторов и локали с использованием надёжного контекста.
  3. Авторизация: определение того, может ли данный участник запросить выполнение операции.
  4. Выполнение: вызов агрегатного метода, обеспечивающего соблюдение инвариантов.

Pydantic может отклонить запрос при отсутствии поля или некорректном значении из перечисления. Однако он не способен самостоятельно определить, что значение “tomorrow” соответствует правильной дате, что пользователь имеет право на доступ к списку задач, или что аналогичная задача уже находится в активном состоянии.

Пример реализации: безопасное добавление задачи

1. Определите предложение, ориентированное на модель

Сохраняйте предложение максимально близким к тому, что может вывести модель. Не требуйте от него генерации идентификаторов баз данных или идентификаторов авторизованных владельцев.

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"

Если пользователь указывает «завтра», приложение должно передать модель конкретную локальную дату или преобразовать это относительное выражение с помощью проверенного парсера дат. Ни в коем случае нельзя использовать часы инференс-сервер в качестве имплицитного контекста бизнес-логики.

2. Включение инварианта в агрегат

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

В примере отсутствует TaskAdded Определение в краткой форме: в полноценном модуле домена это неизменяемый объект значений. Агрегат предоставляет доступ к данным в виде кортежа, а не к изменяемой словарной структуре, поэтому вызывающие код не могут вносить изменения в его содержимое путём добавления элементов. add_task().

Распознавание дубликатов здесь сознательно реализовано в упрощённой форме. В реальных сценариях могут потребоваться нормализация с учётом локали, анализ рекуррентных закономерностей или ограничение уникальности в базе данных в качестве финального защитного механизма, обеспечивающего надёжность работы в условиях высокой конкуренции.

3. Определение порта хранилища

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

Инфраструктурный адаптер может реализовывать оптимистичную конкурентность с использованием столбца версии. Доменный контракт определяет критически важные аспекты без зависимости от SQLAlchemy или конкретной базы данных.

4. Согласование сценария использования

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

Комментарий относительно одной транзакции имеет решающее значение: операции сохранения в хранилище и вставки в очередь отправки должны либо успешно завершиться совместно, либо провалиться одновременно. Абстракция единицы работы может управлять этой транзакцией в том случае, когда конкретное хранилище и очередь отправки используют одну и ту же базу данных.

В данном сервисе отсутствует модель. Один адаптер может его получить. AddTaskProposal Одни данные поступают из LLM, другие — из HTTP-формы, причём тесты могут формировать этот объект непосредственно. Бизнес-логика при этом остаётся неизменной.


Сопоставление инструментов карт с командами приложения

Инструменты агента должны предоставлять возможности применения, а не базовые операции с базой данных. Рекомендуется использовать:

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)

Первый набор обеспечивает работу с языком домена и предоставляет приложению механизм для авторизации и реализации правил. Второй набор позволяет модель описывать произвольные изменения состояния данных в памяти.

Результат работы инструмента должен выделять те сбои, по которым агент может принять меры: недействительное предложение, неуполномоченный исполнитель, конфликт доменов, одновременная обработка данных и недоступная инфраструктура. Не следует сводить все сбои к одной строке, что приводит к слепым попыткам повторного выполнения операций.

Проверка граничных условий по слоям

Тестирование доменов

Проведите тестирование агрегатов без модель, сети и базы данных:

Тестирование приложений

Для проверки процесса загрузки, порядка авторизации, сохранения ожидаемой версии, поведения очереди отправки сообщений и сопоставления ошибок используются фиктивные репозитории и механизмы авторизации.

Модель — оценка контрактов

Оцените вероятностный адаптер отдельно:

Тест от начала до конца должен затем подтвердить, что некорректные предложения никогда не обходят методы того же домена, которые используются доверенными интерфейсами.

Когда дизайн функционирует корректно

Вы должны иметь возможность заменять поставщика модель без необходимости изменения тестов для конкретного домена. Любое изменение политики должно касаться либо одного агрегатного сервиса, либо одного доменного сервиса, а не нескольких промпты. В документации трейс следует использовать термины, понятные команде, ответственной за проект. Некорректные или несанкционированные предложения должны отклоняться ещё на этапе внесения изменений, а одновременные операции сохранения данных должны приводить к ошибке, вместо того чтобы молча перезаписывать текущее состояние системы.

Именно в этом и заключается практическая ценность подхода DDD для агентов. Он не превращает модель в детерминированный элемент. Вместо этого он делает границы авторитета системы, её языка и согласованности настолько очевидными, что модель становится избыточным.

Список литературы