Сервисы AI-агентов на Django

Сервисы AI-агентов на Django

Недавно мы завершили внедрение AI-ботов на базе LangChain в наш Django-проект, используя для этого сервисную архитектуру. Применение LangChain позволило решить сразу несколько задач:

  • Убрали привязку к конкретной LLM. Одни и те же задачи мы запускали на разных моделях и смотрели, какая из них справляется лучше, быстрее и дешевле.

  • Глубоко интегрировали агента с нашей бизнес-логикой.

Иными словами, мы создали внутренние сервисы, которые используют другие компоненты Django и одновременно предоставляют набор инструментов для AI-агента. С их помощью LLM могут взаимодействовать с нашим приложением: работать с базой данных через ORM, запускать Celery-задачи.

Основная задача состояла в том, чтобы добавить практические задания в курсы по Python и SQL. Каждое задание проверяется на наших собственных серверах, и мы хотели передать AI-агентам создание условий и написание тестов для таких учебных задач.

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

Безопасность

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

Поэтому мы определили несколько уровней защиты:

  • Личная учетная запись. У AI-агента есть собственная учётная запись в Django. Агент может управлять только теми записями в базе, которые создал сам. Это легко обеспечить фильтрацией на уровне ORM.

  • Персональный сервис. Набор инструментов, доступных AI-агенту, ограничен и спроектирован специально для него. Хотя внутри мы можем использовать общие сервисы, напрямую агенту доступен только его собственный сервис.

  • Ограничение инструментов. Есть два варианта предоставления инструментов агенту:

    • Первый — сделать максимально широкие и гибкие инструменты с подробным описанием схемы, а затем поставить перед LLM общую задачу, чтобы она сама выбирала нужное действие. Однако даже с подробными описаниями, LLM периодически вызывает не те функции.
    • Второй вариант — выдавать агенту только тот инструментарий, который необходим для решения конкретной задачи. В этом случае вы получаете больше предсказуемости и контроля. Мы сочетаем оба варианта.
  • Идемпотентность и защита от повторов. Инструменты должны корректно отрабатывать при повторных вызовах либо блокироваться в рамках одной сессии агента. Не сомневайтесь: рано или поздно LLM попробует повторно вызвать ваш инструмент. В худшем случае модель войдёт в цикл и начнёт бесконечно создавать объекты в базе данных, попутно расходуя токены.

  • Останавливайте выполнение. В случае, если что-то пошло не так, — логируйте ошибку и останавливайте выполнение программы. Да, вы можете попросить LLM поправить JSON, если он не проходит валидацию с первой попытки, но если в JSON содержатся недопустимые для конкретного инструмента данные — просто завершите работу кода. Проанализируйте логи, поправьте промпты, расширьте сервис (если требуется), а затем запустите агент с того места, где закончили.

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

  • Ручная проверка. На данном этапе мы не можем полностью доверять LLM, поэтому на всех важных этапах сохраняется ручная проверка и корректировка (человек в цикле). Кроме того, это позволяет уточнять промпты, улучшая взаимодействие с агентом на каждой итерации.

Схема взаимодействия сервиса Django AI-агента
Схема взаимодействия AI-агента на Django.

Архитектура AI-сервиса

AI-сервисы — это то место, где находится бизнес-логика ваших агентов:

  • классы и функции для взаимодействия с вашей системой;
  • промпты;
  • схемы;
  • сами агенты.

Сервисы мы обычно размещаем в пакете ai_services внутри Django-приложения (по аналогии с Django-Styleguide).

Базовая структура выглядит так:

app/
    ai_services/
        __init__.py

        # Общая структура
        prompts.py  # классы и функции для генерации промптов
        schemas.py  # схемы данных для Pydantic-валидации
        agents.py   # агенты

        # Сервисы бизнес-логики
        gen_programming_tasks_service.py  # сервис генерации заданий по Python
        gen_sql_tasks_service.py          # сервис генерации заданий по SQL

Сам сервис – это просто класс с набором методов и инструментов. Мы тестировали три модели взаимодействия:

  1. AI-агент запрашивает текст и сразу добавляет его в базу данных.

  2. AI-агент запрашивает JSON, далее вызывает инструмент валидации и затем возвращает в программу готовый список или словарь. После этого мы отдельным процессом запускаем метод для сохранения данных в базе.

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

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

У нас было несколько агентов:

  1. Агент, который создает условия заданий. За один проход он готовит сразу несколько заданий, запрашивая JSON у выбранной LLM-модели.
  2. Агент, который создает тестовый набор данных.
  3. Агент, который создаёт код решения.

