Сервисы 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, поэтому на всех важных этапах сохраняется ручная проверка и корректировка (человек в цикле). Кроме того, это позволяет уточнять промпты, улучшая взаимодействие с агентом на каждой итерации.

Архитектура 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
Сам сервис – это просто класс с набором методов и инструментов. Мы тестировали три модели взаимодействия:
AI-агент запрашивает текст и сразу добавляет его в базу данных.
AI-агент запрашивает JSON, далее вызывает инструмент валидации и затем возвращает в программу готовый список или словарь. После этого мы отдельным процессом запускаем метод для сохранения данных в базе.
Всё то же, что в пункте 2, но после валидации агент сам размещает информацию в базе данных с помощью инструмента. На этом варианте мы и остановились.
Агенты и сервисы
У нас было несколько агентов:
- Агент, который создает условия заданий. За один проход он готовит сразу несколько заданий, запрашивая JSON у выбранной LLM-модели.
- Агент, который создает тестовый набор данных.
- Агент, который создаёт код решения.
Остановимся на первом агенте, который создает условие задания.
Ниже мы будем использовать модель 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 позволило максимально гибко работать с нашим проектом и использовать знакомую архитектуру и сервисы.
За два дня работы агентов мы выполнили квартальный план по добавлению задач.

