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

03. Инструменты и Function Calling — "руки" агента

Примечание о терминологии: В этой главе используются термины "Function Calling" (название механизма в API) и "Tool Call" (конкретный вызов инструмента). Это одно и то же: механизм, при котором LLM возвращает структурированный запрос на вызов функции Go. В коде агента мы обычно говорим "tool" (инструмент), потому что это более общее понятие (инструмент может быть не только функцией Go, но и API-вызовом, командой и т.д.).

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

Инструменты превращают LLM из болтуна в работника. Без инструментов агент может только отвечать текстом, но не может взаимодействовать с реальным миром.

Function Calling — это механизм, который позволяет модели вызывать реальные функции Go, выполнять команды, читать данные и выполнять действия.

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

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

Проблема: Бот не может реально проверить сервер. Он только говорит: "Я проверю статус сервера web-01 для вас..." (текст)

Решение: Function Calling позволяет модели вызывать реальные функции Go. Модель генерирует структурированный JSON с именем функции и аргументами, ваш код выполняет функцию и возвращает результат обратно в модель.

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

Как работает Function Calling?

  1. Вы описываете функцию в формате JSON Schema
  2. LLM видит описание и решает: "Мне нужно вызвать эту функцию"
  3. LLM генерирует JSON с именем функции и аргументами
  4. Ваш код парсит JSON и выполняет реальную функцию
  5. Результат возвращается в LLM для дальнейшей обработки

Function Calling — механизм работы

Function Calling — это механизм, при котором LLM возвращает не текст, а структурированный JSON с именем функции и аргументами.

Полный цикл: от определения до выполнения

Давайте разберем полный цикл на примере инструмента ping:

Шаг 1: Определение инструмента (Tool Schema)

tools := []openai.Tool{
    {
        Type: openai.ToolTypeFunction,
        Function: &openai.FunctionDefinition{
            Name:        "ping",
            Description: "Ping a host to check connectivity",
            Parameters: json.RawMessage(`{
                "type": "object",
                "properties": {
                    "host": {
                        "type": "string",
                        "description": "Hostname or IP address to ping"
                    }
                },
                "required": ["host"]
            }`),
        },
    },
}

Что происходит: Мы описываем инструмент в формате JSON Schema. Это описание отправляется в модель вместе с запросом.

Шаг 2: Запрос к модели

messages := []openai.ChatCompletionMessage{
    {Role: "system", Content: "You are a network admin. Use tools to check connectivity."},
    {Role: "user", Content: "Проверь доступность google.com"},
}

req := openai.ChatCompletionRequest{
    Model:    openai.GPT3Dot5Turbo,
    Messages: messages,
    Tools:    tools,  // Модель видит описание инструментов!
    Temperature: 0,
}

resp, _ := client.CreateChatCompletion(ctx, req)
msg := resp.Choices[0].Message

Что происходит: Модель видит:

  • System prompt (роль и инструкции)
  • User input (запрос пользователя)
  • Tools schema (описание доступных инструментов)

Шаг 3: Ответ модели (Tool Call)

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

// msg.ToolCalls содержит:
[]openai.ToolCall{
    {
        ID: "call_abc123",
        Type: "function",
        Function: openai.FunctionCall{
            Name:      "ping",
            Arguments: `{"host": "google.com"}`,
        },
    },
}

Что происходит: Модель сгенерировала tool_call для инструмента ping и JSON с аргументами. Это не магия — модель видела Description: "Ping a host to check connectivity" и связала это с запросом пользователя.

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

Давайте расширим пример, добавив несколько инструментов:

tools := []openai.Tool{
    {
        Function: &openai.FunctionDefinition{
            Name:        "ping",
            Description: "Ping a host to check network connectivity. Use this when user asks about network reachability or connectivity.",
        },
    },
    {
        Function: &openai.FunctionDefinition{
            Name:        "check_http",
            Description: "Check HTTP status code of a website. Use this when user asks about website availability or HTTP errors.",
        },
    },
    {
        Function: &openai.FunctionDefinition{
            Name:        "traceroute",
            Description: "Trace the network path to a host. Use this when user asks about network routing or path analysis.",
        },
    },
}

userInput := "Проверь доступность google.com"

