Перейти к содержанию

01. Физика LLM — как работает "мозг" агента

Зачем это нужно?

Чтобы управлять агентом, полезно понимать, как работает его "мозг". Без базового понимания физики LLM вы не сможете:

  • Правильно настроить модель для агента
  • Понять, почему агент ведет себя недетерминированно
  • Управлять контекстом и историей диалога
  • Избежать галлюцинаций и ошибок

В этой главе разберём основы работы LLM простым языком, без лишней математики.

Реальный кейс

Ситуация: Вы создали агента для DevOps. Пользователь пишет: "Проверь статус сервера web-01"

Проблема: Агент иногда отвечает текстом "Сервер работает", а иногда вызывает инструмент check_status. Поведение непредсказуемо.

Решение: Понимание вероятностной природы LLM помогает управлять разнообразием генерации: низкая Temperature, чёткие Description у инструментов. Но предсказуемость процесса агента обеспечивает не «детерминизм» модели, а структура — строгие схемы вызовов, валидация ответов и гарды в коде. Понимание контекстного окна помогает управлять историей диалога.

Теория простыми словами

Вероятностная природа

Ключевой факт: LLM не думает, она предсказывает.

LLM — это функция NextToken(Context) -> Distribution.
На вход подается последовательность токенов \(x_1, ..., x_t\). Модель вычисляет распределение вероятностей для следующего токена:

\[P(x_{t+1} | x_1, ..., x_t)\]

Что это значит на практике?

Примечание: Примеры ниже используют Function Calling — механизм вызова инструментов через LLM API. Детальное объяснение — в Главе 03: Инструменты и Function Calling. Сейчас достаточно понимать, что модель получает описания инструментов (tools[]) и выбирает подходящий на основе Description.

Пример 1: DevOps — Магия vs Реальность

Магия (как обычно объясняют):

Промпт: "Проверь статус сервера"
Модель видит контекст и предсказывает: "Я вызову инструмент check_status" (вероятность 0.85)

Реальность (как на самом деле):

1. Что отправляется в модель:

// Нейтральные структуры для прямого протокола tool-calling.
// У большинства провайдеров (облачных и локальных) протокол выглядит так же —
// детали и полный цикл см. в Главе 03.
type Message struct {
    Role       string     // "system" | "user" | "assistant" | "tool"
    Content    string
    ToolCalls  []ToolCall // заполняется моделью для role="assistant"
    ToolCallID string     // для role="tool": ID вызова, к которому относится результат
}

type ToolCall struct {
    ID        string // например, "call_abc123"
    Name      string // например, "check_status"
    Arguments string // JSON-строка, например {"hostname":"web-01"}
}

type ToolDef struct {
    Name        string
    Description string
    Parameters  json.RawMessage // JSON Schema аргументов
}

type Request struct {
    Model       string
    Messages    []Message
    Tools       []ToolDef
    Temperature float64
}

// System Prompt (задает роль и поведение)
systemPrompt := `You are a DevOps assistant. 
When user asks about server status, use the check_status tool.
When user asks about logs, use the read_logs tool.
When user asks to restart, use the restart_service tool.`

// User Input
userInput := "Проверь статус сервера"

// Описание доступных инструментов (tools schema)
// ВАЖНО: Модель видит ВСЕ инструменты и выбирает нужный!
tools := []ToolDef{
    {
        Name:        "check_status",
        Description: "Check the status of a server by hostname. Use this when user asks about server status or availability.",
        Parameters: json.RawMessage(`{
            "type": "object",
            "properties": {
                "hostname": {"type": "string", "description": "Server hostname"}
            },
            "required": ["hostname"]
        }`),
    },
    {
        Name:        "read_logs",
        Description: "Read the last N lines of service logs. Use this when user asks about logs, errors, or troubleshooting.",
        Parameters: json.RawMessage(`{
            "type": "object",
            "properties": {
                "service": {"type": "string", "description": "Service name"},
                "lines": {"type": "number", "description": "Number of lines to read"}
            },
            "required": ["service"]
        }`),
    },
    {
        Name:        "restart_service",
        Description: "Restart a systemd service. Use this when user explicitly asks to restart a service. WARNING: This causes downtime.",
        Parameters: json.RawMessage(`{
            "type": "object",
            "properties": {
                "service_name": {"type": "string", "description": "Service name to restart"}
            },
            "required": ["service_name"]
        }`),
    },
}

