Агент-инженер по репозиторию · Модуль 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).
+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
+} 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?
Верный ответ: B
B. Модели нужен сигнал об ошибке (is_error=true) и достаточно информации, чтобы исправиться, но без утечки внутренних деталей (стек, пути, секреты) — это и безопасность, и чистота контекста. Сырой стек (A) засоряет и течёт; пустой ответ (C) и молчание (D) скрывают сбой.