Остановимся на первом агенте, который создает условие задания.

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

Модель Task будет отвечать за задание к уроку: задание имеет название, текст, набор Unit-тестов для проверки и код решения.

Все агенты и сервисы будут работать вокруг них.

Схемы

app/ai_services/schemas.py

Как уже упомяналось, от качества схемы и документации зависит то, насколько корректные данные подготовит для вас LLM, — поэтому начинаем с оформления схем.

При работе с вложенными схемами спускаемся сверху вниз: сначала описываем отдельные элементы, а в конце — общую схему, которая содержит списки и словари. Для финальной схемы мы обычно добавляем суффикс Schema (например, TasksSchema) — так проще ориентироваться в коде.

from pydantic import BaseModel, Field

# Схемы для валидации условий задания
class TaskItem(BaseModel):
    name: str = Field(description="Название задания")
    text: str = Field(description="Текст задания в HTML-формате")
    code: str = Field(description="Начальный код задания (если требуется)")

class TasksSchema(BaseModel):
    tasks: list[TaskItem]

Промпты

app/ai_services/prompts.py

После схемы мы пишем промпты — максимально подробные и детальные. От качества промпта зависит вообще всё. Совокупный объём промптов для наших агентов превышает объём кода, который их обслуживает.

from courses.models import Module


class ProgrammingTasksPrompt:
    """
    Генератор промптов для формирования списка заданий по программированию.
    Работает на основе уже опубликованного модуля с уроком.
    """

    def __init__(self, module: Module):
        self.module = module

    def get_prompt(self, tasks_count: int = 5):
        prompt = f"Текст промпта"
        prompt += f"Продолжение промпта"
        return prompt

Инструменты и сервисы

app/ai_services/gen_programming_tasks_services.py

После того как, сформированы промпты и схемы данных, можно создать AI-сервис и инструменты. Все важные моменты мы указали в комментариях к коду:

from langchain.tools import tool

from users.models import User
from courses.models import Module
from practice.models import Task
from practice.ai_services.schemas import TaskItem, TasksSchema


class AIGenProgrammingTaskService:
    """
    Сервис генерации задач по программированию.
    """

    def __init__(self, module: Module):
        self.module = module

    @property
    def tools(self):
        """
        Набор инструментов, который мы предоставляем LLM.
        """
        return [
            self.add_tasks_tool(),
            self.add_task_tests_tool(),
            self.add_task_solution_tool(),
        ]

    def _create_task(self, name: str, text: str, code: str = ""):
        """
        Внутренний метод, который добавляет задания в базу данных.
        """

        Task.objects.create(
            # Бизнес-поля
            name=name,
            text=text,
            code=code,
            active=False,

            # AI-поля
            ai_status="draft",          # Явно устанавливаем статус
            ai_user=User.get_ai_user()  # Указываем владельца, чтобы обозначить область видимости агента
        )

    def add_tasks_tool(self):
        """
        Возвращает инструмент. Название метода оканчивается на _tool для удобства.
        """

        # Обязательно передаём схему данных в args_schema.
        # return_direct=True явно говорит, что после выполнения инструмента не нужно делать повторный запрос в LLM.
        # Без return_direct запросов всегда будет минимум два.

        @tool("add_tasks", args_schema=TasksSchema, description="Добавляет задания по программированию", return_direct=True)
        def add_tasks(tasks: list[TaskItem]):
            for task in tasks:
                self._create_task(name=task.name, text=task.text, code=task.code)

        return add_tasks

Агенты

app/ai_services/agents.py

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

Например, вы можете упаковать ваш сервис внутри агента и далее в представлениях или Celery-задачах использовать только один объект.

Организация самого агента сильно зависит от бизнес-логики. Я приведу лишь один из вариантов:

from pydantic import BaseModel
from typing import Type, Union

# LangChain
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_anthropic import ChatAnthropic
from langchain.agents.structured_output import ProviderStrategy

# Классы сервиса
from practice.ai_services.gen_programming_tasks_services import AIGenProgrammingTaskService
from practice.ai_services.schemas import TasksSchema
from practice.ai_services.prompts import ProgrammingTasksPrompt

# Django-модели
from courses.models import Module


SupportedChats = Union[ChatOpenAI, ChatGoogleGenerativeAI, ChatAnthropic]