Процесс выбора:

  1. Модель видит все три инструмента и их Description:
  2. ping: "check network connectivity... Use this when user asks about network reachability"
  3. check_http: "Check HTTP status... Use this when user asks about website availability"
  4. traceroute: "Trace network path... Use this when user asks about routing"

  5. Модель сопоставляет запрос "Проверь доступность google.com" с описаниями:

  6. ping — описание содержит "connectivity" и "reachability" → выбирает этот
  7. check_http — про HTTP статус, не про сетевую доступность
  8. traceroute — про маршрутизацию, не про проверку доступности

  9. Модель возвращает tool call для ping:

    {"name": "ping", "arguments": "{\"host\": \"google.com\"}"}
    

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

userInput := "Проверь, отвечает ли сайт google.com"

// Модель видит те же 3 инструмента
// Сопоставляет:
// - ping: про сетевую доступность → не совсем то
// - check_http: "Use this when user asks about website availability" → ✅ ВЫБИРАЕТ ЭТОТ
// - traceroute: про маршрутизацию → не подходит

// Модель возвращает:
// {"name": "check_http", "arguments": "{\"url\": \"https://google.com\"}"}

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

Что делает LLM, а что делает Runtime?

LLM (модель):

  • Видит описание инструментов в tools[]
  • Генерирует JSON с именем инструмента и аргументами
  • Возвращает структурированный ответ в поле tool_calls

Runtime (ваш код):

  • Парсит tool_calls из ответа модели
  • Валидирует имя инструмента (существует ли он?)
  • Валидирует JSON аргументов (корректен ли?)
  • Выполняет реальную функцию Go
  • Добавляет результат в историю (messages[])
  • Отправляет обновленную историю обратно в модель

Важно: LLM не выполняет код. Она только генерирует JSON с запросом на выполнение. Ваш Runtime выполняет реальный код.

Шаг 4: Валидация (Runtime)

// Проверяем, что инструмент существует
if msg.ToolCalls[0].Function.Name != "ping" {
    return fmt.Errorf("unknown tool: %s", msg.ToolCalls[0].Function.Name)
}

// Валидируем JSON аргументов
var args struct {
    Host string `json:"host"`
}
if err := json.Unmarshal([]byte(msg.ToolCalls[0].Function.Arguments), &args); err != nil {
    return fmt.Errorf("invalid JSON: %v", err)
}

// Проверяем обязательные поля
if args.Host == "" {
    return fmt.Errorf("host is required")
}

Что происходит: Runtime валидирует вызов перед выполнением. Это критично для безопасности.

Шаг 5: Выполнение инструмента

func executePing(host string) string {
    cmd := exec.Command("ping", "-c", "1", host)
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Sprintf("Error: %s", err)
    }
    return string(output)
}

result := executePing(args.Host)  // "PING google.com: 64 bytes from ..."

Что происходит: Runtime выполняет реальную функцию (в данном случае системную команду ping).

Шаг 6: Возврат результата в модель

// Добавляем результат в историю как сообщение с ролью "tool"
messages = append(messages, openai.ChatCompletionMessage{
    Role:       openai.ChatMessageRoleTool,
    Content:    result,  // "PING google.com: 64 bytes from ..."
    ToolCallID: msg.ToolCalls[0].ID,  // Связываем с вызовом
})

// Отправляем обновленную историю в модель снова
resp2, _ := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
    Model:    openai.GPT3Dot5Turbo,
    Messages: messages,  // Теперь включает результат инструмента!
    Tools:    tools,
})

Что происходит: Модель видит результат выполнения инструмента и может:

  • Сформулировать финальный ответ пользователю
  • Вызвать другой инструмент, если нужно
  • Задать уточняющий вопрос

Шаг 7: Финальный ответ

finalMsg := resp2.Choices[0].Message
if len(finalMsg.ToolCalls) == 0 {
    // Это финальный текстовый ответ
    fmt.Println(finalMsg.Content)  // "google.com доступен, время отклика 10ms"
}

Что происходит: Модель видела результат ping и сформулировала понятный ответ для пользователя.

Сквозной протокол: полный запрос и два хода

Теперь разберем полный протокол с точки зрения разработчика агента: где что хранится, как собирается запрос, и как runtime обрабатывает ответы.

