Реализовать prompt linting

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать prompt linting

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

Научиться проектировать и реализовывать систему статического анализа промптов (prompt linting), которая выявляет типовые проблемы: недопустимую длину, отсутствующие обязательные placeholders, запрещённые паттерны (инъекции, утечки контекста). Встроить линтер в CI-пайплайн, чтобы пайплайн завершался с ошибкой (красный) при обнаружении проблем, а при чистом результате — проходил зелёным.

Ключевой результат Рабочий CLI-линтер + Actions workflow]], который при каждом коммите проверяет промпты из указанной директории и блокирует слияние при ошибках.


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

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

Что нужноОткуда взять
Коллекция промптов (5–10 шаблонов)Создать вручную в папке prompts/ — файлы .yaml или .txt
Файл конфигурации правил линтераСоздать в корне проекта prompt-linter.yml
CI-системаGitHub Actions (репозиторий на GitHub)
Python 3.10+Установить локально или в GitHub Codespace

Если нет реального CI-пайплайна — симулируем:

  1. Создать новый публичный/приватный репозиторий на GitHub с веткой main и develop.
  2. Настроить Actions через .github/workflows/lint.yml.
  3. Запускать workflow локально с помощью act (nektos/act) или через веб-интерфейс GitHub после каждого пуша.

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

КомпонентИнструментыНазначение
Язык реализацииPython 3.10+Написание ядра линтера
Обработка YAMLPyYAML / ruamel.yamlЧтение промптов и конфигурации
Шаблоны для placeholdersre (регулярные выражения)Поиск {placeholder} и проверка их наличия
Проверка длиныlen()Подсчёт символов/токенов
Запрещённые паттерныre или fnmatchБлокировка ignore previous instructions, [SYSTEM] и т.п.
CLI интерфейсargparse / clickЗапуск линтера из командной строки
CI/CDGitHub ActionsАвтоматический запуск при каждом коммите в PR
ТестированиеpytestЮнит-тесты для правил и ядра

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

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

Действия

  1. Определить три категории проверок

    КатегорияПример проблемыЖелаемое поведение
    Длина (length)Промпт > 2000 символовERROR
    Placeholders (placeholders)Обязательный {query} не найденERROR
    Запрещённые паттерны (forbidden)Текст ignore previous instructionsERROR
  2. Создать конфигурационный файл prompt-linter.yml в корне проекта:

    rules:
      length:
        enabled: true
        max_chars: 2000
        severity: error
      placeholders:
        enabled: true
        required:
          - "{query}"
          - "{context}"
        severity: error
      forbidden:
        enabled: true
        patterns:
          - "ignore previous instructions"
          - "forget all instructions"
          - "you are now a "
        severity: error
    
  3. Подготовить промпты|тестовые промпты в prompts/ (минимум 5):

    • good_prompt.yaml — корректный
    • too_long_prompt.yaml — >2000 символов
    • missing_placeholder_prompt.yaml — без {query}
    • forbidden_pattern_prompt.yaml — содержит ignore previous instructions
    • mixed_issues_prompt.yaml — несколько ошибок

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


Этап 2: Реализация ядра линтера (2 часа)

