Реализация Agent Skills на Python через файловую систему | AiManual
AiManual Logo Ai / Manual.
15 Янв 2026 Гайд

Agent Skills на Python: почему файловая система умнее любой оркестрации

Как заменить сложную оркестрацию агентов простой файловой системой. Реализация на 100 строк кода, SKILL.md и прогрессивное раскрытие навыков.

Сложность оркестрации: когда агенты превращаются в кошмар

Представьте себе типичную сцену: вы пишете AI-агента, который должен обрабатывать запросы пользователей. Сначала все просто – пара функций, базовый промпт. Потом добавляете обработку файлов, работу с API, анализ данных. Через месяц у вас уже не агент, а монстр с 50 инструментами, сложной логикой переключения между ними и кучей конфигурационных файлов.

И вот вы уже не пишете код, а занимаетесь оркестрацией. Redis для состояния, Celery для очередей, Kubernetes для масштабирования. Агент превратился в распределенную систему, которую нужно мониторить, дебажить и поддерживать.

Проблема в фундаментальном подходе: мы пытаемся управлять агентами как микросервисами, хотя они по природе своей – файлы с инструкциями.

Файловая система как универсальный протокол

Что если я скажу вам, что можно забыть про сложные системы оркестрации? Что вместо RabbitMQ, Redis и Kubernetes достаточно... обычной файловой системы?

Идея проста до гениальности: каждый навык агента – это отдельный каталог с файлом SKILL.md. Агент не «знает» о всех навыках заранее. Он обнаруживает их динамически, читая файловую систему. Это называется прогрессивным раскрытием.

💡
Прогрессивное раскрытие – это когда агент видит только те навыки, которые нужны для текущей задачи. Не все 50 сразу, а именно те, что релевантны контексту. Файловая система делает это естественно: вы просто кладете нужные 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% проектов файловая система более чем достаточна. Особенно если вы только начинаете работать с агентами.

Что дальше?

Эта реализация – только начало. Можно развивать идею в нескольких направлениях:

  1. Версионирование навыков: хранить несколько версий SKILL.md и переключаться между ними
  2. Навыки как пакеты: упаковывать навыки в pip-пакеты и устанавливать через requirements.txt
  3. Репозиторий навыков: централизованное хранилище, откуда агенты могут скачивать новые навыки
  4. Автоматическое документирование: генерировать документацию по навыкам на основе кода

Самое интересное начинается, когда вы комбинируете этот подход с другими техниками. Например, с параллельным запуском coding-агентов или с продвинутой упаковкой знаний.

Главный урок: не усложняйте то, что можно сделать просто. Файловая система существует 50 лет не просто так. Она решает 80% проблем оркестрации без единой строки дополнительного кода.

Попробуйте эту реализацию. Возьмите код выше, создайте папку skills/, положите туда пару SKILL.md файлов. Удивитесь, насколько это просто работает. А потом добавьте еще один навык. И еще один. Без изменения кода агента.

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