Где что хранится в коде агента?

graph TB
    A["System Prompt (в коде/конфиге)"] --> D[ChatCompletionRequest]
    B["Tools Registry (в коде runtime)"] --> D
    C["User Input (от пользователя)"] --> D
    D --> E[LLM API]
    E --> F{Ответ}
    F -->|tool_calls| G[Runtime валидирует]
    G --> H[Runtime выполняет]
    H --> I[Добавляет tool result]
    I --> D
    F -->|content| J[Финальный ответ]

Схема хранения:

  1. System Prompt — хранится в коде агента (константа или конфиг):
  2. Инструкции (Role, Goal, Constraints)
  3. Few-shot примеры (если используются)
  4. SOP (алгоритм действий)

  5. Tools Schema — хранится в registry runtime (не в промпте!):

  6. Определения инструментов (JSON Schema)
  7. Функции-обработчики инструментов
  8. Валидация и выполнение

  9. User Input — приходит от пользователя:

  10. Текущий запрос
  11. История диалога (хранится в messages[])

  12. Tool Results — генерируются runtime'ом:

  13. После выполнения инструмента
  14. Добавляются в messages[] с Role = "tool"

Полный протокол: JSON запросы и ответы (2 хода)

Ход 1: Request с несколькими инструментами

{
  "model": "gpt-3.5-turbo",
  "messages": [
    {
      "role": "system",
      "content": "Ты DevOps инженер. Используй инструменты для проверки сервисов.\n\nПримеры использования:\nUser: \"Проверь статус nginx\"\nAssistant: возвращает tool_call check_status(\"nginx\")\n\nUser: \"Перезапусти сервер\"\nAssistant: возвращает tool_call restart_service(\"web-01\")"
    },
    {
      "role": "user",
      "content": "Проверь статус nginx"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "check_status",
        "description": "Check if a service is running. Use this when user asks about service status.",
        "parameters": {
          "type": "object",
          "properties": {
            "service": {
              "type": "string",
              "description": "Service name"
            }
          },
          "required": ["service"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "restart_service",
        "description": "Restart a systemd service. Use this when user explicitly asks to restart a service.",
        "parameters": {
          "type": "object",
          "properties": {
            "service_name": {
              "type": "string",
              "description": "Service name to restart"
            }
          },
          "required": ["service_name"]
        }
      }
    }
  ],
  "tool_choice": "auto"
}

Где что находится:

  • System Prompt (инструкции + few-shot примеры) → messages[0].content
  • User Inputmessages[1].content
  • Tools Schema (2 инструмента с полными JSON Schema) → отдельное поле tools[]

Response #1 (tool call):

{
  "id": "chatcmpl-abc123",
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [
        {
          "id": "call_xyz789",
          "type": "function",
          "function": {
            "name": "check_status",
            "arguments": "{\"service\": \"nginx\"}"
          }
        }
      ]
    }
  }]
}

Runtime обрабатывает tool call:

  1. Валидирует: tool_calls[0].function.name существует в registry
  2. Парсит: json.Unmarshal(tool_calls[0].function.arguments){"service": "nginx"}
  3. Выполняет: check_status("nginx") → результат: "Service nginx is ONLINE"
  4. Добавляет tool result в messages[]

Ход 2: Request с tool result

