MODULE_01 · УРОК 1.2

Первые инструменты

С read агент умеет открыть файл, если знает имя. «Найди все TODO в проекте» — и он начинает наугад открывать файлы. Это не поиск. Добавим grep. Но появится новая задача: модель должна сама понять, когда звать read, а когда grep. Подсказка — в тексте описания инструмента.

ЧТО ПОЛУЧИТСЯ Инструмент grep с понятным описанием «когда вызывать». На поиск модель идёт в grep, на «открой вот этот файл» — в read. Переключение — за счёт docstring, без отдельного роутера в вашем коде.

Слова, которые встретятся

  • Docstring — строка в тройных кавычках у функции. Для человека это документация; для модели — инструкция «в каких ситуациях звать этот инструмент».
  • Regex (регулярное выражение) — шаблон поиска вроде TODO или def \w+ (имя функции).
  • Glob — маска имени файла: *.py — «все Python-файлы».
  • Маршрутизация / выбор инструмента — решение модели, какую функцию вызвать. Вы не пишете if «если TODO → grep»; вы пишете хорошие описания, и модель выбирает сама.

Быстрый путь

  1. Добавьте grep: шаблон поиска, по желанию папка и маска файлов, не больше 50 совпадений.
  2. Опишите его блоками WHEN TO USE / WHEN NOT TO USE / DO NOT USE FOR / EXAMPLES.
  3. Тем же форматом обновите описание у read, чтобы он «отталкивал» поиск.

Упражнение 1.2

  • Аргументы: pattern, опционально path и glob.
  • Внутри — системная команда grep через subprocess. Не лезьте в .venv, __pycache__, .git (там шум, не ваш код).
  • Если совпадений больше 50 — покажите первые 50 и напишите, сколько было всего.
  • «Ничего не найдено» — нормальный успешный ответ, а не авария программы.

Плохое описание → плохой выбор

Сначала нарочно сделайте описание из двух слов и посмотрите, как модель ошибётся:

main.py

@agent.tool
def grep(ctx: RunContext[Path], pattern: str, glob: str | None = None) -> str:
    """Search files."""
    ...

Terminal

uv run python main.py . "Найди все TODO-комментарии в проекте"

Часто модель проигнорирует grep и полезет в read (или, когда появится терминал, — в bash). Двух слов мало, чтобы выбрать: она угадывает.

Описание — это подсказка модели, не «для документации»

Чините не обязательно код поиска. Сначала — текст, который видит модель:

фрагмент grep для main.py

@agent.tool
def grep(
    ctx: RunContext[Path],
    pattern: str,
    path: str | None = None,
    glob: str | None = None,
) -> str:
    """Поиск по содержимому файлов через regex. Строки с путями.

    WHEN TO USE: паттерны по многим файлам, определения функций,
      импорты, TODO, сообщения об ошибках.
    WHEN NOT TO USE: чтение уже известного файла (используй read).
    DO NOT USE FOR: запуск команд, листинг каталогов.
    EXAMPLES:
      - TODO: pattern "TODO" glob "*.py"
      - определения функций: pattern "def \\w+" glob "*.py"
    """
    import shlex
    import subprocess

    root = (ctx.deps / (path or ".")).resolve()
    include = glob or "*.py"
    cmd = (
        f"grep -rn --exclude-dir=.venv --exclude-dir=__pycache__ "
        f"--exclude-dir=.git --include={shlex.quote(include)} "
        f"-E {shlex.quote(pattern)} {shlex.quote(str(root))}"
    )
    try:
        proc = subprocess.run(
            cmd, shell=True, capture_output=True, text=True, timeout=10
        )
        lines = [ln for ln in proc.stdout.strip().splitlines() if ln]
    except subprocess.TimeoutExpired:
        return "Таймаут: поиск занял слишком много времени."

    if not lines:
        return "Совпадений нет."

    MAX_MATCHES = 50
    truncated = len(lines) > MAX_MATCHES
    shown = lines[:MAX_MATCHES]
    body = "\n".join(shown)
    if truncated:
        body += f"\n... ({len(lines)} всего, показаны первые {MAX_MATCHES})"
    return body

Что значат блоки в описании:

  • WHEN TO USE — «зови меня, если…»
  • WHEN NOT TO USE — «в этой ситуации лучше другой инструмент»
  • DO NOT USE FOR — ещё раз жёстко: чем этим инструментом не решают задачу
  • EXAMPLES — конкретные примеры аргументов
ТЯГА К ТЕРМИНАЛУ DeepSeek (и многие другие модели) при слабых описаниях любят «просто выполнить команду в shell». Поэтому запреты пишут дважды: и WHEN NOT TO USE, и DO NOT USE FOR. Один раз сказать часто мало.

То же самое — для read: он должен явно отправлять поиск к grep:

"""Прочитать файл проекта. Возвращает пронумерованные строки.
WHEN TO USE: просмотр содержимого, конфигов, исходников.
WHEN NOT TO USE: поиск по нескольким файлам (используй grep).
DO NOT USE FOR: запуск команд, листинг каталогов.
"""
ЗАЧЕМ ПОТОЛОК 50 СОВПАДЕНИЙ Поиск слова вроде import по большому проекту без лимита вернёт сотни строк. Модели этого не нужно, чтобы ответить «где TODO». 50 — достаточно. 500 — уже мусор в контексте разговора.

Проверьте

Положите в маленький файл пару строк с # TODO:, затем:

uv run python main.py . "Найди все TODO-комментарии в проекте"
uv run python main.py . "Прочитай pyproject.toml"

Первый запрос должен уйти в grep, второй — в read. Если оба раза один и тот же инструмент — подкрутите описания, а не только код.

Готово, если:

  • «Найди TODO» вызывает grep, а не read
  • «Прочитай pyproject.toml» вызывает read
  • grep отдаёт не больше 50 строк и пишет, сколько было всего, если обрезал
  • У обоих инструментов в описании есть WHEN TO USE / WHEN NOT TO USE / DO NOT USE FOR
ЭКСПЕРИМЕНТ Портьте описание grep по кусочкам: сначала уберите EXAMPLES, потом DO NOT USE FOR, потом WHEN NOT TO USE. На каком шаге модель снова начинает угадывать через read (или терминал)?