// Полный запрос к API
req := Request{
    Model: "glm-5.3", // любая актуальная модель с native tool calling
    Messages: []Message{
        {Role: "system", Content: systemPrompt},
        {Role: "user", Content: userInput},
    },
    Tools: tools,  // Важно: модель видит описание инструментов!
}

2. Что возвращает модель:

Модель не возвращает текст "Я вызову инструмент". Она возвращает структурированный tool call:

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "check_status",
        "arguments": "{\"hostname\": \"web-01\"}"
      }
    }
  ]
}

Как модель выбирает инструмент?

Модель видит все три инструмента и их Description:

  • check_status: "Check the status... Use this when user asks about server status"
  • read_logs: "Read logs... Use this when user asks about logs"
  • restart_service: "Restart service... Use this when user explicitly asks to restart"

Запрос пользователя: "Проверь статус сервера"

Модель сопоставляет запрос с описаниями:

  • check_status — описание содержит "server status" → выбирает этот
  • read_logs — описание про логи, не про статус
  • restart_service — описание про рестарт, не про проверку

Пример с другим запросом:

userInput := "Покажи последние ошибки в логах nginx"

// Модель видит те же 3 инструмента
// Сопоставляет:
// - check_status: про статус, не про логи → не подходит
// - read_logs: "Use this when user asks about logs" → ВЫБИРАЕТ ЭТОТ
// - restart_service: про рестарт → не подходит

// Модель возвращает:
// tool_calls: [{function: {name: "read_logs", arguments: "{\"service\": \"nginx\", \"lines\": 50}"}}]

Суть: Модель выбирает инструмент на основе семантического соответствия между запросом пользователя и Description инструмента. Чем точнее Description, тем лучше выбор.

3. Что делает Runtime:

resp, err := client.Complete(ctx, req) // нейтральный вызов: один протокол, любой провайдер
if err != nil {
    return err // ошибки клиента — не для модели: retry/логи в коде (гл. 04)
}
msg := resp.Choices[0].Message // ответ модели

// Runtime проверяет: есть ли tool_calls?
if len(msg.ToolCalls) > 0 {
    call := msg.ToolCalls[0]

    // Парсим аргументы (Arguments — JSON-строка)
    var args struct {
        Hostname string `json:"hostname"`
    }
    json.Unmarshal([]byte(call.Arguments), &args)

    // Выполняем реальную функцию
    result := checkStatus(args.Hostname)  // "Server is ONLINE"

    // Возвращаем результат обратно в модель:
    // assistant-сообщение с tool_calls + результат с role="tool"
    req.Messages = append(req.Messages, msg,
        Message{Role: "tool", Content: result, ToolCallID: call.ID},
    )

    // Отправляем обновленную историю в модель снова
    // Модель видит результат и решает, что делать дальше
}

Примечание о "вероятностях":

Цифры типа "вероятность 0.85" — это иллюстрация. API провайдеров (облачных и локальных) обычно не возвращает эти вероятности напрямую (если не использовать logprobs). Важно другое: когда в контексте есть tools с хорошим Description, модель с высокой вероятностью выберет tool call вместо текста. Но это происходит внутри модели — снаружи мы видим только итоговый выбор.

Пример 2: Support — Магия vs Реальность

Магия:

Промпт: "Пользователь жалуется на ошибку 500" Модель предсказывает: "Сначала соберу контекст через get_ticket_details" (вероятность 0.9)

Реальность: Принцип тот же, что и в DevOps-примере. Модель получает 4 инструмента (get_ticket_details, check_account_status, search_kb, draft_reply) с описаниями. Она выбирает get_ticket_details, потому что его Description содержит "Use this FIRST when user reports an error" — наилучшее семантическое совпадение с запросом.

Модель выбирает инструменты последовательно:

// Итерация 1: Пользователь жалуется на ошибку
// Модель выбирает: get_ticket_details (собирает контекст)

// Итерация 2: В контексте появились детали тикета
// Модель выбирает: search_kb("error 500") (ищет решение)

// Итерация 3: В контексте появилось решение из KB
// Модель выбирает: draft_reply(ticket_id, solution) (создает ответ)

Runtime на каждой итерации парсит tool call, выполняет функцию и возвращает результат. Подробнее о протоколе tool call — в Главе 03.

Пример 3: Data Analytics — Магия vs Реальность

