English translation is not available yet. Showing Russian content.

Реализовать Agent Loop с нуля

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать Agent Loop с нуля

1. Цель задачи

Научиться проектировать и реализовывать базовый цикл работы агента (цикл агента|цикл агента|Agent Loop) без использования высокоуровневых фреймворков (LangChain, CrewAI). Вы напишете на Python управляющий цикл, который последовательно вызывает LLM, парсит ответ на наличие tool calls, исполняет выбранные инструменты и записывает результат в память (историю диалога). Ключевой результат Работающий агент, способный выполнить как минимум 3 шага (call|вызов LLM → tool → вывод) исключительно на вашем коде.

2. Исходные данные

Перед началом необходимо иметь:

Что нужноОткуда взять
Python 3.10+ и опыт работы с нимУстановить / проверить
Базовый LLM API (OpenAI / Anthropic / Ollama)Ключ API или локальная модель (через OpenAI-совместимый endpoint)
Описание инструментов (tools) в формате JSON SchemaРазработать самостоятельно (например: get_weather, calculator, search_wiki)
Системный промпт для агентаНаписать, объяснить агенту, как и когда вызывать инструменты
Среда для тестирования (jupyter / скрипт)Любая

Если нет доступа к платному LLM API — симулируем:

  1. Используйте локальную модель через llama-cpp-python или ollama (например, llama3.2:1b).
  2. Для теста можно подменить LLM вызов заглушкой: функция, которая всегда возвращает предопределённый JSON с tool call.
  3. В production-ready решении заглушка заменяется на реальный вызов LLM.

3. Технологический стек

КомпонентИнструментыНазначение
Язык программированияPython 3.10+Реализация цикла
LLM APIOpenAI / Anthropic / OllamaГенерация ответов и tool calls
Формат сообщенийOpenAI ChatCompletions API (messages)Единый интерфейс
Парсинг ответаjson.loads / pydanticИзвлечение tool_call из ответа LLM
Выполнение инструментовPython functions + inspect.signatureДинамический вызов с валидацией аргументов
Память (история)list[dict] in-memoryХранение сообщений
Логированиеlogging / richОтладка цикла
ТестированиеpytestUnit-тесты для каждого компонента

4. Этапы выполнения

Этап 1: Проектирование структуры агента (30 мин)

Действия

  1. Спроектируйте архитектуру цикла:
    • Главная функция agent_loop(user_input, tools, system_prompt, max_steps=5).
    • Внутри неё инициализируйте историю сообщений messages.
    • Цикл while step < max_steps:
      1. Получить ответ от LLM.
      2. Если ответ содержит tool_calls — извлечь, выполнить, добавить результаты в messages.
      3. Иначе — вернуть ответ пользователю и завершить.
  2. Определите интерфейс инструмента:
    class Tool:
        name: str
        description: str
        parameters: dict  # JSON schema
        func: callable
    
  3. Создайте 2-3 простых инструмента для демонстрации (например, get_current_time без аргументов, multiply(a,b)).
  4. Напишите системный промпт, который инструктирует LLM отвечать в формате:
    • Если нужно вызвать инструмент — вернуть JSON с {"tool": "имя", "args": {...}}.
    • Иначе — обычный текст.

Ожидаемый результат этапа Чёткая схема цикла, определённые классы и инструменты.

Этап 2: Реализация вызова LLM и парсинга ответа (1 час)

Действия

  1. Напишите функцию call_llm(messages: list) -> dict, которая отправляет запрос к API и возвращает ответ (содержимое choices[0].message).
  2. Реализуйте парсинг:
    • Если в ответе есть поле function_call (OpenAI) или tool_calls — извлеките имя и аргументы.
    • Для моделей без нативной поддержки tool calls — распарсите JSON из текста с помощью регулярного выражения или инструкции в промпте.
  3. Обработайте крайние случаи:
    • LLM вернул невалидный JSON → повторный запрос с сообщением об ошибке.
    • LLM вызвал несуществующий инструмент → уведомить и пропустить.
  4. Протестируйте с простым запросом: "Сколько будет 3 умножить на 4?"

Ожидаемый результат этапа Функция parse_tool_call корректно извлекает вызов инструмента.

Этап 3: Выполнение инструментов и обработка результатов (1 час)

Действия

  1. Напишите функцию execute_tool(tool_name, arguments, tools) -> str:
    • Найдите инструмент по имени.
    • Проверьте аргументы согласно JSON schema (можно использовать pydantic или просто isinstance).
    • Вызовите tool.func(**arguments) и верните результат (преобразованный в строку).
  2. Обработайте исключения времени выполнения (деление на ноль, таймаут) — верните сообщение об ошибке.
  3. Добавьте результат вызова инструмента в messages как сообщение с ролью "tool":
    messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
    
    (если используете OpenAI-формат) или просто {"role": "tool", "name": tool_name, "content": result}.
  4. Убедитесь, что после каждого tool call цикл снова вызывает LLM, чтобы агент мог продолжить.

Ожидаемый результат этапа Агент может выполнить цепочку «LLM → tool → LLM → tool → …».

Этап 4: Интеграция памяти и финальный цикл (1 час)

