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

Подключаем модель: anthropic-sdk-go, ключ и модель из конфига

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

Заглушка callModel не думает. Подключим настоящую модель через anthropic-sdk-go. Сразу делаем правильно: модель и ключ — конфигурируемы, а не зашиты в код. Это нужно и для безопасности (ключ не в репозитории), и для версии v5, где мы введём каскад моделей haiku→sonnet ради стоимости.

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

Решение: клиент anthropic.NewClient() читает ANTHROPIC_API_KEY из окружения; имя модели и лимиты берём из небольшого конфига (env с дефолтами). Ответ разбираем по блокам контента: текстовые блоки — это ответ модели, блоки tool_use (версия 1.3) — запрос инструментов. Завершение хода читаем из resp.StopReason.

Альтернативы: хардкод ключа/модели — недопустимо (утечка секрета, нельзя переключить модель без пересборки). Свой HTTP-клиент к API — лишняя работа и риск ошибок в форме запроса; SDK даёт типобезопасные параметры и сам ретраит транзиентные ошибки. (Версионная пометка: API ниже — для anthropic-sdk-go v1.x; сверяйтесь с актуальной версией SDK.)

DIFF

Добавляем конфиг и заменяем заглушку на реальный вызов. Тип step уходит — работаем напрямую с *anthropic.Message.

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

Ключ — только из окружения, никогда в коде и не в логах. Добавьте .env в .gitignore. Логируйте факт вызова и usage (токены), но не сам ключ и не полный промпт с потенциально чувствительными данными. Это первый кирпич аудита, который в версии v5 станет полноценным аудит-логом.

Проверка

ANTHROPIC_API_KEY=... go run . — агент отвечает на простой текстовый вопрос (без инструментов). ANTHROPIC_MODEL переопределяет модель. Без ключа — понятная ошибка конфигурации, а не паника.

Глубже

Управление окном и prompt caching, выбор модели под задачу — курс «Продакшн-разработка», Модуль 1 (context engineering) и Модуль 6 (стоимость/латентность, гл. 12). Каскад моделей соберём в уроке 6.3.

config.go: модель и ключ из окружения (новый файл)
+package main
+
+import "os"
+
+// Config — всё, что должно настраиваться снаружи, а не хардкодиться.
+type Config struct {
+	Model     string // ANTHROPIC_MODEL; дефолт — быстрая модель
+	MaxTokens int
+}
+
+func loadConfig() Config {
+	model := os.Getenv("ANTHROPIC_MODEL")
+	if model == "" {
+		model = "claude-haiku-4-5" // дефолт; в версии v5 — каскад haiku->sonnet
+	}
+	return Config{Model: model, MaxTokens: 1024}
+}
main.go: реальный вызов модели вместо заглушки (anthropic-sdk-go v1.x)
-import (
-	"context"
-	"fmt"
-)
+import (
+	"context"
+	"fmt"
+
+	"github.com/anthropics/anthropic-sdk-go"
+)
+
+// Agent держит клиента модели и конфиг. Ключ ANTHROPIC_API_KEY читает SDK.
+type Agent struct {
+	client anthropic.Client
+	cfg    Config
+}
+
+func newAgent(cfg Config) *Agent {
+	return &Agent{client: anthropic.NewClient(), cfg: cfg}
+}

-// callModel — заглушка. В уроке 1.2 заменим на вызов anthropic-sdk-go.
-func callModel(ctx context.Context, history []string) (step, error) {
-	return step{Reason: stopEndTurn, Text: "(заглушка) пока без модели"}, nil
-}
+func (a *Agent) callModel(ctx context.Context, msgs []anthropic.MessageParam) (*anthropic.Message, error) {
+	return a.client.Messages.New(ctx, anthropic.MessageNewParams{
+		Model:     anthropic.Model(a.cfg.Model),
+		MaxTokens: int64(a.cfg.MaxTokens),
+		Messages:  msgs,
+	})
+}
main.go: цикл теперь крутится на реальных сообщениях и stop_reason
-func runAgent(ctx context.Context, task string) (string, error) {
-	const maxSteps = 25
-	history := []string{task}
-	for i := 0; i < maxSteps; i++ {
-		s, err := callModel(ctx, history)
-		if err != nil {
-			return "", err
-		}
-		if s.Reason != stopToolUse {
-			return s.Text, nil
-		}
-		history = append(history, "(результат инструмента)")
-	}
-	return "", fmt.Errorf("превышен предел в %d шагов", maxSteps)
-}
+func (a *Agent) Run(ctx context.Context, task string) (string, error) {
+	const maxSteps = 25
+	msgs := []anthropic.MessageParam{
+		anthropic.NewUserMessage(anthropic.NewTextBlock(task)),
+	}
+	for i := 0; i < maxSteps; i++ {
+		resp, err := a.callModel(ctx, msgs)
+		if err != nil {
+			return "", err
+		}
+		msgs = append(msgs, resp.ToParam())
+		if resp.StopReason != anthropic.StopReasonToolUse {
+			return textOf(resp), nil // ход завершён
+		}
+		// Исполнение инструментов добавим в уроке 1.3.
+	}
+	return "", fmt.Errorf("превышен предел в %d шагов", maxSteps)
+}
+
+// textOf собирает текстовые блоки ответа в строку.
+func textOf(resp *anthropic.Message) string {
+	var out string
+	for _, block := range resp.Content {
+		if t, ok := block.AsAny().(anthropic.TextBlock); ok {
+			out += t.Text
+		}
+	}
+	return out
+}

Anti-patterns

Грабли подключения модели
ГрабляПочему плохоКак правильно
Ключ или имя модели зашиты в кодУтечка секрета в git-историю; смена модели требует пересборки и деплояКлюч — из ANTHROPIC_API_KEY (читает SDK), модель/лимиты — из конфига с дефолтами
Логировать полный промпт и ключ для отладкиСекрет и чувствительные данные утекают в логи и системы агрегацииЛогировать факт вызова, модель и usage; ключ и сырой промпт — никогда
Писать свой HTTP-клиент к API вместо SDKЛегко ошибиться в форме запроса; нет встроенных ретраев транзиентных ошибокИспользовать anthropic-sdk-go: типобезопасные параметры, ретраи, разбор контента

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

  • Добавить config.go с чтением ANTHROPIC_MODEL/лимитов и дефолтами; ключ оставить за SDK (ANTHROPIC_API_KEY).
  • Заменить заглушку на Agent.callModel через client.Messages.New; цикл Run крутить на []anthropic.MessageParam и resp.StopReason.
  • Добавить .env/секреты в .gitignore. Закоммитить: git commit -m "v0: wire anthropic-sdk-go, configurable model".

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

В ревью замечают, что имя модели и ключ API заданы константами прямо в main.go.

Чем это плохо в первую очередь и как исправить?

  • A Ничем; константы быстрее читаются
  • B Секрет попадает в git-историю, а смена модели требует пересборки; ключ — из окружения, модель — из конфига
  • C Нужно просто переименовать константы
  • D Надо перейти на свой HTTP-клиент

Агент получил ответ с resp.StopReason == "end_turn" и текстовым блоком.

Как корректно достать ответ и завершить ход?

  • A Взять resp.Content[0] как строку напрямую
  • B Пройти по блокам контента, собрать текстовые (anthropic.TextBlock) и вернуть; ход завершён, так как stop_reason != tool_use
  • C Сделать ещё один вызов модели на всякий случай
  • D Искать в тексте маркер завершения