Агент-инженер по репозиторию · Модуль 2 · Урок 2.2

Инструменты исследования: list_files/glob и grep

Зачем (какую проблему чиним)

Чтобы взяться за задачу, агент должен сориентироваться в репозитории: какие есть файлы, где искать символ или строку. Читать файлы вслепую — дорого и медленно. Дадим инструменты list_files (обзор структуры по glob) и grep (поиск по содержимому).

Решение и альтернативы

Решение: list_files обходит дерево от корня репозитория, фильтрует по glob-шаблону и исключает мусор (.git, vendor, node_modules) и бинарники; grep ищет строку/регэксп, возвращая путь:строку с ограничением на число совпадений (бюджет контекста). Оба — только чтение.

Альтернативы: звать системные find/grep через shell — соблазнительно, но это тянет нас к произвольному shell (см. урок 2.4, где мы это явно запрещаем) и непереносимо. Реализуем на стандартной библиотеке Go (io/fs, regexp) — детерминированно, переносимо, без shell.

DIFF

Добавляем два инструмента поверх контракта Tool; оба используют safeJoin и игнор-листы.

⚠ Безопасность

Поиск тоже ограничен корнем репозитория (тот же safeJoin для базовой директории). Исключение .git и секрет-подобных файлов (.env, ключи) из выдачи — чтобы агент не затягивал секреты в контекст модели. Ограничение числа совпадений — защита и от взрыва контекста, и от случайного дампа большого файла.

Проверка

Спросите «найди, где определяется safeJoin» — агент вызывает grep, получает tools.go:NN и отвечает точно, не читая весь репозиторий. list_files /*.go возвращает только Go-файлы вне игнор-листа.

Глубже

Почему поиск важнее «прочитать всё» — это уже про retrieval и бюджет окна: курс «Продакшн-разработка», Модуль 1 (context engineering) и Модуль 2 (RAG). В версии v2 мы заменим наивный grep гибридным поиском.

tools_explore.go: list_files и grep на стандартной библиотеке (новый файл, фрагмент)
+// grepTool ищет подстроку/регэксп по файлам репозитория. Только чтение.
+type grepTool struct{ root string }
+
+func (grepTool) Spec() anthropic.ToolParam {
+	return anthropic.ToolParam{
+		Name:        "grep",
+		Description: anthropic.String("Найти строки по регулярному выражению в файлах репозитория."),
+		InputSchema: anthropic.ToolInputSchemaParam{
+			Properties: map[string]any{
+				"pattern": map[string]any{"type": "string"},
+				"glob":    map[string]any{"type": "string", "description": "напр. **/*.go"},
+			},
+			Required: []string{"pattern"},
+		},
+	}
+}
+
+func (g grepTool) Run(ctx context.Context, input json.RawMessage) (string, error) {
+	var in struct{ Pattern, Glob string }
+	if err := json.Unmarshal(input, &in); err != nil {
+		return "", fmt.Errorf("неверные аргументы")
+	}
+	re, err := regexp.Compile(in.Pattern)
+	if err != nil {
+		return "", fmt.Errorf("неверный regexp: %v", err)
+	}
+	const maxHits = 100 // бюджет контекста: не вываливаем весь репозиторий
+	var hits []string
+	walkRepo(g.root, in.Glob, func(rel string, line int, text string) bool {
+		if re.MatchString(text) {
+			hits = append(hits, fmt.Sprintf("%s:%d: %s", rel, line, text))
+		}
+		return len(hits) < maxHits
+	})
+	if len(hits) == 0 {
+		return "совпадений не найдено", nil
+	}
+	return strings.Join(hits, "\n"), nil
+}
+
+// walkRepo обходит файлы вне игнор-листа (.git, vendor, node_modules, секреты).
+// Реализация в helpers.go использует io/fs и safeJoin; shell не вызывается.

Anti-patterns

Грабли инструментов исследования
ГрабляПочему плохоКак правильно
Вызывать системные find/grep через shellТянет к произвольному shell (дыра безопасности), непереносимо между ОСРеализовать на io/fs/regexp — детерминированно, переносимо, без shell
Возвращать все совпадения без лимитаВзрыв контекста и стоимости; один большой файл может затопить окноОграничить число совпадений (maxHits) и длину строки
Включать .git, .env, ключи в выдачу поискаСекреты затягиваются в контекст модели и логиИгнор-лист служебных и секрет-подобных путей

Практическое задание (RA-v1)

  • Добавить list_files (glob по дереву с игнор-листом) и grep (regexp с лимитом совпадений), оба read-only и в пределах корня.
  • Исключить .git/vendor/node_modules и секрет-подобные файлы из обхода.
  • Закоммитить: git commit -m "v1: list_files and grep tools".

Проверка знаний

Почему grep-инструмент реализуют на стандартной библиотеке Go, а не вызовом системного grep через shell?

  • A Системный grep медленнее
  • B Вызов через shell открывает путь к произвольному исполнению команд и непереносим; stdlib даёт детерминированный безопасный поиск
  • C В Go нет регулярных выражений
  • D Так требует anthropic-sdk-go