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

Дизайн инструментов: схемы, валидация входа, формат ошибок

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

В версии v0 read_file мы написали «на коленке»: разбор аргументов и обработка ошибок размазаны по dispatch. Инструментов станет много (list, grep, git diff, run_tests). Без единого контракта каждый новый инструмент будет повторять одни и те же ошибки: невалидированный вход, неинформативные ошибки, молчаливые сбои. Введём единый интерфейс инструмента.

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

Решение: интерфейс Tool с методами Spec() (имя/описание/JSON-схема) и Run(ctx, input). Реестр Registry собирает инструменты и сам формирует список для модели и диспетчеризацию. Каждый Run возвращает (результат, ошибка); ошибка превращается в tool_result с is_error=true и понятным текстом для модели (чтобы она могла исправиться), но без утечки внутренних деталей (стек, абсолютные пути).

Альтернативы: гигантский switch в одной функции (как в v0) — не масштабируется, тестировать тяжело. Кодогенерация схем из структур — заманчиво, но на этом этапе усложняет; вернёмся при росте числа инструментов. Явный интерфейс — лучший компромисс между простотой и расширяемостью.

DIFF

Вводим интерфейс Tool и Registry, переносим read_file на новый контракт.

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

Контракт инструмента — место, где живёт валидация недоверенного ввода. Правило: каждый аргумент проверяется до любого побочного эффекта. Текст ошибки для модели не должен раскрывать внутренние пути и секреты — это и защита, и гигиена контекста.

Проверка

go test ./... на юнит-тестах реестра: неизвестный инструмент → ошибка; битые аргументы → is_error с понятным текстом; валидный вызов read_file работает как раньше.

Глубже

Дизайн инструментов и обработка ошибок в проде — курс «Продакшн-разработка», Модуль 4 (гл. 7–8); проектирование инструментов и MCP — курс архитектора (CCA).

tool.go: единый контракт инструмента и реестр (новый файл)
+package main
+
+import (
+	"context"
+	"encoding/json"
+
+	"github.com/anthropics/anthropic-sdk-go"
+)
+
+// Tool — единый контракт. Spec описывает инструмент для модели; Run исполняет.
+type Tool interface {
+	Spec() anthropic.ToolParam
+	Run(ctx context.Context, input json.RawMessage) (string, error)
+}
+
+// Registry собирает инструменты, отдаёт их список модели и диспетчеризует вызовы.
+type Registry struct{ tools map[string]Tool }
+
+func NewRegistry(ts ...Tool) *Registry {
+	r := &Registry{tools: map[string]Tool{}}
+	for _, t := range ts {
+		r.tools[t.Spec().Name] = t
+	}
+	return r
+}
+
+func (r *Registry) Params() []anthropic.ToolUnionParam {
+	out := make([]anthropic.ToolUnionParam, 0, len(r.tools))
+	for _, t := range r.tools {
+		spec := t.Spec()
+		out = append(out, anthropic.ToolUnionParam{OfTool: &spec})
+	}
+	return out
+}
+
+// Dispatch исполняет один tool_use, возвращая текст и флаг ошибки для tool_result.
+func (r *Registry) Dispatch(ctx context.Context, tu anthropic.ToolUseBlock) (string, bool) {
+	t, ok := r.tools[tu.Name]
+	if !ok {
+		return "неизвестный инструмент: " + tu.Name, true
+	}
+	out, err := t.Run(ctx, tu.Input)
+	if err != nil {
+		return err.Error(), true // is_error=true: модель увидит и сможет исправиться
+	}
+	return out, false
+}
main.go: цикл использует реестр вместо ручного switch
 		var results []anthropic.ContentBlockParamUnion
 		for _, block := range resp.Content {
 			tu, ok := block.AsAny().(anthropic.ToolUseBlock)
 			if !ok {
 				continue
 			}
-			out, err := a.dispatch(tu)
-			results = append(results, anthropic.NewToolResultBlock(tu.ID, out, err != nil))
+			out, isErr := a.tools.Dispatch(ctx, tu)
+			results = append(results, anthropic.NewToolResultBlock(tu.ID, out, isErr))
 		}
 		msgs = append(msgs, anthropic.NewUserMessage(results...))

Anti-patterns

Грабли дизайна инструментов
ГрабляПочему плохоКак правильно
Один растущий switch по имени инструментаСвязность растёт, тестировать инструмент в отрыве нельзя, легко забыть валидациюИнтерфейс Tool + реестр: каждый инструмент изолирован и тестируется отдельно
Возвращать модели сырую ошибку Go (стек, абсолютные пути)Утечка внутренних деталей в контекст; модель тонет в шумеКороткий понятный текст ошибки + is_error=true, без внутренних путей/секретов
Молчаливый сбой инструмента (пустой результат без флага ошибки)Модель строит ответ на пустоте, не зная о провалеВсегда возвращать результат или ошибку с флагом; никаких «тихих» пустых ответов

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

  • Ввести интерфейс Tool и Registry; перенести read_file на новый контракт.
  • Написать юнит-тесты реестра (неизвестный инструмент, битые аргументы, валидный вызов).
  • Закоммитить: git commit -m "v1: tool interface and registry".

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

Инструмент упал с внутренней ошибкой. Что вернуть модели в tool_result?

  • A Полный стек ошибки с абсолютными путями — пусть модель разбирается
  • B Короткий понятный текст ошибки с флагом is_error=true, без внутренних путей и секретов
  • C Пустую строку без флага ошибки
  • D Ничего не возвращать