Магия:

Промпт: "Покажи продажи за последний месяц" Модель предсказывает: "Сформулирую SQL-запрос через sql_select" (вероятность 0.95)

Реальность: Модель получает 3 инструмента (describe_table, sql_select, check_data_quality). Запрос "Покажи продажи за последний месяц" лучше всего соответствует sql_select с описанием "when user asks for specific data or reports". Модель генерирует SQL прямо в аргументах:

{
  "tool_calls": [{
    "function": {
      "name": "sql_select",
      "arguments": "{\"query\": \"SELECT region, SUM(amount) FROM sales WHERE date >= NOW() - INTERVAL '1 month' GROUP BY region\"}"
    }
  }]
}

Для сложных вопросов модель использует инструменты последовательно: сначала describe_table (узнать структуру), потом sql_select (получить данные), потом check_data_quality (проверить качество). Runtime валидирует, что это SELECT (не DELETE/DROP!), и выполняет запрос через read-only соединение. Подробнее — в Главе 03.

Почему это важно для инженера?

1. Недетерминированность

Запустив агента дважды с одним промптом, вы можете получить разные действия.

Пример:

Запрос 1: "Проверь сервер"
Ответ 1: [Вызывает check_status]

Запрос 2: "Проверь сервер" (тот же промпт)
Ответ 2: [Отвечает текстом "Сервер работает"]

Решение: Понизьте Temperature (для агентных циклов типично 0.0–0.3) — это уменьшает разнообразие генерации и делает выбор инструментов более устойчивым. Но не ждите «детерминизма»: у многих провайдеров T=0 технически не гарантирует greedy-декодинг (батчинг запросов, MoE-архитектуры). Воспроизводимость процесса агента дают структура (JSON Schema, валидация), гарды в коде и evals — об этом подробнее в разделе «Температура» ниже.

req := Request{
    Model:       "glm-5.3",
    Temperature: 0.2, // меньше разброса в выборе инструментов
    // ...
}

2. Галлюцинации

Модель стремится сгенерировать правдоподобный, а не истинный текст.

DevOps пример: Модель может написать "используй флаг --force" для команды, которая его не поддерживает.

Data пример: Модель может сгенерировать SQL с несуществующим полем user.email вместо users.email.

Support пример: Модель может "выдумать" решение проблемы, которого нет в базе знаний.

Решение: Grounding (Заземление). Мы даем агенту доступ к реальным данным (Tools/RAG) и запрещаем выдумывать факты.

systemPrompt := `You are a DevOps assistant.
Do not invent facts: you have tools that return real data, and real data
is always more reliable than a plausible guess. If you don't know
something, say "I don't know" or use a tool.`

Токены и контекстное окно

Что такое токен?

Токен — это единица текста, которую обрабатывает модель.

  • Один токен ≈ 0.75 слова (в английском)
  • В русском: одно слово ≈ 1.5 токена

Пример:

Текст: "Проверь статус сервера"
Токены: ["Проверь", " статус", " сервера"]  // ~3 токена

Контекстное окно (Context Window)

Контекстное окно — это "оперативная память" модели.

Порядок величин (без точных цифр, которые быстро устаревают):

  • У современных моделей контекстные окна — от ~128k до ~1M токенов (GLM-5.3, Fable 5.1, Opus 5.5)
  • Точное значение смотрите в документации провайдера — окна меняются чаще, чем выходят версии книг

Примечание: Главное — принцип: чем больше контекстное окно, тем больше информации агент может "помнить" в одном запросе. Но большое окно не отменяет управление контекстом: длинная история стоит денег и деградирует по качеству (см. Главу 13: Context Engineering).

Что это значит для агента?

Все, что агент "знает" о текущей задаче — это то, что влезает в контекстное окно (Prompt + History).

Пример расчета (приблизительно):

Контекстное окно: 128k токенов
System Prompt + tools: 2k токенов
История диалога: 60k токенов
Результаты инструментов: 30k токенов
Осталось места: 36k токенов

Важно для агента (правило главы 13). В рантайме агента первичный источник числа токенов — usage.PromptTokens / usage.CompletionTokens из ответа провайдера, а не свой счётчик: провайдер уже знает свой токенайзер и применил его. Для грубой пред-оценки до отправки достаточно эвристики ~3 символа/токен (для английского ближе к 4); токенайзеры вроде tiktoken точнее, но для решения «не пора ли сжимать историю» эта точность обычно не нужна. Делать бюджет агента на основе только своих оценок — рецепт расхождения с реальностью на 10–30%, что особенно больно у большого окна. Подробнее см. Глава 13: Context Engineering.

