Агент-инженер по репозиторию · Модуль 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.
+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}
+}-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,
+ })
+}-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.
Чем это плохо в первую очередь и как исправить?
Верный ответ: B
B. Хардкод ключа — утечка секрета в историю репозитория (необратимо без переписывания истории и ротации ключа); хардкод модели мешает переключать её без пересборки. Ключ читает SDK из ANTHROPIC_API_KEY, модель и лимиты — из конфига. Переименование (C) не решает проблему; свой клиент (D) — лишняя работа.
Агент получил ответ с resp.StopReason == "end_turn" и текстовым блоком.
Как корректно достать ответ и завершить ход?
Верный ответ: B
B. Контент ответа — это список блоков разных типов; текст собирают, проходя по блокам и отбирая TextBlock. stop_reason != tool_use означает конец хода — выходим. Индексация Content[0] (A) хрупка и не типобезопасна; лишний вызов (C) и текстовый маркер (D) не нужны.