Действия

  1. Создать структуру проекта

    prompt-linter/
    ├── .github/
    │   └── workflows/
    │       └── lint.yml          # будет на этапе 3
    ├── prompts/
    │   ├── good_prompt.yaml
    │   ├── too_long_prompt.yaml
    │   ├── missing_placeholder_prompt.yaml
    │   ├── forbidden_pattern_prompt.yaml
    │   └── mixed_issues_prompt.yaml
    ├── linter/
    │   ├── __init__.py
    │   ├── core.py               # основная логика
    │   ├── rules.py              # реализация каждого правила
    │   ├── config.py             # загрузка конфига
    │   └── cli.py                # точка входа
    ├── tests/
    │   ├── __init__.py
    │   └── test_linter.py
    ├── prompt-linter.yml         # конфиг
    ├── requirements.txt
    └── setup.py (или pyproject.toml)
    
  2. Реализовать linter/rules.py — функции проверки:

    import re
    from typing import List, Dict
    
    def check_length(prompt: str, rule_config: dict) -> List[Dict]:
        errors = []
        max_chars = rule_config.get("max_chars", 2000)
        if len(prompt) > max_chars:
            errors.append({
                "rule": "length",
                "severity": rule_config.get("severity", "error"),
                "message": f"Prompt length {len(prompt)} exceeds max {max_chars}"
            })
        return errors
    
    def check_placeholders(prompt: str, rule_config: dict) -> List[Dict]:
        errors = []
        required = rule_config.get("required", [])
        for ph in required:
            if ph not in prompt:
                errors.append({
                    "rule": "placeholders",
                    "severity": rule_config.get("severity", "error"),
                    "message": f"Required placeholder '{ph}' not found"
                })
        return errors
    
    def check_forbidden(prompt: str, rule_config: dict) -> List[Dict]:
        errors = []
        patterns = rule_config.get("patterns", [])
        for pat in patterns:
            if re.search(re.escape(pat), prompt, re.IGNORECASE):
                errors.append({
                    "rule": "forbidden",
                    "severity": rule_config.get("severity", "error"),
                    "message": f"Forbidden pattern found: '{pat}'"
                })
        return errors
    
  3. Реализовать linter/core.py — агрегатор:

    from linter.config import load_config
    from linter.rules import check_length, check_placeholders, check_forbidden
    
    def lint_file(filepath: str, config: dict) -> list:
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
        
        errors = []
        rules = config.get("rules", {})
        
        if rules.get("length", {}).get("enabled", False):
            errors.extend(check_length(content, rules["length"]))
        if rules.get("placeholders", {}).get("enabled", False):
            errors.extend(check_placeholders(content, rules["placeholders"]))
        if rules.get("forbidden", {}).get("enabled", False):
            errors.extend(check_forbidden(content, rules["forbidden"]))
        
        return errors
    
  4. Реализовать linter/cli.py:

    import argparse, sys, glob
    from linter.core import lint_file
    from linter.config import load_config
    
    def main():
        parser = argparse.ArgumentParser(description="Prompt linter")
        parser.add_argument("paths", nargs="+", help="Prompt files or directories")
        parser.add_argument("--config", default="prompt-linter.yml")
        args = parser.parse_args()
        
        config = load_config(args.config)
        all_errors = []
        
        for pattern in args.paths:
            for filepath in glob.glob(pattern, recursive=True):
                errors = lint_file(filepath, config)
                for e in errors:
                    print(f"{filepath}: {e['severity']}: {e['message']}")
                all_errors.extend(errors)
        
        if all_errors:
            sys.exit(1)  # exit code 1 -> CI красный
        
    if __name__ == "__main__":
        main()
    
  5. Написать юнит-тесты (tests/test_linter.py) для каждой функции проверки на тестовых промптах. Убедиться, что покрытие >80%.

Ожидаемый результат этапа CLI-команда python -m linter.cli prompts/ запускается, выводит ошибки и возвращает code|exit code 1 при проблемах.


Этап 3: Интеграция с CI (1 час)

Действия

  1. Создать файл .github/workflows/lint.yml

    name: Prompt Lint
    on:
      push:
        branches: [main, develop]
      pull_request:
        branches: [main]
    jobs:
      lint:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-python@v5
            with:
              python-version: '3.10'
          - name: Install dependencies
            run: |
              python -m pip install --upgrade pip
              pip install -r requirements.txt
          - name: Run linter
            run: |
              python -m linter.cli prompts/ --config prompt-linter.yml
    
  2. Проверить работу workflow

    • Создать ветку feature/add-prompt-lint
    • Запушить коммит с промптом, содержащим ошибку
    • Убедиться, что GitHub Actions завершается с красным статусом (error) и блокирует слияние (если настроены branch rules)
    • Исправить промпт, запушить — статус зелёный

Ожидаемый результат этапа Workflow работает, при ошибках пайплайн красный, при исправлении — зелёный.


Этап 4: Тестирование и валидация (30 минут)

Действия

  1. Проверить edge case: пустые файлы, не-YAML файлы (игнорировать или ошибка?)

    • В core.py добавить проверку расширения: if not filepath.endswith(('.yaml', '.txt')): continue
    • Для пустых файлов — ошибка? Мягко: предупреждение.
  2. Прогнать линтер на всех подготовленных промптах:

    ПромптОжидаемый результат
    good_prompt.yaml0 ошибок
    too_long_prompt.yaml1 ошибка length
    missing_placeholder_prompt.yaml1 ошибка placeholders
    forbidden_pattern_prompt.yaml1 ошибка forbidden
    mixed_issues_prompt.yaml3 ошибки
  3. Проверить, что exit code равен 0 при чистом запуске и 1 при наличии ошибок (скрипт exit_code_test.sh).

  4. Написать простой скрипт для измерения времени выполнения линтера на 50 промптах — должно быть < 1 секунды.

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