Если история переполняется, агент "забывает" начало разговора. На практике это значит: либо ранние сообщения обрезаны/сжаты (и модель их реально не видит), либо, если переполнение не обработать, API вернёт ошибку превышения контекстного окна.

Модель Stateless: Она не помнит ваш прошлый запрос, если вы не передали его снова в messages.

// Каждый запрос должен включать всю историю
messages := []Message{
    {Role: "system", Content: systemPrompt},
    {Role: "user", Content: "Проверь сервер"},
    {Role: "assistant", Content: "Проверяю..."},
    {Role: "tool", Content: "Server is ONLINE"},
    {Role: "user", Content: "А что с базой?"},  // Агент видит всю историю!
}

Температура (Temperature)

Температура — это настройка качества и разнообразия генерации: она управляет энтропией распределения вероятностей при выборе следующего токена.

Temperature: 0.0–0.3  // типично для агентных циклов и структурированных вызовов
Temperature: 0.7      // баланс креативности и стабильности
Temperature: 1.0+     // креативные задачи, брейншторм

Когда использовать какое значение?

Temperature Использование Пример
0.0–0.3 Агентные циклы, структурированные вызовы, JSON DevOps-агент стабильно вызывает restart_service, а не "творит"; выбор инструментов и аргументов дополнительно фиксируется JSON Schema и валидацией
0.3–0.7 Шаблонные ответы, черновики Support-агент генерирует шаблоны ответов
0.7–1.0 Креативные задачи Product-агент пишет маркетинговые тексты

Чего температура не даёт

Распространённое заблуждение: «Temperature = 0 делает агента детерминированным». Это не так:

  • Технически T=0 у многих провайдеров не гарантирует greedy-детерминизм: батчинг запросов и MoE-архитектуры вносят недетерминизм независимо от температуры.
  • Воспроизводимость процесса агента дают не настройки сэмплинга, а структура: строгая JSON Schema для аргументов инструментов, валидация ответов, гарды в коде (контроль допустимых действий) и evals, фиксирующие поведение.

Низкая температура снижает разброс и «шум» в выборе инструментов — это полезно. Но это настройка качества генерации, а не переключатель «детерминизм вкл/выкл».

Практический пример:

// ПЛОХО: для агентного цикла
req := Request{
    Temperature: 0.9,  // слишком большой разброс: модель будет "творить"
    // ...
}

// ХОРОШО: для агентного цикла
req := Request{
    Temperature: 0.2,  // меньше разброса; предсказуемость процесса — schema + гарды + evals
    // ...
}

Выбор модели

Не все модели одинаково хороши для агентов.

Актуальные модели (на момент ревизии)

  • Fable 5.1 и Opus 5.5 — флагманские модели для сложных агентных сценариев
  • GLM-5.3 — сильная модель с native tool calling, хороший баланс цены и качества
  • GLM-5.3 Flash — дешёвый и быстрый вариант для массовых и простых вызовов

Примечание: Имена и версии моделей живут месяцами, а не годами. Доверяйте не спискам из книг, а свежей документации провайдера и собственному capability-тесту (см. ниже).

Локальный запуск

Для локального запуска (Ollama, vLLM, LM Studio) берите актуальную instruct-модель с заявленной поддержкой function calling — и обязательно прогоняйте capability benchmark: работа инструментов в локальных рантаймах зависит от chat-шаблона модели, а не только от весов.

Критерии выбора

  1. Поддержка Function Calling (native tool calling): Модель должна уметь генерировать структурированные вызовы инструментов.

    • Хорошо: модели с native tool calling (GLM-5.3, GLM-5.3 Flash, Fable 5.1, Opus 5.5)
    • Плохо: базовые модели без обучения на tools

    Примечание: Конкретные модели меняются. Проверяйте поддержку function calling через capability benchmark (см. Приложение: Capability Benchmark).

  2. Размер контекста: В окно должны помещаться system prompt, tools, история и результаты инструментов. У современных моделей это от ~128k токенов — при условии управления контекстом (см. Главу 13) этого обычно достаточно.

  3. Качество следования инструкциям: Модель должна строго следовать System Prompt.

  4. Совместимость chat-шаблона (для локальных рантаймов): Роли сообщений (system, user, assistant, tool) должны соответствовать chat-шаблону модели, иначе tool-протокол не заработает. Подробнее см. Ошибка 3: LM Studio — неверный Prompt Template в разделе "Типовые ошибки". У облачных провайдеров с native tool calling этой проблемы нет.

