Разработка ИИ-агентов · Модуль 1 · Урок 1.2

Вызов LLM API напрямую на Go: messages, роли, параметры

Модель общается списком сообщений

Современные чат-модели принимают на вход не одну строку, а список сообщений. Каждое сообщение имеет роль (role) и содержимое (content). Базовые роли: system — инструкции и рамки поведения (задаются один раз в начале), user — реплики пользователя, assistant — предыдущие ответы самой модели. Чуть позже добавится роль tool — для результатов инструментов.

Важно понять: API без состояния (stateless). Модель не «помнит» прошлый разговор сама — всю историю вы передаёте заново при каждом запросе. Поэтому «память» агента — это просто растущий слайс сообщений, который вы аккуратно ведёте на стороне Go. Это и хорошо (полный контроль), и накладно (за каждый токен истории вы платите снова).

OpenAI-совместимый запрос на Go

Мы будем ходить в модель через OpenRouter — это шлюз с OpenAI-совместимым Chat Completions API. Запрос — это обычный HTTP POST с JSON-телом: поле model (слаг модели), messages (список) и параметры генерации. Ответ содержит choices, внутри — message с ответом и finish_reason (почему модель остановилась).

Слаги моделей и точные имена параметров версионно-зависимы — сверяйтесь с актуальными доками провайдера. На момент написания у Anthropic это семейства claude-sonnet-4.x / claude-opus-4.x / claude-haiku-4.x; в OpenRouter они адресуются как anthropic/claude-.... Не хардкодьте слаг «навечно» — выносите в конфигурацию.

Параметры генерации

Несколько параметров, которые стоит знать сразу:

  • temperature — «креативность». Около 0 ответы детерминированнее и стабильнее (это нужно агентам, где важна предсказуемость), выше — разнообразнее, но менее надёжно.
  • max_tokens — потолок длины ответа. Защищает от слишком длинной (и дорогой) генерации.
  • system-сообщение — самый сильный рычаг поведения: роль агента, что можно и нельзя, формат ответа.

Для агентов почти всегда берут низкую температуру: цикл и так добавляет неопределённости, и лишний разброс на каждом шаге вредит.

Минимальный вызов чат-модели через OpenRouter (OpenAI-совместимый API)
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    body := map[string]any{
        "model": "anthropic/claude-sonnet-4.6", // сверьтесь с актуальным слагом
        "messages": []map[string]string{
            {"role": "system", "content": "Ты лаконичный помощник. Отвечай по делу."},
            {"role": "user", "content": "Объясни в двух предложениях, что такое агентный цикл."},
        },
        "temperature": 0,
        "max_tokens":  300,
    }
    raw, _ := json.Marshal(body)

    req, _ := http.NewRequest("POST",
        "https://openrouter.ai/api/v1/chat/completions", bytes.NewReader(raw))
    req.Header.Set("Authorization", "Bearer "+os.Getenv("OPENROUTER_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    data, _ := io.ReadAll(resp.Body)

    var out struct {
        Choices []struct {
            Message struct {
                Content string `json:"content"`
            } `json:"message"`
            FinishReason string `json:"finish_reason"`
        } `json:"choices"`
    }
    if err := json.Unmarshal(data, &out); err != nil || len(out.Choices) == 0 {
        fmt.Println("ответ не разобран:", string(data))
        return
    }
    fmt.Println("Ответ:", out.Choices[0].Message.Content)
    fmt.Println("finish_reason:", out.Choices[0].FinishReason)
}

Anti-patterns

Анти-паттернПочему плохоКак правильно
Хранить ключ API прямо в кодеУтечка секрета в репозиторийЧитать из окружения (os.Getenv), .env — в .gitignore
Считать API statefulМодель «забывает» контекст — ответы рассыпаютсяПередавать всю нужную историю сообщений в каждом запросе
Хардкодить слаг модели по всему кодуСлаги меняются; правок много, легко забытьОдин источник — конфиг/env; в примерах помечать «сверьтесь с доками»
Высокая температура у агентаНестабильные шаги, плавающее поведениеНизкая температура (≈0) для предсказуемости цикла

Практическое задание

  • Заведите OPENROUTER_API_KEY в окружении и сделайте первый успешный вызов модели из Go.
  • Добавьте system-сообщение и убедитесь, что оно меняет поведение (тон, формат, ограничения).
  • Проведите мини-диалог из 2 ходов, вручную дозаписывая ответ модели как сообщение с ролью assistant в историю.
  • Поэкспериментируйте с temperature 0 и 1 на одном и том же запросе — сравните стабильность ответов.
  • Выведите finish_reason и проверьте, что при обычном ответе он равен stop.

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

Почему историю диалога приходится передавать модели в каждом запросе?

  • A Так быстрее работает сеть
  • B Потому что Chat Completions API без состояния (stateless): модель не хранит контекст между запросами
  • C Этого делать не нужно — модель помнит всё сама
  • D Так требует компилятор Go

Какая роль задаёт модели рамки поведения и обычно ставится в начале списка сообщений?

  • A user
  • B assistant
  • C tool
  • D system

Какое значение temperature обычно предпочтительно для агента и почему?

  • A Высокое — чтобы агент был «умнее»
  • B Низкое (около 0) — чтобы шаги были предсказуемыми и воспроизводимыми
  • C Значение не влияет на поведение агента
  • D Ровно 1.0 всегда