Этап 5 (опционально): Документация и расширение (30 минут)

Действия

  1. Написать README.md с примерами использования.
  2. Добавить возможность игнорирования файлов (.promptlintignore).
  3. Реализовать поддержку многострочных промптов из JSON (если нужно).

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


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

  • Линтер запускается как CLI: python -m linter.cli prompts/ и возвращает ноль или ненулевой код.
  • Правило длины срабатывает на промпт > max_chars.
  • Правило placeholders обнаруживает отсутствие обязательных подстановок.
  • Правило forbidden находит и блокирует указанные паттерны (регистронезависимо).
  • Конфигурация читается из prompt-linter.yml (или указанного --config).
  • GitHub Actions workflow запускается на push и PR и завершается красным при наличии ошибок.
  • Линтер корректно обрабатывает пустые файлы и файлы с неподдерживаемыми расширениями (пропускает).
  • Покрытие кода тестами > 70% (измеряется pytest --cov).
  • Документация (README) содержит описание установки, конфигурации и пример.
  • Линтер не блокирует выполнение при отсутствии ошибок (exit code 0).

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

Итоговый артефакт — репозиторий prompt-linter, содержащий:

  • Папка linter/ — модуль с классом PromptLinter, функциями проверки правил и CLI-интерфейсом.
  • Папка prompts/ — примеры промптов (как валидные, так и невалидные).
  • Файл .github/workflows/lint.yml — CI-пайплайн.
  • Файл prompt-linter.yml — конфигурация правил (по умолчанию).
  • Файл requirements.txt — зависимости (pyyaml, pytest, click и т.д.).
  • README.md — инструкция по установке и использованию.

Дополнительно (по желанию): поддержка .promptlintignore, плагины для VS Code.


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

СложностьРешение
Ложные срабатывания на месте не-dangerous паттерновИспользовать чёрный список только самых явных инъекций; дать возможность конфигурировать исключения через allowed_patterns
Разные кодировки файлов (UTF-16, BOM)При открытии файла в core.py использовать encoding='utf-8-sig' и обрабатывать исключение UnicodeDecodeError
Определение длины в токенах (не символах)Добавить опциональную интеграцию с tiktoken (OpenAI tokenizer); вынести в отдельное правило, отключаемое по умолчанию
Производительность при большом количестве промптовИспользовать многопоточность (concurrent.futures) для параллельной проверки файлов; кэшировать загруженный конфиг
Workflow в GitHub Actions не срабатывает при push в ветки, отличные от mainПроверить, что on: push: корректно настроен на все ветки, или использовать paths: для фильтрации изменений в prompts/
Необходимость различать severity (warning vs error)В конфиг добавить поле severity, а в CLI — флаг --fail-on (error в дефолте, но можно изменить)

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

ЭтапВремя
Этап 1: Проектирование правил30 мин
Этап 2: Реализация ядра линтера2 ч
Этап 3: Интеграция с CI1 ч
Этап 4: Тестирование и валидация30 мин
Этап 5 (опционально): Документация30 мин
Итого4,5–5 ч

Примечание Для первого раза может потребоваться до 6 часов с учётом отладки ошибок и настройки GitHub Actions.


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

ВопросТема
12Базовые принципы prompt engineering
34Шаблоны и placeholders в промптах
58Безопасность промптов: инъекции и утечки
71CI/CD для ML-проектов
88Статический анализ кода (linters)
105YAML для конфигурации
123Использование регулярных выражений
140Тестирование утилит командной строки (pytest)
167Интеграция GitHub Actions
189Управление ошибками и кодами возврата

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

  • Я создал репозиторий со структурой, описанной в Этапе 2.
  • Я написал тесты для каждого правила (хотя бы 1 позитивный и 1 негативный сценарий).
  • Я запустил pytest и убедился, что все тесты проходят.
  • Я проверил, что линтер из командной строки корректно обрабатывает все тестовые промпты.
  • Я закоммитил в ветку feature и убедился, что GitHub Actions выполняется и красный при ошибках.
  • Я написал README, описывающий установку и конфигурацию.