Django часто живёт там, где уже есть зрелый монолит: пользователи, админка, права, ORM, Celery, платежи, письма, legacy и бизнес, который не хочет переписывать систему ради модного AI-сервиса. И правильно не хочет. Переписывать рабочий монолит на микросервисы только потому, что появилась LLM-фича, — слабая инженерная воля, замаскированная под архитектурную смелость.
Где Django хорош для LLM
Django удобен, когда LLM-фича должна встроиться в существующие бизнес-процессы:
- админка оператора;
- обработка заявок;
- внутренние copilot-функции;
- модерация;
- генерация черновиков писем;
- извлечение данных из загруженных файлов;
- back-office workflows.
У Django уже есть то, что часто приходится собирать вокруг FastAPI-сервиса отдельно: auth, admin, ORM, migrations, permissions, forms, management commands.
Рекомендуемая структура приложения
project/
ai/
models.py
services/
llm_client.py
prompts.py
support.py
documents.py
tasks.py
admin.py
views.py
schemas.py
evals/Не кладите всё в models.py или views.py. Django это позволяет. Именно поэтому надо держать себя за руку.
Pydantic внутри Django
Django models хороши для хранения. Pydantic хорош для контрактов LLM-ответа. Их не надо противопоставлять.
from pydantic import BaseModel, Field
class TicketTriageResult(BaseModel):
category: str
confidence: float = Field(ge=0, le=1)
needs_human: bool
operator_note: str = Field(max_length=500)Django model может хранить результат:
class AITriage(models.Model):
ticket = models.ForeignKey("support.Ticket", on_delete=models.CASCADE)
prompt_version = models.CharField(max_length=100)
model_name = models.CharField(max_length=100)
result = models.JSONField()
input_tokens = models.PositiveIntegerField(null=True, blank=True)
output_tokens = models.PositiveIntegerField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)Pydantic валидирует форму ответа. Django хранит и связывает с доменной моделью. Спорить, кто из них «главнее», бессмысленно. Это разные инструменты.
Celery в Django
Для LLM-фич в Django почти всегда нужен Celery. Не потому, что модно. Потому что LLM-вызовы медленные, внешние и периодически падают.
@shared_task(bind=True, max_retries=2)
def triage_ticket(self, ticket_id: int) -> None:
ticket = Ticket.objects.get(id=ticket_id)
service = SupportAIService()
try:
result = service.triage(ticket.message)
except TransientLLMError as exc:
raise self.retry(exc=exc, countdown=20)
AITriage.objects.create(
ticket=ticket,
prompt_version=result.prompt_version,
model_name=result.model,
result=result.data.model_dump(),
input_tokens=result.input_tokens,
output_tokens=result.output_tokens,
)Ошибка команды
Команда добавляет кнопку «Сгенерировать ответ» прямо во view Django admin. View вызывает LLM синхронно. На тесте работает. В проде оператор нажимает кнопку, запрос висит, nginx режет соединение, оператор нажимает ещё раз, создаются два разных черновика, один из них уходит клиенту после ручного копирования. Потом никто не может восстановить, какой prompt и какой вход использовались.
Правильное решение:
- кнопка создаёт
AIJob; - Celery выполняет задачу;
- результат сохраняется;
- админка показывает статус;
- оператор видит diff/черновик;
- отправка письма остаётся человеческим действием;
- все LLM-вызовы логируются.
Правило главы
В Django LLM-интеграция должна уважать монолит, а не пытаться его унизить. Используйте ORM, admin, permissions и Celery. Но держите LLM-контракты и сервисный слой отдельно, иначе AI-код превратит зрелый проект в кашу из view, signals и надежды.
Чеклист главы
Практикум
Задача 30.1 — AI-триаж в существующем Django-проекте. ⭑⭑
Возьмите любой свой Django-проект (или стандартный туториал-проект с моделью Ticket). Встройте триаж тикетов по схеме главы: app ai, сервисный слой, Pydantic-контракт, Celery-задача, модель AITriage для результатов.
Критерии приёмки:
Подсказки:
- Не заводите отдельный FastAPI-сервис «для AI» — вся ценность задачи в том, чтобы встроиться в монолит аккуратно.
Задача 30.2 — кнопка «Сгенерировать ответ» без инцидента. ⭑⭑⭑
Реализуйте сценарий из «ошибки команды» правильно: в админке тикета кнопка «Сгенерировать черновик ответа». Кнопка создаёт AIJob, Celery выполняет генерацию, страница показывает статус, оператор видит черновик и вручную решает, отправлять ли.
Критерии приёмки:
Подсказки:
- Уникальный constraint
(ticket_id, job_type, status="pending")через partial index решает дубли надёжнее любого JS. - Это учебная версия паттерна human-in-the-loop из главы 35 — здесь достаточно ручной отправки и лога.