Реализовать 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-пайплайна — симулируем:
- Создать новый публичный/приватный репозиторий на GitHub с веткой
mainиdevelop. - Настроить Actions через
.github/workflows/lint.yml. - Запускать workflow локально с помощью act (nektos/act) или через веб-интерфейс GitHub после каждого пуша.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык реализации | Python 3.10+ | Написание ядра линтера |
| Обработка YAML | PyYAML / ruamel.yaml | Чтение промптов и конфигурации |
| Шаблоны для placeholders | re (регулярные выражения) | Поиск {placeholder} и проверка их наличия |
| Проверка длины | len() | Подсчёт символов/токенов |
| Запрещённые паттерны | re или fnmatch | Блокировка ignore previous instructions, [SYSTEM] и т.п. |
| CLI интерфейс | argparse / click | Запуск линтера из командной строки |
| CI/CD | GitHub Actions | Автоматический запуск при каждом коммите в PR |
| Тестирование | pytest | Юнит-тесты для правил и ядра |
4. Этапы выполнения
Этап 1: Проектирование правил линтинга (30 минут)
Действия
-
Определить три категории проверок
Категория Пример проблемы Желаемое поведение Длина (length) Промпт > 2000 символов ERROR Placeholders (placeholders) Обязательный {query}не найденERROR Запрещённые паттерны (forbidden) Текст ignore previous instructionsERROR -
Создать конфигурационный файл
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 -
Подготовить промпты|тестовые промпты в
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 часа)
Действия
-
Создать структуру проекта
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) -
Реализовать
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 -
Реализовать 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 -
Реализовать
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() -
Написать юнит-тесты (
tests/test_linter.py) для каждой функции проверки на тестовых промптах. Убедиться, что покрытие >80%.
Ожидаемый результат этапа CLI-команда python -m linter.cli prompts/ запускается, выводит ошибки и возвращает code|exit code 1 при проблемах.
Этап 3: Интеграция с CI (1 час)
Действия
-
Создать файл
.github/workflows/lint.ymlname: 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 -
Проверить работу workflow
- Создать ветку
feature/add-prompt-lint - Запушить коммит с промптом, содержащим ошибку
- Убедиться, что GitHub Actions завершается с красным статусом (error) и блокирует слияние (если настроены branch rules)
- Исправить промпт, запушить — статус зелёный
- Создать ветку
Ожидаемый результат этапа Workflow работает, при ошибках пайплайн красный, при исправлении — зелёный.
Этап 4: Тестирование и валидация (30 минут)
Действия
-
Проверить edge case: пустые файлы, не-YAML файлы (игнорировать или ошибка?)
- В core.py добавить проверку расширения: if not filepath.endswith(('.yaml', '.txt')): continue
- Для пустых файлов — ошибка? Мягко: предупреждение.
-
Прогнать линтер на всех подготовленных промптах:
Промпт Ожидаемый результат good_prompt.yaml 0 ошибок too_long_prompt.yaml 1 ошибка length missing_placeholder_prompt.yaml 1 ошибка placeholders forbidden_pattern_prompt.yaml 1 ошибка forbidden mixed_issues_prompt.yaml 3 ошибки -
Проверить, что exit code равен 0 при чистом запуске и 1 при наличии ошибок (скрипт
exit_code_test.sh). -
Написать простой скрипт для измерения времени выполнения линтера на 50 промптах — должно быть < 1 секунды.
Ожидаемый результат этапа Все тесты пройдены, производительность приемлема.
Этап 5 (опционально): Документация и расширение (30 минут)
Действия
- Написать README.md с примерами использования.
- Добавить возможность игнорирования файлов (
.promptlintignore). - Реализовать поддержку многострочных промптов из 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: Интеграция с CI | 1 ч |
| Этап 4: Тестирование и валидация | 30 мин |
| Этап 5 (опционально): Документация | 30 мин |
| Итого | 4,5–5 ч |
Примечание Для первого раза может потребоваться до 6 часов с учётом отладки ошибок и настройки GitHub Actions.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 12 | Базовые принципы prompt engineering |
| 34 | Шаблоны и placeholders в промптах |
| 58 | Безопасность промптов: инъекции и утечки |
| 71 | CI/CD для ML-проектов |
| 88 | Статический анализ кода (linters) |
| 105 | YAML для конфигурации |
| 123 | Использование регулярных выражений |
| 140 | Тестирование утилит командной строки (pytest) |
| 167 | Интеграция GitHub Actions |
| 189 | Управление ошибками и кодами возврата |
10. Чек-лист самопроверки
- Я создал репозиторий со структурой, описанной в Этапе 2.
- Я написал тесты для каждого правила (хотя бы 1 позитивный и 1 негативный сценарий).
- Я запустил pytest и убедился, что все тесты проходят.
- Я проверил, что линтер из командной строки корректно обрабатывает все тестовые промпты.
- Я закоммитил в ветку feature и убедился, что GitHub Actions выполняется и красный при ошибках.
- Я написал README, описывающий установку и конфигурацию.