Действия

  1. Реализуйте управление историей:
    • При старте добавьте системный промпт (роль "system").
    • После каждого вызова LLM добавляйте его ответ (роль "assistant").
    • После выполнения tool добавляйте результат (роль "tool").
  2. Напишите основной цикл с условием выхода:
    • Если в ответе нет tool call → сформировать финальный ответ пользователю.
    • Если превышен max_steps → вернуть сообщение о лимите.
  3. Протестируйте сценарий из нескольких шагов, например:
    • "Узнай текущее время, затем умножь его на 2 (часы) и скажи результат".
  4. Добавьте логирование каждого шага с помощью logging (INFO-level) для отладки.

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

Этап 5: Тестирование и документирование (30 мин)

Действия

  1. Напишите три unit-теста с помощью pytest:
    • Тест парсинга tool call из корректного JSON.
    • Тест выполнения инструмента с валидными аргументами.
    • Тест цикла с заглушкой LLM (фиксированный ответ с tool call, затем текст).
  2. Проверьте, что агент корректно обрабатывает ошибки (невалидный JSON, неизвестный инструмент).
  3. Напишите краткую документацию (README), описывающую как запустить цикл и добавить новый инструмент.

Ожидаемый результат этапа Код покрыт тестами, документирован, готов к демонстрации.

5. Критерии приемки (Definition of Done)

  • Цикл полностью написан без использования LangChain / CrewAI / других agent-фреймворков.
  • Агент успешно выполняет последовательность из 3 шагов (например, «вызвать tool → получить результат → вызвать другой tool → ответить»).
  • Реализовано не менее 2 различных инструментов (с разными сигнатурами).
  • Обработаны случаи: невалидный JSON от LLM, неизвестный инструмент, ошибка выполнения инструмента.
  • История сообщений правильно аккумулируется и передаётся в каждый последующий вызов LLM.
  • Цикл имеет настраиваемый лимит шагов (max_steps) и корректно завершается при его превышении.
  • Код покрыт минимум 3 unit-тестами, все проходят.
  • Проект опубликован в репозитории (GitHub) или сдан в виде архива с README.

6. Ожидаемый результат

Основной артефакт

  • Python-файл agent_loop.py (или модуль), содержащий:
    • Определение класса Tool.
    • Функции call_llm (реальная или заглушка), parse_tool_call, execute_tool, agent_loop.
    • Реализацию двух демонстрационных инструментов.
  • Файл test_agent.py с тестами.
  • Файл README.md с инструкцией по запуску и примером использования.

Опционально

  • Поддержка стриминга ответов LLM.
  • Логирование через rich для наглядного вывода.
  • Возможность загрузки инструментов из внешнего конфига (YAML/JSON).

7. Возможные сложности и их решение

СложностьРешение
LLM не всегда возвращает tool call в нужном форматеИспользовать structured output (OpenAI response_format / json_mode) или итеративные исправления: при ошибке парсинга отправить LLM сообщение: «Твой ответ не является валидным JSON. Повтори попытку».
Аргументы инструмента не проходят валидациюИспользовать pydantic.BaseModel для параметров и генерировать JSON schema автоматически.
Бесконечный цикл из-за повторяющихся tool callsВнедрить детектор повторений (если последние 3 вызова одинаковы — прервать).
Ограничение контекста (слишком длинная история)Реализовать суммирование (summarization) старых сообщений или обрезать историю до последних N токенов.
Зависимость от внешнего API (таймауты, сбои)Добавить retry с exponential backoff и фолбэк на заглушку в тестах.

8. Бюджет времени (оценка)

ЭтапВремя
Этап 1: Проектирование30 минут
Этап 2: LLM вызов + парсинг1 час
Этап 3: Выполнение инструментов1 час
Этап 4: Интеграция памяти + цикл1 час
Этап 5: Тестирование + документация30 минут
Итого4 часа

Примечание Для первого выполнения задачи может потребоваться до 6 часов из-за отладки парсинга и edge cases.

9. Связанные вопросы из базы знаний

ВопросТема
12Какие паттерны проектирования используются в agent loops?
45Как реализовать tool calling без фреймворков?
78Разница между ReAct и Plan-and-Execute агентами
102Обработка ошибок при выполнении инструментов
156Метрики оценки производительности агента
211Управление историей диалога в памяти
305Structured output в OpenAI API
419Тестирование агентов с заглушками
567Как избежать infinite loops в агентах
689Сравнение форматов tool calls: OpenAI vs Anthropic

10. Чек-лист самопроверки

  • Я определил класс Tool с полями name, description, parameters, func.
  • Моя функция call_llm принимает список сообщений и возвращает словарь.
  • Парсер корректно извлекает tool call как из JSON-строки, так и из нативного формата.
  • После выполнения tool я добавляю результат в историю с правильной ролью.
  • Цикл завершается, если LLM не вернул tool call или превышен max_steps.
  • Я написал хотя бы два инструмента с разным количеством обязательных аргументов.
  • Я покрыл тестами парсинг, выполнение и полный сценарий из 3 шагов.
  • Код не содержит зависимостей от agent-фреймворков (LangChain, CrewAI).
  • README содержит пример запуска и описание архитектуры.
  • Проект запускается без ошибок на чистом Python 3.10+ с установленным openai (или аналогичным).