class AIAgent:
    """
    Общий класс для формирования AI-агентов.
    """
    def __init__(self, model: str, chat: Type[SupportedChats], tools: list, response_schema: Type[BaseModel]):
        self.model = chat(model=model, temperature=0.5, timeout=60, max_retries=2)
        # Создаем LangChain-агента, который будет делать запросы к API LLM-сервисов.
        self.agent = create_agent(
            model=self.model,
            tools=tools,
            response_format=ProviderStrategy(response_schema)
        )


class GenProgrammingTaskAgent(AIAgent):
    """
    Агент генерации условий для заданий на программирование.
    """

    def __init__(self, model: str, chat: Type[SupportedChats], module: Module, tasks_count: int):
        self.service = AIGenProgrammingTaskService(module=module)
        self.prompt = ProgrammingTasksPrompt(module=module)
        self.tasks_count = tasks_count
        super().__init__(model=model, chat=chat, tools=self.service.tools, response_schema=TasksSchema)

    def run(self):
        # Непосредственно запрос к LLM по API с передачей промпта.
        self.agent.invoke({
            "messages": [{
                "role": "user",
                "content": self.prompt.get_prompt(tasks_count=self.tasks_count)
            }]
        })

Использование агента

Форма

У нас была очень простая задача – генерировать N-заданий с помощью разных моделей.
Количество заданий и модели мы выбираем через форму:

from django import forms

# Мы работаем с LLM через прокси-сервис, и названия моделей, которые используются в нём,
# могут не совпадать с реальными моделями.
LLM_CHOICES = (
    ("OpenAI", (
        ("gpt-5.4-mini", "ChatGPT 5.4 Mini"),
        ("gpt-5.4", "ChatGPT 5.4"),
    )),
    ("Google", (
        ("gemini-3-flash-preview", "Gemini 3 Flash"),
        ("gemini-3.1-pro-preview", "Gemini 3.1 Pro"),
    )),
    ("Anthropic", (
        ("claude-sonnet-4-6", "Claude Sonnet"),
        ("claude-opus-4-7", "Claude Opus"),
    )),
)


class GenTaskForm(forms.Form):
    tasks_count = forms.IntegerField(
        label="Количество задач",
        initial=5,
        min_value=1,
    )

    llm_models = forms.MultipleChoiceField(
        label="LLM-модели",
        choices=LLM_CHOICES,
        widget=forms.CheckboxSelectMultiple,
        initial=["gpt-5.4-mini"],
    )

Представление

Теперь мы можем спокойно создавать агентов и подключать их к нашему сервису:

# Django
from django.http import HttpResponse
from django.template.loader import get_template

# LangChain чат-модели
from langchain_anthropic import ChatAnthropic
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_openai import ChatOpenAI

# Форма, агент и модуль
from ai.forms import GenTaskForm
from practice.ai_services.agents import GenProgrammingTaskAgent
from courses.models import Module


def generate_tasks(request, module_id):
    # Получаем модуль - связующее звено
    module = Module.objects.get(id=module_id)

    # Обрабатываем форму
    gen_task_form = GenTaskForm(request.POST or None)
    if request.method == "POST" and gen_task_form.is_valid():
        llm_models = gen_task_form.cleaned_data["llm_models"]

        # Перебираем модели
        for model in llm_models:
            chat = {
                "gpt": ChatOpenAI,
                "claude": ChatAnthropic,
                "gemini": ChatGoogleGenerativeAI,
            }.get(model[:model.find("-")], ChatOpenAI)

            # Создаем и запускаем агента
            GenProgrammingTaskAgent(
                model=model, 
                chat=chat, 
                module=module, 
                tasks_count=gen_task_form.cleaned_data["tasks_count"]
            ).run()
            
    template = get_template("ai/generate_tasks.html")
    context = {
        "module": module,
        "gen_task_form": gen_task_form
    }

    return HttpResponse(template.render(context, request))

Аналогичный подход используется для агентов по формированию тестов и решений.

Добавление AI-агентов непосредственно внутрь Django позволило максимально гибко работать с нашим проектом и использовать знакомую архитектуру и сервисы.

За два дня работы агентов мы выполнили квартальный план по добавлению задач.

Автор

Никита Шультайс

Никита Шультайс

Профессиональный web-программист с опытом коммерческой разработки более 10 лет. Преподаватель, автор курсов и статей по IT.

  • Fullstack-разработчик на Python/Django
  • Автор курсов по Python, SQL, Алгоритмам.
  • Участник олимпиад по математике и программированию.
  • Научил IT-навыкам более 5000 человек.
  • Победитель конкурса образовательных проектов Edcrunch Award.
  • Автор статей в журнале Linux Format.