Сложность оркестрации: когда агенты превращаются в кошмар
Представьте себе типичную сцену: вы пишете AI-агента, который должен обрабатывать запросы пользователей. Сначала все просто – пара функций, базовый промпт. Потом добавляете обработку файлов, работу с API, анализ данных. Через месяц у вас уже не агент, а монстр с 50 инструментами, сложной логикой переключения между ними и кучей конфигурационных файлов.
И вот вы уже не пишете код, а занимаетесь оркестрацией. Redis для состояния, Celery для очередей, Kubernetes для масштабирования. Агент превратился в распределенную систему, которую нужно мониторить, дебажить и поддерживать.
Проблема в фундаментальном подходе: мы пытаемся управлять агентами как микросервисами, хотя они по природе своей – файлы с инструкциями.
Файловая система как универсальный протокол
Что если я скажу вам, что можно забыть про сложные системы оркестрации? Что вместо RabbitMQ, Redis и Kubernetes достаточно... обычной файловой системы?
Идея проста до гениальности: каждый навык агента – это отдельный каталог с файлом SKILL.md. Агент не «знает» о всех навыках заранее. Он обнаруживает их динамически, читая файловую систему. Это называется прогрессивным раскрытием.
1 Структура навыка: что внутри SKILL.md?
Давайте посмотрим на реальный пример. Допустим, у нас есть навык для работы с файлами. Вот как выглядит его структура:
skills/
├── file_operations/
│ ├── SKILL.md # Описание навыка и инструкции
│ ├── requirements.txt # Зависимости (опционально)
│ └── tools.py # Реализация функций (опционально)
├── web_search/
│ ├── SKILL.md
│ └── tools.py
└── data_analysis/
├── SKILL.md
└── requirements.txt
А вот содержимое типичного SKILL.md:
# File Operations Skill
## Описание
Навык для работы с файловой системой: чтение, запись, копирование, удаление.
## Инструкции для LLM
1. Когда пользователь просит прочитать файл, используй функцию read_file
2. Для записи файла используй write_file
3. Всегда проверяй существование файла перед операцией
## Доступные инструменты
- read_file(path: str) -> str
- write_file(path: str, content: str) -> bool
- delete_file(path: str) -> bool
- list_files(directory: str) -> list
Красота в том, что этот файл читают и люди, и LLM. Для людей – это документация. Для агента – это инструкции.
2 Реализация на Python: 100 строк кода
Теперь самое интересное – как заставить это работать. Вот полная реализация загрузчика навыков:
import os
import yaml
import importlib.util
from pathlib import Path
from typing import Dict, List, Any
class SkillLoader:
def __init__(self, skills_dir: str = "skills"):
self.skills_dir = Path(skills_dir)
self.skills = {}
self.tools = {}
def discover_skills(self) -> Dict[str, Dict[str, Any]]:
"""Найти все навыки в директории"""
skills = {}
for skill_dir in self.skills_dir.iterdir():
if not skill_dir.is_dir():
continue
skill_file = skill_dir / "SKILL.md"
if not skill_file.exists():
continue
skill_data = self._parse_skill_file(skill_file)
skill_name = skill_dir.name
# Загружаем Python-инструменты, если они есть
tools_module = self._load_tools_module(skill_dir)
if tools_module:
skill_data['tools'] = tools_module
self.tools.update(self._extract_tools(tools_module))
skills[skill_name] = skill_data
self.skills = skills
return skills
def _parse_skill_file(self, skill_path: Path) -> Dict[str, Any]:
"""Парсим SKILL.md в структурированные данные"""
content = skill_path.read_text(encoding='utf-8')
skill_data = {
'name': skill_path.parent.name,
'description': '',
'instructions': '',
'tools_description': '',
'requirements': []
}
# Простой парсинг Markdown по секциям
lines = content.split('\n')
current_section = ''
for line in lines:
if line.startswith('## '):
current_section = line[3:].strip().lower()
elif line.startswith('### '):
current_section = line[4:].strip().lower()
elif current_section:
if 'описание' in current_section:
skill_data['description'] += line + '\n'
elif 'инструкции' in current_section:
skill_data['instructions'] += line + '\n'
elif 'инструменты' in current_section:
skill_data['tools_description'] += line + '\n'
# Проверяем requirements.txt
req_file = skill_path.parent / "requirements.txt"
if req_file.exists():
skill_data['requirements'] = req_file.read_text().splitlines()
return skill_data
def _load_tools_module(self, skill_dir: Path):
"""Динамически загружаем модуль tools.py"""
tools_file = skill_dir / "tools.py"
if not tools_file.exists():
return None
module_name = f"skills.{skill_dir.name}.tools"
spec = importlib.util.spec_from_file_location(module_name, tools_file)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _extract_tools(self, module) -> Dict[str, callable]:
"""Извлекаем функции-инструменты из модуля"""
tools = {}
for attr_name in dir(module):
attr = getattr(module, attr_name)
if callable(attr) and not attr_name.startswith('_'):
tools[attr_name] = attr
return tools
def get_context_for_llm(self, skill_names: List[str] = None) -> str:
"""Генерируем контекст для LLM с выбранными навыками"""
if skill_names is None:
skill_names = list(self.skills.keys())
context_parts = ["Доступные навыки:\n\n"]
for skill_name in skill_names:
if skill_name not in self.skills:
continue
skill = self.skills[skill_name]
context_parts.append(f"## {skill_name}\n")
context_parts.append(f"Описание: {skill['description']}\n")
context_parts.append(f"Инструкции: {skill['instructions']}\n")
context_parts.append(f"Инструменты: {skill['tools_description']}\n\n")
return "\n".join(context_parts)
def execute_tool(self, tool_name: str, *args, **kwargs):
"""Выполняем инструмент по имени"""
if tool_name not in self.tools:
raise ValueError(f"Инструмент {tool_name} не найден")
return self.tools[tool_name](*args, **kwargs)
Вот и вся магия. Никаких сложных конфигураций, никаких YAML на 1000 строк. Просто файлы в папках.
3 Как это работает в реальном агенте
Теперь давайте посмотрим, как использовать этот загрузчик в реальном агенте. Вот минимальный пример с Anthropic Claude API:
import anthropic
from skill_loader import SkillLoader
class FileSystemAgent:
def __init__(self):
self.skill_loader = SkillLoader("skills")
self.skills = self.skill_loader.discover_skills()
self.client = anthropic.Anthropic(api_key="your_key_here")
def select_relevant_skills(self, user_query: str) -> list:
"""Выбираем релевантные навыки на основе запроса"""
# Здесь можно использовать embedding или простые ключевые слова
relevant_skills = []
query_lower = user_query.lower()
for skill_name, skill_data in self.skills.items():
# Простая эвристика: ищем ключевые слова в описании
skill_desc = skill_data['description'].lower()
if 'файл' in query_lower and 'файл' in skill_desc:
relevant_skills.append(skill_name)
elif 'поиск' in query_lower and 'поиск' in skill_desc:
relevant_skills.append(skill_name)
# Добавьте больше правил по необходимости
return relevant_skills if relevant_skills else list(self.skills.keys())[:3]
def process_query(self, user_query: str) -> str:
"""Обрабатываем запрос пользователя"""
# 1. Выбираем релевантные навыки
relevant_skills = self.select_relevant_skills(user_query)
# 2. Генерируем контекст с этими навыками
context = self.skill_loader.get_context_for_llm(relevant_skills)
# 3. Формируем промпт для LLM
prompt = f"""{context}
Запрос пользователя: {user_query}
Ответь, какие инструменты нужно использовать и с какими параметрами.
Формат ответа: TOOL_CALL:название_инструмента:параметры_json"""
# 4. Отправляем в LLM
response = self.client.messages.create(
model="claude-3-sonnet-20240229",
max_tokens=1000,
messages=[{"role": "user", "content": prompt}]
)
# 5. Парсим ответ и выполняем инструменты
response_text = response.content[0].text
if "TOOL_CALL:" in response_text:
# Извлекаем вызов инструмента
tool_call = response_text.split("TOOL_CALL:")[1].strip()
tool_name, params_json = tool_call.split(":", 1)
# Выполняем инструмент
import json
params = json.loads(params_json)
result = self.skill_loader.execute_tool(tool_name, **params)
return f"Результат выполнения {tool_name}: {result}"
return response_text
Обратите внимание на ключевой момент: агент видит только те навыки, которые релевантны запросу. Это и есть прогрессивное раскрытие. Не нужно грузить LLM всеми 50 навыками сразу.
Почему это работает лучше сложных систем
Давайте сравним подходы. Вот что происходит в типичной системе оркестрации агентов:
| Сложная оркестрация | Файловая система |
|---|---|
| Redis для хранения состояния агентов | Состояние в файлах (или вообще без состояния) |
| Celery/Kafka для очередей задач | Очереди? Какие очереди? Это же один процесс |
| Kubernetes для масштабирования | Масштабирование через репликацию папок |
| Сложная конфигурация в YAML | Конфигурация в SKILL.md (читают и люди, и машины) |
| Отладка через логи в Kibana | Отладка через чтение файлов в IDE |
| Обновление через deployment pipeline | Обновление через git pull в папке skills |
Видите разницу? Файловая система – это самый старый, самый надежный и самый понятный протокол обмена данными. Она работает везде: от вашего ноутбука до кластера в облаке.
Типичные ошибки и как их избежать
Когда я впервые реализовал эту систему, я наступил на все грабли. Вот что нужно знать, чтобы не повторить моих ошибок:
Ошибка 1: Слишком сложные SKILL.md
Как НЕ надо делать:
# Слишком сложный SKILL.md
## Архитектурные решения
Мы используем паттерн репозитория для абстракции доступа к данным...
## Алгоритмы оптимизации
Для ускорения запросов применяется кэширование второго уровня...
## Мониторинг и метрики
Метрики экспортируются в Prometheus со следующими labels...
LLM не нужна вся эта информация. Ей нужны конкретные инструкции: «как вызвать функцию», «что она делает», «какие параметры принимает». Все остальное – шум.
Ошибка 2: Глобальное состояние в инструментах
# ПЛОХО: инструмент с глобальным состоянием
cache = {}
def search_with_cache(query: str):
if query in cache:
return cache[query]
result = expensive_search(query)
cache[query] = result
return result
Почему это плохо? Потому что состояние будет теряться при перезагрузке агента. Вместо этого используйте внешнее хранилище (базу данных, Redis) или делайте инструменты идемпотентными.
Ошибка 3: Слишком много навыков в контексте
Не нужно загружать все 50 навыков для каждого запроса. Если пользователь спрашивает про погоду, ему не нужны навыки работы с базой данных. Реализуйте умный выбор релевантных навыков. Можно даже использовать embedding для этого.
Продвинутые техники
Когда вы освоите базовый подход, можно добавить несколько продвинутых фич:
1. Динамическая загрузка навыков
Что если навыки могут появляться и исчезать во время работы агента? Добавьте вот такой метод:
def watch_for_new_skills(self):
"""Наблюдаем за появлением новых навыков"""
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
class SkillHandler(FileSystemEventHandler):
def on_created(self, event):
if event.is_directory and (event.src_path / "SKILL.md").exists():
self.reload_skills()
observer = Observer()
observer.schedule(SkillHandler(), self.skills_dir, recursive=True)
observer.start()
2. Иерархия навыков
Некоторые навыки зависят от других. Например, навык «анализ данных» требует навык «чтение файлов». Реализуйте это через зависимости в SKILL.md:
## Зависимости
- file_operations
- math_operations
3. Тестирование навыков
Создайте папку tests/ рядом с каждым навыком. В ней храните тестовые сценарии и expected output. Агент может сам тестировать свои навыки перед использованием.
Когда это не подойдет
Честно говоря, файловая система – не серебряная пуля. Вот когда нужно использовать традиционную оркестрацию:
- Распределенные агенты: если ваши агенты работают на разных машинах и должны общаться между собой
- Высокая нагрузка: тысячи запросов в секунду – файловая система не справится
- Сложные workflow: когда нужно точно контролировать порядок выполнения и rollback
- Многопользовательские системы: если разные пользователи должны иметь разные наборы навыков
Но для 90% проектов файловая система более чем достаточна. Особенно если вы только начинаете работать с агентами.
Что дальше?
Эта реализация – только начало. Можно развивать идею в нескольких направлениях:
- Версионирование навыков: хранить несколько версий SKILL.md и переключаться между ними
- Навыки как пакеты: упаковывать навыки в pip-пакеты и устанавливать через requirements.txt
- Репозиторий навыков: централизованное хранилище, откуда агенты могут скачивать новые навыки
- Автоматическое документирование: генерировать документацию по навыкам на основе кода
Самое интересное начинается, когда вы комбинируете этот подход с другими техниками. Например, с параллельным запуском coding-агентов или с продвинутой упаковкой знаний.
Главный урок: не усложняйте то, что можно сделать просто. Файловая система существует 50 лет не просто так. Она решает 80% проблем оркестрации без единой строки дополнительного кода.
Попробуйте эту реализацию. Возьмите код выше, создайте папку skills/, положите туда пару SKILL.md файлов. Удивитесь, насколько это просто работает. А потом добавьте еще один навык. И еще один. Без изменения кода агента.
Вот что значит прогрессивное раскрытие: система растет вместе с вашими потребностями, а не ломается под их тяжестью.