{
  "model": "gpt-3.5-turbo",
  "messages": [
    {
      "role": "system",
      "content": "Ты DevOps инженер. Используй инструменты для проверки сервисов.\n\nПримеры использования:\nUser: \"Проверь статус nginx\"\nAssistant: возвращает tool_call check_status(\"nginx\")\n\nUser: \"Перезапусти сервер\"\nAssistant: возвращает tool_call restart_service(\"web-01\")"
    },
    {
      "role": "user",
      "content": "Проверь статус nginx"
    },
    {
      "role": "assistant",
      "content": null,
      "tool_calls": [
        {
          "id": "call_xyz789",
          "type": "function",
          "function": {
            "name": "check_status",
            "arguments": "{\"service\": \"nginx\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "content": "Service nginx is ONLINE",
      "tool_call_id": "call_xyz789"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "check_status",
        "description": "Check if a service is running. Use this when user asks about service status.",
        "parameters": {
          "type": "object",
          "properties": {
            "service": {
              "type": "string",
              "description": "Service name"
            }
          },
          "required": ["service"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "restart_service",
        "description": "Restart a systemd service. Use this when user explicitly asks to restart a service.",
        "parameters": {
          "type": "object",
          "properties": {
            "service_name": {
              "type": "string",
              "description": "Service name to restart"
            }
          },
          "required": ["service_name"]
        }
      }
    }
  ]
}

Где что находится:

  • System Promptmessages[0].content (тот же)
  • User Inputmessages[1].content (тот же)
  • Tool Callmessages[2] (добавлен runtime'ом после первого ответа)
  • Tool Resultmessages[3].content (добавлен runtime'ом после выполнения)
  • Tools Schema → поле tools[] (тот же)

Response #2 (финальный ответ):

{
  "id": "chatcmpl-def456",
  "choices": [{
    "message": {
      "role": "assistant",
      "content": "nginx работает нормально, сервис ONLINE",
      "tool_calls": null
    }
  }]
}

Эволюция messages[] массива (JSON)

До первого запроса:

[
  {"role": "system", "content": "Ты DevOps инженер..."},
  {"role": "user", "content": "Проверь статус nginx"}
]

После первого запроса (runtime добавляет tool call):

[
  {"role": "system", "content": "Ты DevOps инженер..."},
  {"role": "user", "content": "Проверь статус nginx"},
  {
    "role": "assistant",
    "content": null,
    "tool_calls": [{
      "id": "call_xyz789",
      "type": "function",
      "function": {
        "name": "check_status",
        "arguments": "{\"service\": \"nginx\"}"
      }
    }]
  }
]

После выполнения инструмента (runtime добавляет tool result):

[
  {"role": "system", "content": "Ты DevOps инженер..."},
  {"role": "user", "content": "Проверь статус nginx"},
  {
    "role": "assistant",
    "content": null,
    "tool_calls": [{
      "id": "call_xyz789",
      "type": "function",
      "function": {
        "name": "check_status",
        "arguments": "{\"service\": \"nginx\"}"
      }
    }]
  },
  {
    "role": "tool",
    "content": "Service nginx is ONLINE",
    "tool_call_id": "call_xyz789"
  }
]

После второго запроса (модель видит tool result и формулирует ответ):

  • Модель видит весь контекст (system + user + tool call + tool result)
  • Генерирует финальный ответ: "nginx работает нормально, сервис ONLINE"

Примечание: Для реализации на Go см. примеры в Lab 02: Tools и Lab 04: Autonomy

Ключевые моменты для разработчика

  1. System Prompt и Tools Schema — разные вещи:
  2. System Prompt — текст в Messages[0].Content (может содержать few-shot примеры)
  3. Tools Schema — отдельное поле Tools в запросе (JSON Schema)

  4. Few-shot примеры — внутри System Prompt:

  5. Это текст, показывающий модели формат ответа или выбор инструментов
  6. Отличается от Tools Schema (которая описывает структуру инструментов)

  7. Runtime управляет циклом:

  8. Валидирует tool_calls
  9. Выполняет инструменты
  10. Добавляет результаты в messages
  11. Отправляет следующий запрос

  12. Tools не "внутри промпта":

  13. В API они передаются отдельным полем Tools
  14. Модель видит их вместе с промптом, но это разные части запроса

См. как писать инструкции и примеры: Глава 02: Промптинг

Практика: Lab 02: Tools, Lab 04: Autonomy

Почему это не магия?

Ключевые моменты:

  1. Модель видит описание ВСЕХ инструментов — она не "знает" про инструменты из коробки, она видит их Description в JSON Schema. Модель выбирает нужный инструмент, сопоставляя запрос пользователя с описаниями.

  2. Механизм выбора основан на семантике — модель ищет соответствие между:

  3. Запросом пользователя ("Проверь доступность")
  4. Описанием инструмента ("Use this when user asks about network reachability")
  5. Контекстом предыдущих результатов (если есть)

  6. Модель возвращает структурированный JSON — это не текст "я вызову ping", а конкретный tool call с именем инструмента и аргументами

  7. Runtime делает всю работу — парсинг, валидация, выполнение, возврат результата

  8. Модель видит результат — она получает результат как новое сообщение в истории и продолжает работу

Пример выбора между похожими инструментами:

tools := []openai.Tool{
    {
        Function: &openai.FunctionDefinition{
            Name:        "check_service_status",
            Description: "Check if a systemd service is running. Use this for Linux services like nginx, mysql, etc.",
        },
    },
    {
        Function: &openai.FunctionDefinition{
            Name:        "check_http_status",
            Description: "Check HTTP response code of a web service. Use this for checking if a website or API is responding.",
        },
    },
}

// Запрос 1: "Проверь статус nginx"
// Модель выбирает: check_service_status (nginx - это systemd service)

// Запрос 2: "Проверь, отвечает ли сайт example.com"
// Модель выбирает: check_http_status (сайт - это HTTP сервис)

Важно: Если описания инструментов слишком похожи или неясны, модель может выбрать неправильный инструмент. Поэтому Description должен быть конкретным и различимым.

Пример определения инструмента:

tools := []openai.Tool{
    {
        Type: openai.ToolTypeFunction,
        Function: &openai.FunctionDefinition{
            Name:        "ping",
            Description: "Ping a host to check connectivity",
            Parameters: json.RawMessage(`{
                "type": "object",
                "properties": {
                    "host": {
                        "type": "string",
                        "description": "Hostname or IP address to ping"
                    }
                },
                "required": ["host"]
            }`),
        },
    },
}

Важно: Description — это самое важное поле! LLM ориентируется именно по нему, решая, какой инструмент вызвать.

Примеры инструментов в разных доменах

DevOps

// Проверка статуса сервиса
{
    Name: "check_service_status",
    Description: "Check if a systemd service is running",
    Parameters: {"service_name": "string"}
}

// Перезапуск сервиса
{
    Name: "restart_service",
    Description: "Restart a systemd service. WARNING: This will cause downtime.",
    Parameters: {"service_name": "string"}
}

// Чтение логов
{
    Name: "read_logs",
    Description: "Read the last N lines of service logs",
    Parameters: {"service": "string", "lines": "number"}
}

Support

// Получение тикета
{
    Name: "get_ticket",
    Description: "Get ticket details by ID",
    Parameters: {"ticket_id": "string"}
}

// Поиск в базе знаний
{
    Name: "search_kb",
    Description: "Search knowledge base for solutions",
    Parameters: {"query": "string"}
}

// Черновик ответа
{
    Name: "draft_reply",
    Description: "Draft a reply to the ticket",
    Parameters: {"ticket_id": "string", "message": "string"}
}

Data Analytics

// SQL запрос (read-only!)
{
    Name: "sql_select",
    Description: "Execute a SELECT query on the database. ONLY SELECT queries allowed.",
    Parameters: {"query": "string"}
}

// Описание таблицы
{
    Name: "describe_table",
    Description: "Get table schema and column information",
    Parameters: {"table_name": "string"}
}

// Проверка качества данных
{
    Name: "check_data_quality",
    Description: "Check for nulls, duplicates, outliers in a table",
    Parameters: {"table_name": "string"}
}

Security

// Запрос к SIEM
{
    Name: "query_siem",
    Description: "Query security information and event management system",
    Parameters: {"query": "string", "time_range": "string"}
}

// Изоляция хоста (требует подтверждения!)
{
    Name: "isolate_host",
    Description: "CRITICAL: Isolate a host from the network. Requires confirmation.",
    Parameters: {"host": "string"}
}

// Проверка IP репутации
{
    Name: "check_ip_reputation",
    Description: "Check if an IP address is known malicious",
    Parameters: {"ip": "string"}
}

Обработка ошибок инструментов

Если инструмент вернул ошибку, агент должен это увидеть и обработать.

Пример:

// Агент вызывает ping("nonexistent-host")
result := ping("nonexistent-host")
// result = "Error: Name or service not known"

// Добавляем ошибку в историю
messages = append(messages, ChatCompletionMessage{
    Role:    "tool",
    Content: result,  // Модель увидит ошибку!
    ToolCallID: call.ID,
})

// Модель получит ошибку и может:
// 1. Попробовать другой хост
// 2. Сообщить пользователю о проблеме
// 3. Эскалировать проблему

Важно: Ошибка — это тоже результат! Не скрывайте ошибки от модели.

Валидация вызова инструментов

Перед выполнением инструмента нужно валидировать аргументы.

Пример валидации:

func executeTool(name string, args json.RawMessage) (string, error) {
    switch name {
    case "restart_service":
        var params struct {
            ServiceName string `json:"service_name"`
        }
        if err := json.Unmarshal(args, &params); err != nil {
            return "", fmt.Errorf("invalid args: %v", err)
        }

        // Валидация
        if params.ServiceName == "" {
            return "", fmt.Errorf("service_name is required")
        }

        // Проверка безопасности
        if params.ServiceName == "critical-db" {
            return "", fmt.Errorf("Cannot restart critical service without confirmation")
        }

        return restartService(params.ServiceName), nil
    }
    return "", fmt.Errorf("unknown tool: %s", name)
}

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

Ошибка 1: Модель не генерирует tool_call

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

Причина:

  • Модель не обучена на function calling
  • Плохое описание инструмента (Description неясное)
  • Temperature > 0 (слишком случайно)

Решение:

// ХОРОШО: Используйте модель с поддержкой tools
// Проверьте через Lab 00: Capability Check

// ХОРОШО: Улучшите Description
Description: "Check the status of a server by hostname. Use this when user asks about server status or availability."

// ХОРОШО: Temperature = 0
Temperature: 0,  // Детерминированное поведение

Ошибка 2: Сломанный JSON в аргументах

Симптом: json.Unmarshal возвращает ошибку. JSON в аргументах некорректен.

Причина: Модель генерирует некорректный JSON (пропущены скобки, неправильный формат).

Решение:

// ХОРОШО: Валидация JSON перед парсингом
if !json.Valid([]byte(call.Function.Arguments)) {
    return "Error: Invalid JSON", nil
}

// ХОРОШО: Temperature = 0 для детерминированного JSON
Temperature: 0,

Ошибка 3: Галлюцинации инструментов

Симптом: Агент вызывает несуществующий инструмент. Модель генерирует tool_call с именем, которого нет в списке.

Причина: Модель не видит четкий список доступных инструментов или список слишком большой.

Решение:

// ХОРОШО: Валидация имени инструмента перед выполнением
allowedFunctions := map[string]bool{
    "get_server_status": true,
    "ping":              true,
}
if !allowedFunctions[call.Function.Name] {
    return "Error: Unknown function", nil
}

Примечание: Если у вас большое пространство инструментов (сотни или тысячи), используйте tool retrieval — динамически выбирайте только релевантные инструменты перед планированием. Подробнее см. Главу 06: RAG.

Ошибка 4: Плохое описание инструмента

Симптом: Модель выбирает неправильный инструмент или не выбирает его вообще.

Причина: Description слишком общее или не содержит ключевых слов из запроса пользователя.

Решение:

// ПЛОХО
Description: "Ping a host"

// ХОРОШО
Description: "Ping a host to check network connectivity. Use this when user asks about network reachability or connectivity."

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

Упражнение 1: Создайте инструмент

Создайте инструмент check_disk_usage, который проверяет использование диска:

tools := []openai.Tool{
    {
        Type: openai.ToolTypeFunction,
        Function: &openai.FunctionDefinition{
            Name:        "check_disk_usage",
            Description: "...",  // Ваш код здесь
            Parameters:  json.RawMessage(`{
                // Ваш код здесь
            }`),
        },
    },
}

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

  • Description содержит ключевые слова: "disk", "usage", "space"
  • JSON Schema корректна
  • Обязательные поля указаны в required

Упражнение 2: Валидация аргументов

Реализуйте функцию валидации аргументов для инструмента:

func validateToolCall(call openai.ToolCall) error {
    // Проверьте имя инструмента
    // Проверьте валидность JSON
    // Проверьте обязательные поля
}

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

  • Функция возвращает ошибку, если имя инструмента неизвестно
  • Функция возвращает ошибку, если JSON некорректен
  • Функция возвращает ошибку, если отсутствуют обязательные поля

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

Сдано:

  • Description конкретное и понятное (содержит ключевые слова)
  • JSON Schema корректна
  • Обязательные поля указаны в required
  • Валидация аргументов реализована
  • Ошибки обрабатываются и возвращаются агенту
  • Критические инструменты требуют подтверждения
  • Модель успешно генерирует tool_call

Не сдано:

  • Модель не генерирует tool_call (плохое описание или неподходящая модель)
  • JSON в аргументах сломан (нет валидации)
  • Модель вызывает несуществующий инструмент (нет валидации имени)
  • Description слишком общее (модель не может выбрать правильный инструмент)

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

Полный пример: от начала до конца

Вот минимальный рабочий пример агента с одним инструментом:

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "os"

    "github.com/sashabaranov/go-openai"
)

func main() {
    // 1. Инициализация клиента
    config := openai.DefaultConfig(os.Getenv("OPENAI_API_KEY"))
    if baseURL := os.Getenv("OPENAI_BASE_URL"); baseURL != "" {
        config.BaseURL = baseURL // Для локальных моделей
    }
    client := openai.NewClientWithConfig(config)

    // 2. Определение инструмента
    tools := []openai.Tool{
        {
            Type: openai.ToolTypeFunction,
            Function: &openai.FunctionDefinition{
                Name:        "check_status",
                Description: "Check the status of a server by hostname. Use this when user asks about server status.",
                Parameters: json.RawMessage(`{
                    "type": "object",
                    "properties": {
                        "hostname": {
                            "type": "string",
                            "description": "Server hostname"
                        }
                    },
                    "required": ["hostname"]
                }`),
            },
        },
    }

    // 3. Запуск агента
    ctx := context.Background()
    userInput := "Проверь статус сервера web-01"

    messages := []openai.ChatCompletionMessage{
        {Role: "system", Content: "Ты DevOps инженер. Используй инструменты для проверки сервисов."},
        {Role: "user", Content: userInput},
    }

    // 4. Цикл агента
    for i := 0; i < 10; i++ {
        resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
            Model:       openai.GPT3Dot5Turbo,
            Messages:    messages,
            Tools:       tools,
            Temperature: 0, // Детерминированное поведение
        })
        if err != nil {
            fmt.Printf("Error: %v\n", err)
            return
        }

        msg := resp.Choices[0].Message
        messages = append(messages, msg)

        // 5. Проверка: это tool_call или финальный ответ?
        if len(msg.ToolCalls) == 0 {
            // Финальный ответ
            fmt.Printf("Agent: %s\n", msg.Content)
            break
        }

        // 6. Выполнение инструмента (Runtime)
        for _, toolCall := range msg.ToolCalls {
            if toolCall.Function.Name != "check_status" {
                result := fmt.Sprintf("Error: Unknown tool %s", toolCall.Function.Name)
                messages = append(messages, openai.ChatCompletionMessage{
                    Role:       openai.ChatMessageRoleTool,
                    Content:    result,
                    ToolCallID: toolCall.ID,
                })
                continue
            }

            // Парсим аргументы
            var args struct {
                Hostname string `json:"hostname"`
            }
            if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
                result := fmt.Sprintf("Error: Invalid JSON: %v", err)
                messages = append(messages, openai.ChatCompletionMessage{
                    Role:       openai.ChatMessageRoleTool,
                    Content:    result,
                    ToolCallID: toolCall.ID,
                })
                continue
            }

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

            // Добавляем результат в историю
            messages = append(messages, openai.ChatCompletionMessage{
                Role:       openai.ChatMessageRoleTool,
                Content:    result,
                ToolCallID: toolCall.ID,
            })
        }
    }
}

// Реальная функция, которую вызывает Runtime
func checkStatus(hostname string) string {
    // В реальном приложении здесь был бы вызов API или команда
    return fmt.Sprintf("Server %s is ONLINE", hostname)
}

Что происходит:

  1. Агент получает запрос "Проверь статус сервера web-01"
  2. Модель видит инструмент check_status и генерирует tool_call
  3. Runtime парсит tool_call, валидирует и выполняет checkStatus("web-01")
  4. Результат добавляется в историю
  5. Модель видит результат и формулирует финальный ответ

Запуск:

export OPENAI_API_KEY="your-key"
export OPENAI_BASE_URL="http://localhost:1234/v1"  # Для локальных моделей
go run main.go

Что дальше?

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


Навигация: ← Промптинг | Оглавление | Автономность →