Почему свой агент, а не очередной фреймворк?
К 2026 году агенты стали мейнстримом. LangChain, CrewAI, AutoGPT — фреймворков горы. Но каждый раз, когда я открываю очередную абстракцию, чувствую себя в музее восковых фигур: красиво, но не дышит. За фасадом «простого API» скрываются тонны магии, которую невозможно отладить, когда что-то идёт не так.
Настоящий DevOps не верит в волшебство. Он верит в код, который можно прочитать и изменить. Поэтому я предлагаю собрать ИИ-агента с нуля. Минимум зависимостей, полный контроль, ясная архитектура. Всё, что вам нужно — Python 3.13+ и API-ключ от любой LLM (я возьму OpenAI, но вы легко переключитесь на локальную модель через Ollama).
Эта статья — прямой наследник нашего гайда по созданию Claude Code с нуля. Теперь мы идём ещё глубже: убираем всё лишнее и оставляем ядро — цикл, LLM, контекст и пользовательский ввод.
Архитектура агента за одну минуту
Агент — это просто бесконечный цикл, который:
- Читает пользовательский ввод (или задачу из очереди)
- Отправляет историю диалога в LLM
- Получает ответ (текст или вызов инструмента)
- Выполняет действие, обновляет контекст и повторяет
Всё. Никакой магии, никаких скрытых очередей. Три компонента: мозг (LLM), память (контекст) и руки (инструменты). Как в статье про Agent Skills, но без лишней сложности — мы начинаем с минимального набора.
Шаг 1. Подключаем LLM: без фреймворков, чистыми HTTP
Забудьте про LangChain. Мы будем использовать официальный клиент OpenAI (или любой совместимый). Установка:
pip install openai python-dotenvСоздайте файл .env с вашим ключом. Теперь напишем функцию вызова LLM:
import os
from openai import OpenAI
from typing import List, Dict, Optional, Callable
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
def get_llm_response(
messages: List[Dict],
model: str = "gpt-4o-2026-05-13", # актуальная версия на июнь 2026
tools: Optional[List[Dict]] = None
) -> Dict:
try:
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
temperature=0.7
)
return response.choices[0].message.model_dump()
except Exception as e:
print(f"[ERROR] LLM call failed: {e}")
# возвращаем fallback
return {"role": "assistant", "content": "Произошла ошибка. Попробуйте позже."}Типичная ошибка: использовать openai.ChatCompletion.create — это устаревший API. В версии 1.x клиент изменился, всегда проверяйте документацию.
Можно легко переключиться на локальную модель через Ollama — достаточно изменить base_url клиента и указать модель llama3.2 или mistral. Подробнее о локальных LLM мы писали в гайде по EdTech-агенту на чистом Python и Gemini.
Шаг 2. Главный цикл: while True без затей
Сердце агента — простой цикл. Он запрашивает ввод, вызывает LLM, обрабатывает результат и повторяет. Добавим поддержку завершения по команде exit.
def run_agent(system_prompt: str = "Ты — полезный ассистент."):
messages = [{"role": "system", "content": system_prompt}]
print("ИИ-агент запущен. Введите 'exit' для выхода.")
while True:
user_input = input("\n> ")
if user_input.lower() == "exit":
print("Завершение работы.")
break
messages.append({"role": "user", "content": user_input})
response = get_llm_response(messages)
assistant_msg = response["content"]
print(f"Ассистент: {assistant_msg}")
messages.append({"role": "assistant", "content": assistant_msg})Уже работает. Но контекст будет расти бесконечно — рано или поздно вы упрётесь в токен-лимит и кошелёк. Нужно управлять длиной истории.
Шаг 3. Контекст: не даём агенту «забыть» всё
Секрет хорошего агента — правильное усечение контекста. Мы не можем хранить всю историю, но и терять суть нельзя. Решение — sliding window: храним system prompt и последние K сообщений.
MAX_HISTORY = 10 # сохраняем последние 10 сообщений
def trim_context(messages: List[Dict]) -> List[Dict]:
if len(messages) - 1 <= MAX_HISTORY: # минус system prompt
return messages
# оставляем system + последние MAX_HISTORY сообщений
return [messages[0]] + messages[-MAX_HISTORY:]Обновим цикл: после каждого обмена с LLM отрезаем лишнее.
Но есть нюанс: если агент возвращает вызов инструмента (function calling), мы должны обработать результат и снова передать его модели. Иначе агент «зависнет», не получив ответа инструмента.
Шаг 4. Инструменты: даём агенту руки
Настоящая сила агента — в возможности выполнять действия. Добавим простой инструмент get_current_time, чтобы модель могла узнать текущую дату.
tools = [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "Возвращает текущую дату и время в формате ISO.",
"parameters": {}
}
}
]
def execute_tool(tool_name: str, tool_args: dict) -> str:
if tool_name == "get_current_time":
from datetime import datetime
return datetime.now().isoformat()
return f"Инструмент {tool_name} не найден."Теперь нужно изменить цикл, чтобы после вызова LLM проверять, не хочет ли модель вызвать инструмент. Если да — выполняем, результат передаём как сообщение с ролью tool и повторяем запрос к LLM.
def run_agent_with_tools():
messages = [{"role": "system", "content": "Ты — ассистент. Можешь узнавать время."}]
while True:
user_input = input("\n> ")
if user_input.lower() == "exit":
break
messages.append({"role": "user", "content": user_input})
while True: # цикл инструментов
response = get_llm_response(messages, tools=tools)
if response.get("tool_calls"):
for tc in response["tool_calls"]:
tool_name = tc["function"]["name"]
tool_args = tc["function"]["arguments"]
result = execute_tool(tool_name, tool_args)
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"content": result
})
else:
assistant_msg = response["content"]
print(f"Ассистент: {assistant_msg}")
messages.append({"role": "assistant", "content": assistant_msg})
break
messages = trim_context(messages)⚠️ Важно: никогда не пропускайте проверку tool_calls — иначе агент будет думать, что он вызвал инструмент, а ответа не получит. Типичная ошибка, из-за которой агенты «зацикливаются».
Шаг 5. Обработка ошибок и ограничений
Агент будет падать, если:
- Кончились токены в API (rate limit)
- Инструмент вернул ошибку
- Модель сгенерировала некорректный JSON для вызова функции
Добавим простую retry-логику с экспоненциальной задержкой и обработчиками для инструментов:
import time
from openai import RateLimitError
def safe_llm_call(messages, tools=None, retries=3):
for attempt in range(retries):
try:
return get_llm_response(messages, tools=tools)
except RateLimitError:
wait = 2 ** attempt
print(f"Rate limit, ждём {wait}с...")
time.sleep(wait)
except Exception as e:
print(f"Ошибка LLM: {e}")
if attempt == retries - 1:
return {"role": "assistant", "content": "Извините, ошибка."}Шаг 6. Расширяем: память, RAG, мониторинг
Минимальный агент готов. Дальше можно надстраивать:
- Внешняя память через векторную БД (например, Chroma) для долгосрочного контекста.
- Инструменты для работы с файловой системой, интернетом, базами данных.
- Мониторинг логов и затрат — пригодится для бизнеса. Мы уже описывали кейсы внедрения агентов для бизнеса.
Если вам нужно, чтобы агент выполнял сложные многошаговые сценарии и не забывал инструкции — обязательно прочитайте статью про Agent Skills и динамическую память. Там показано, как избежать «тупения» агента на длинных диалогах.
Пара слов о качестве кода
Когда вы пишете ИИ-агента, особенно для продакшена, не забывайте о тестировании. Агенты с LLM — недетерминированные системы. Мы рекомендуем подход TDD и контроль через «контекстные файлы» вроде AGENTS.md. Это снижает количество сюрпризов.
Если вы хотите углубиться в архитектуру и узнать, как строить production-ready агентов с микросервисной логикой, обратите внимание на курс «Python-разработчик + ИИ» от Skillbox. Там разбирают и продвинутые паттерны, и практику работы с нейросетями.
Неочевидный совет напоследок
Не пытайтесь сделать универсального агента. Начните с минимального цикла — 50 строк кода дадут вам больше контроля, чем самый умный фреймворк. Накидайте пару инструментов конкретно под вашу задачу. Тестируйте на реальных сценариях. А когда почувствуете, что не хватает памяти или маршрутизации — уже будете знать, как это строить.