Как проверить модель?

Теория: См. Приложение: Capability Benchmark — подробное описание того, что проверяем и почему это важно.

Практика: См. Lab 00: Model Capability Benchmark — готовый инструмент для проверки модели.

Типовые ошибки

Ошибка 1: Модель ведет себя по-разному при одинаковых запросах

Симптом: Один и тот же промпт дает разные результаты. Агент иногда вызывает инструмент, иногда отвечает текстом.

Причина: LLM сэмплирует из распределения вероятностей. При высокой температуре разброс растет, и «серые зоны» — ситуации, где и текст, и tool call правдоподобны — становятся нестабильными.

Решение:

// ПЛОХО: высокая температура в агентном цикле
req := Request{
    Temperature: 0.9,  // большой разброс: выбор инструментов нестабилен
    // ...
}

// ХОРОШО: низкая температура + структурные гарантии
req := Request{
    Temperature: 0.2,  // меньше разброса
    // ...
}
// И главное: валидируйте tool calls по JSON Schema, отклоняйте
// нераспарсенные аргументы, ограничивайте допустимые инструменты.
// Воспроизводимость процесса агента дают гарды и evals, а не температура.

Ошибка 2: Контекст переполняется

Симптом: Агент "забывает" начало разговора. После N сообщений перестает помнить, что обсуждалось в начале. Или API возвращает ошибку превышения контекстного окна.

Причина: История диалога растет и превышает размер контекстного окна модели.

Решение: Condense — одно сжатие истории при подходе к лимиту

Канонический механизм (подробнее — Глава 09: Memory и Глава 13: Context Engineering):

  1. Порог — ~0.85 контекстного окна, считается по usage.PromptTokens из ответа провайдера (не по собственному счётчику).
  2. Максимум одно сжатие за Run: при превышении порога старая часть истории (head) суммируется отдельным LLM-вызовом, свежая (tail) сохраняется как есть.
  3. Пары tool_call ↔ tool_result не разрываются: граница сжатия проходит только между завершёнными парами, иначе история становится невалидной.
  4. Кэшируемый префикс не меняется: messages[0] (system prompt) и список tools остаются неизменными — провайдер кэширует именно этот префикс.
// Condense: порог по данным провайдера, head -> summary, tail как есть
const condenseThreshold = 0.85

if float64(resp.Usage.PromptTokens) > condenseThreshold*float64(ctxWindow) && !condensed {
    // 1. Граница: индекс, на котором завершена последняя пара
    //    tool_call <-> tool_result (пары не разрываем!)
    cut := lastCompleteToolBoundary(req.Messages)

    // 2. Head (все после system) суммируем отдельным LLM-вызовом
    summary := summarize(ctx, req.Messages[1:cut])

    // 3. Новый контекст: system (без изменений!) + summary + tail
    req.Messages = append([]Message{
        req.Messages[0], // system: мутировать нельзя — сломает prompt cache
        {Role: "user", Content: "Summary of earlier conversation:\n" + summary},
    }, req.Messages[cut:]...)
    condensed = true // не чаще одного раза за Run
}

Почему НЕ «вставить summary вторым system-сообщением»?

Частый соблазн — приклеить сжатую историю сразу после system:

// ПЛОХО: мутирует кэшируемый префикс
compressed := []Message{
    systemMsg,
    {Role: "system", Content: "Summary of previous conversation:\n" + summary},
}

Это ломает prompt cache: провайдер кэширует неизменный префикс (system + tools). Вставка нового сообщения сразу после system меняет префикс — кэш инвалидируется, и каждый запрос после сжатия оплачивается полностью. Summary должно идти в теле истории (например, как user-сообщение), а messages[0] трогать нельзя.

Почему НЕ обрезка?

Наивная обрезка «System + последние N» теряет важную информацию из начала: имя пользователя, контекст задачи, принятые решения. Condense сохраняет суть: 2000 токенов сжимаются до ~200 с сохранением ключевых фактов.

