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?¶
- Вы описываете функцию в формате JSON Schema
- LLM видит описание и решает: "Мне нужно вызвать эту функцию"
- LLM генерирует JSON с именем функции и аргументами
- Ваш код парсит JSON и выполняет реальную функцию
- Результат возвращается в 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: "gpt-4o-mini",
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"
Процесс выбора:
-
Модель видит все три инструмента и их
Description:ping: "check network connectivity... Use this when user asks about network reachability"check_http: "Check HTTP status... Use this when user asks about website availability"traceroute: "Trace network path... Use this when user asks about routing"
-
Модель сопоставляет запрос "Проверь доступность google.com" с описаниями:
-
ping— описание содержит "connectivity" и "reachability" → выбирает этот -
check_http— про HTTP статус, не про сетевую доступность -
traceroute— про маршрутизацию, не про проверку доступности
-
-
Модель возвращает tool call для
ping:
Пример с другим запросом:
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: "gpt-4o-mini",
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[Финальный ответ]
Схема хранения:
-
System Prompt — хранится в коде агента (константа или конфиг):
- Инструкции (Role, Goal, Constraints)
- Few-shot примеры (если используются)
- SOP (алгоритм действий)
-
Tools Schema — хранится в registry runtime (не в промпте!):
- Определения инструментов (JSON Schema)
- Функции-обработчики инструментов
- Валидация и выполнение
-
User Input — приходит от пользователя:
- Текущий запрос
- История диалога (хранится в
messages[])
-
Tool Results — генерируются runtime'ом:
- После выполнения инструмента
- Добавляются в
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 Input →
messages[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:
- Валидирует:
tool_calls[0].function.nameсуществует в registry - Парсит:
json.Unmarshal(tool_calls[0].function.arguments)→{"service": "nginx"} - Выполняет:
check_status("nginx")→ результат:"Service nginx is ONLINE" - Добавляет 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 Prompt →
messages[0].content(тот же) - User Input →
messages[1].content(тот же) - Tool Call →
messages[2](добавлен runtime'ом после первого ответа) - Tool Result →
messages[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
Ключевые моменты для разработчика¶
-
System Prompt и Tools Schema — разные вещи:
- System Prompt — текст в
Messages[0].Content(может содержать few-shot примеры) - Tools Schema — отдельное поле
Toolsв запросе (JSON Schema)
- System Prompt — текст в
-
Few-shot примеры — внутри System Prompt:
- Это текст, показывающий модели формат ответа или выбор инструментов
- Отличается от Tools Schema (которая описывает структуру инструментов)
-
Runtime управляет циклом:
- Валидирует
tool_calls - Выполняет инструменты
- Добавляет результаты в
messages - Отправляет следующий запрос
- Валидирует
-
Tools не "внутри промпта":
- В API они передаются отдельным полем
Tools - Модель видит их вместе с промптом, но это разные части запроса
- В API они передаются отдельным полем
См. как писать инструкции и примеры: Глава 02: Промптинг
Практика: Lab 02: Tools, Lab 04: Autonomy
Что происходит на деле?¶
Коротко:
-
Модель видит описание ВСЕХ инструментов — она не "знает" про инструменты из коробки, она видит их
Descriptionв JSON Schema. Модель выбирает нужный инструмент, сопоставляя запрос пользователя с описаниями. -
Механизм выбора основан на семантике — модель ищет соответствие между:
- Запросом пользователя ("Проверь доступность")
- Описанием инструмента ("Use this when user asks about network reachability")
- Контекстом предыдущих результатов (если есть)
-
Модель возвращает структурированный JSON — это не текст "я вызову ping", а конкретный tool call с именем инструмента и аргументами
-
Runtime делает всю работу — парсинг, валидация, выполнение, возврат результата
-
Модель видит результат — она получает результат как новое сообщение в истории и продолжает работу
Пример выбора между похожими инструментами:
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, ¶ms); 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)
}
Repair аргументов tool call (если JSON/схема невалидны)¶
В продакшене это случается регулярно: модель иногда возвращает:
- синтаксически невалидный JSON (пропущена
}, кавычки и т.д.), - валидный JSON, но не проходящий вашу JSON Schema (не те поля/типы).
Если это случилось, не выполняйте инструмент в таком виде. Вместо этого добавьте repair-шаг: отдельный вызов LLM (или отдельный режим/модель), который получает:
- имя инструмента,
- JSON Schema аргументов,
- исходные аргументы (как строку),
и возвращает только исправленный JSON, без объяснений.
Скелет логики:
if !json.Valid([]byte(call.Function.Arguments)) {
// 1) repairArgs := repairWithLLM(call.Function.Arguments, toolSchema)
// 2) повторить валидацию (json.Valid + schema)
// 3) если всё ок — выполнять, иначе вернуть понятную ошибку и не выполнять tool
}
Ключевое правило: repair-шаг не меняет намерение, он чинит формат под схему.
Стратегия выбора модели для разных этапов¶
Зачем нужны разные модели?¶
В примерах выше мы использовали одну модель (GPT3Dot5Turbo) для всех этапов: и для выбора инструмента, и для анализа результатов. В учебном примере это нормально. В продакшене — нет.
Разные этапы работы агента требуют разных способностей:
| Этап | Что делает модель | Нужная способность | Подходящая модель |
|---|---|---|---|
| Tool Selection | Сопоставляет запрос с описаниями инструментов | Следование инструкциям, structured output | Быстрая и дешёвая (GPT-4o-mini, Claude Haiku) |
| Argument Generation | Заполняет JSON Schema аргументов | JSON generation, следование схеме | Быстрая и дешёвая |
| Result Analysis | Анализирует результат инструмента, формулирует ответ | Рассуждение, синтез информации | Мощная (GPT-4o, Claude Sonnet) |
| Complex Planning | Решает, какие инструменты вызвать и в каком порядке | Планирование, multi-step reasoning | Мощная |
Суть: Tool selection — это простая задача классификации. Анализ результатов — это задача рассуждения. Платить за GPT-4o на этапе "выбрать ping из трёх инструментов" — расточительство.
Как это работает — Магия vs Реальность¶
Магия (как обычно объясняют):
"Используйте лучшую модель для всего. Она справится."
Реальность (как на самом деле):
Агент делает 5-15 вызовов LLM за одну задачу. Если каждый вызов стоит $0.01 (GPT-4o) вместо $0.0002 (GPT-4o-mini), разница — 50x за задачу. При 10 000 задач в день это $1 000 vs $20.
Практика: используем дешёвую модель там, где задача простая, мощную — где нужно думать.
Реализация: ModelRouter¶
// ModelSelector выбирает модель в зависимости от этапа работы агента.
type ModelSelector struct {
ToolCallModel string // Модель для выбора инструмента и генерации аргументов
AnalysisModel string // Модель для анализа результатов и финального ответа
ComplexModel string // Модель для сложных задач (планирование, multi-tool)
}
func NewModelSelector() *ModelSelector {
return &ModelSelector{
ToolCallModel: "gpt-4o-mini", // Быстрая, дешёвая, хорошо следует инструкциям
AnalysisModel: "gpt-4o", // Мощная, хорошо рассуждает
ComplexModel: "gpt-4o", // Для сложного планирования
}
}
// SelectModel определяет, какую модель использовать на текущей итерации.
// needsToolCall: ожидаем ли мы вызов инструмента на этом шаге.
// toolCount: сколько инструментов доступно (влияет на сложность выбора).
func (ms *ModelSelector) SelectModel(needsToolCall bool, toolCount int) string {
if !needsToolCall {
// Этап анализа результатов — нужна мощная модель
return ms.AnalysisModel
}
if toolCount > 20 {
// Много инструментов — сложный выбор, нужна мощная модель
return ms.ComplexModel
}
// Обычный tool call — хватит дешёвой модели
return ms.ToolCallModel
}
Как определить этап?¶
Runtime знает, на каком этапе находится агент. Правило простое:
func isToolCallExpected(messages []openai.ChatCompletionMessage) bool {
if len(messages) == 0 {
return true // Первый вызов — скорее всего нужен tool call
}
lastMsg := messages[len(messages)-1]
// Если последнее сообщение — результат инструмента,
// модель будет анализировать результат.
// Но она МОЖЕТ вызвать ещё один инструмент.
if lastMsg.Role == openai.ChatMessageRoleTool {
return true // Может быть и tool call, и финальный ответ
}
return true
}
Важный нюанс: На практике мы не всегда знаем заранее, вызовет ли модель инструмент или ответит текстом. Поэтому часто используют более простую стратегию — переключение после N tool calls:
// Простая стратегия: первые N итераций — дешёвая модель,
// финальная итерация (когда tool calls закончились) — мощная.
func (ms *ModelSelector) SelectByIteration(iteration int, lastResponseHadToolCalls bool) string {
if lastResponseHadToolCalls {
// Ещё выполняем инструменты — дешёвая модель
return ms.ToolCallModel
}
// Инструменты закончились — переключаемся на мощную для финального ответа
return ms.AnalysisModel
}
Использование в Agent Loop¶
selector := NewModelSelector()
lastHadToolCalls := true // Первая итерация — ожидаем tool call
for i := 0; i < maxIterations; i++ {
model := selector.SelectByIteration(i, lastHadToolCalls)
resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: model, // Разная модель на разных этапах!
Messages: messages,
Tools: tools,
})
if err != nil {
return fmt.Errorf("LLM call failed: %w", err)
}
msg := resp.Choices[0].Message
messages = append(messages, msg)
lastHadToolCalls = len(msg.ToolCalls) > 0
if !lastHadToolCalls {
fmt.Printf("[model=%s] Final: %s\n", model, msg.Content)
break
}
fmt.Printf("[model=%s] Tool calls: %d\n", model, len(msg.ToolCalls))
// ... выполнение инструментов ...
}
Когда НЕ нужны разные модели¶
- Прототип / MVP: Одна модель проще. Оптимизируйте потом.
- Мало вызовов: Если агент делает 1-2 вызова в день — экономия не оправдает сложность.
- Критичная согласованность: Некоторые задачи требуют, чтобы одна модель "помнила" свои рассуждения между итерациями. Смена модели может нарушить цепочку рассуждений.
Подробнее о стоимости и оптимизации см. Главу 20: Cost & Latency Engineering.
Типовые ошибки¶
Ошибка 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слишком общее (модель не может выбрать правильный инструмент)
Связь с другими главами¶
- Физика LLM: Почему модель выбирает tool call вместо текста, см. Главу 01: Физика LLM
- Промптинг: Как описать инструменты так, чтобы модель их правильно использовала, см. Главу 02: Промптинг
- Цикл: Как результаты инструментов возвращаются в модель, см. Главу 04: Автономность
- MCP: Стандартный протокол подключения инструментов к агентам — Model Context Protocol. Подробности в Главе 18: Протоколы Инструментов
Полный пример: от начала до конца¶
Вот минимальный рабочий пример агента с одним инструментом:
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: "gpt-4o-mini",
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)
}
Что происходит:
- Агент получает запрос "Проверь статус сервера web-01"
- Модель видит инструмент
check_statusи генерирует tool_call - Runtime парсит tool_call, валидирует и выполняет
checkStatus("web-01") - Результат добавляется в историю
- Модель видит результат и формулирует финальный ответ
Запуск:
export OPENAI_API_KEY="your-key"
export OPENAI_BASE_URL="http://localhost:1234/v1" # Для локальных моделей
go run main.go
Что дальше?¶
После изучения инструментов переходите к:
- 04. Автономность и Циклы — как агент работает в цикле