Пример:

Исходная история (60k токенов):
- User: "Меня зовут Иван, я DevOps инженер"
- Assistant: "Привет, Иван!"
- User: "У нас проблема с сервером"
- Assistant: "Опишите проблему"
... (еще 50 сообщений и результаты инструментов)

  • После обрезки: теряем имя и контекст
  • После condense: "Пользователь Иван, DevOps инженер. Обсуждали проблему с сервером. Текущая задача: диагностика." + свежие сообщения как есть

См. подробнее: раздел "Memory (Память)" в Главе 09: Анатомия Агента и Lab 09: Context Optimization

Ошибка 3: LM Studio — неверный Prompt Template (ошибка ролей)

Симптом: При локальном запуске через LM Studio вы получаете ошибку вида "Only user and assistant roles are supported!".

Причина и решение: Chat-шаблон модели принимает только роли user/assistant и не знает ролей system/tool, которые нужны агенту. Выберите в настройках модели шаблон, соответствующий ей (ChatML-совместимый или специфичный для семейства модели), или возьмите модель из репозитория с уже корректным шаблоном. У облачных провайдеров с native tool calling этой проблемы нет.

Критерии сдачи / Чек-лист

Сдано:

  • Понимаете, что LLM предсказывает токены, а не "думает"
  • Понимаете, что температура — настройка качества генерации; воспроизводимость процесса агента дают JSON Schema, валидация, гарды и evals
  • Понимаете ограничения контекстного окна
  • Знаете, как управлять историей диалога (condense при ~0.85 окна по usage.PromptTokens)
  • Модель поддерживает Function Calling (проверено через Lab 00)
  • System Prompt мотивированно запрещает галлюцинации, grounding через Tools/RAG настроен

Не сдано:

  • Разброс поведения агента не контролирован (высокая температура, нет валидации tool calls)
  • Агент "забывает" начало разговора (контекст переполняется, condense не реализован)
  • Модель выдумывает факты (нет grounding через Tools/RAG)

Мини-упражнения

Упражнение 1: Подсчет токенов

Контекст. Эта функция — пред-оценка, чтобы прикинуть, не пора ли сжимать историю до отправки запроса. После запроса используем resp.Usage.PromptTokens от провайдера как авторитетный источник. См. примечание в разделе «Контекстное окно» выше и Главу 13.

Напишите функцию, которая приблизительно подсчитывает количество токенов в тексте:

func estimateTokens(text string) int {
    // Примерная оценка: 1 токен ≈ 4 символа (для английского)
    // Для русского: 1 токен ≈ 3 символа
    return len(text) / 4
}

Ожидаемый результат:

  • Функция возвращает приблизительное количество токенов
  • Учитывает разницу между английским и русским текстом

Упражнение 2: Управление историей

Реализуйте функцию обрезки истории сообщений:

func trimHistory(messages []Message, maxTokens int) []Message {
    // Оставляем System Prompt + последние сообщения, которые влезают в maxTokens
    // Граница обрезки не должна разрывать пару tool_call <-> tool_result
    // ...
}

Ожидаемый результат:

  • System Prompt всегда остается первым (messages[0] не мутируется)
  • Последние сообщения добавляются, пока не превысят maxTokens
  • Граница обрезки не разрывает пары tool_call ↔ tool_result
  • Функция возвращает обрезанную историю

Для любопытных

Углубление в формализм. Можно пропустить, если вас интересует только практика.

Температура — формально

Температура \(T\) масштабирует логиты распределения \(P(x_{t+1} | x_1, ..., x_t)\) перед softmax: при \(T \to 0\) распределение вырождается в аргмакс (greedy), при \(T = 1\) остается "как есть", при \(T > 1\) выравнивается и становится более случайным. Отсюда практический эффект из раздела «Температура»: низкая температура сужает разброс генерации, но не гарантирует воспроизводимость на уровне провайдера (батчинг, MoE).

Почему модель "выбирает" инструмент?

Формально модель не «выбирает» — она генерирует последовательность токенов, соответствующую формату tool call, потому что обучалась на примерах вызовов функций. System Prompt и tools[] смещают распределение в сторону этого формата: чем точнее Description инструмента совпадает с запросом, тем выше вероятность соответствующих токенов. Никакой магии — результат обучения.

Связь с другими главами

Что дальше?

После изучения физики LLM переходите к: