Function Calling: как LLM учится вызывать инструменты

opensourceaillmfunction-callingtoolsit
← Back to Blog

Введение: LLM без рук

LLM может генерировать текст, но не может выполнить действие. Function Calling решает эту проблему.

Факт: GPT-4 может вызвать API, выполнить поиск, запустить код — но только если вы правильно оформите function schema.


Что такое Function Calling?

Проблема без Function Calling

Без Function Calling:
  User: "Какая погода в Москве?"
  LLM: "Я не знаю погоду, я языковая модель"
  ✗ Нет доступа к внешнему миру
  ✗ Нет выполнения действий
  ✗ Только генерация текста

С Function Calling:
  User: "Какая погода в Москве?"
  LLM: { "function": "get_weather", "args": {"city": "Москва"} }
  System: { "result": {"temp": 25, "condition": "sunny"} }
  LLM: "В Москве сейчас 25°C, солнечно"
  ✓ Внешние данные
  ✓ Выполнение действий
  ✓ Интеграция с API

Architecture

User Query
    |
    v
[1] LLM с function schemas
    |
    v
[2] LLM решает: вызвать функцию?
    |
    ├─ Нет → прямой ответ
    └─ Да → JSON с function + args
              |
              v
         [3] System executes function
              |
              v
         [4] System sends result back
              |
              v
         [5] LLM генерирует ответ
              с результатом

Уровень 1: Function Schema

Что такое Function Schema?

Function Schema = описание функции для LLM

{
  "name": "get_weather",
  "description": "Get current weather for a location",
  "parameters": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "City name, e.g. 'Moscow'"
      },
      "units": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "Temperature units"
      }
    },
    "required": ["city"]
  }
}

Schema Rules

1. name:
   - snake_case или camelCase
   - Уникальное в системе
   - Описательное

2. description:
   - Четкое описание что делает
   - LLM использует для решения

3. parameters:
   - JSON Schema format
   - type: object
   - properties: { key: { type, description } }
   - required: [keys]

4. types:
   - string, number, boolean, array, object
   - enum для фиксированных значений

Complex Schema

{
  "name": "create_ticket",
  "description": "Create a support ticket",
  "parameters": {
    "type": "object",
    "properties": {
      "title": {
        "type": "string",
        "description": "Ticket title"
      },
      "priority": {
        "type": "string",
        "enum": ["low", "medium", "high", "critical"]
      },
      "assignee": {
        "type": "string",
        "description": "Team to assign: 'backend', 'frontend', 'devops'"
      },
      "tags": {
        "type": "array",
        "items": { "type": "string" }
      },
      "metadata": {
        "type": "object",
        "properties": {
          "user_id": { "type": "string" },
          "browser": { "type": "string" }
        }
      }
    },
    "required": ["title", "priority"]
  }
}

Уровень 2: OpenAI Function Calling

API Format

import openai

response = openai.ChatCompletion.create(
    model='gpt-4',
    messages=[
        {'role': 'user', 'content': 'Какая погода в Москве?'}
    ],
    functions=[{
        'name': 'get_weather',
        'description': 'Get current weather',
        'parameters': {
            'type': 'object',
            'properties': {
                'city': {'type': 'string'}
            },
            'required': ['city']
        }
    }],
    function_call='auto'  # или {'name': 'get_weather'}
)

# Response:
# {
#   'role': 'assistant',
#   'content': None,
#   'function_call': {
#     'name': 'get_weather',
#     'arguments': '{"city": "Москва"}'
#   }
# }

Multi-turn with Function Result

# Step 1: Get function call
response1 = openai.ChatCompletion.create(
    model='gpt-4',
    messages=[{'role': 'user', 'content': 'Какая погода в Москве?'}],
    functions=[weather_schema],
)

# LLM returns: function_call: get_weather({"city": "Москва"})

# Step 2: Execute function
result = get_weather("Москва")
# {"temp": 25, "condition": "sunny"}

# Step 3: Send result back
response2 = openai.ChatCompletion.create(
    model='gpt-4',
    messages=[
        {'role': 'user', 'content': 'Какая погода в Москве?'},
        {
            'role': 'assistant',
            'function_call': {
                'name': 'get_weather',
                'arguments': '{"city": "Москва"}'
            }
        },
        {
            'role': 'function',
            'name': 'get_weather',
            'content': json.dumps(result)
        }
    ],
    functions=[weather_schema],
)

# LLM returns: "В Москве сейчас 25°C, солнечно"

Уровень 3: Multiple Functions

Function Router

functions = [
    {
        'name': 'get_weather',
        'description': 'Get weather for a city',
        'parameters': {
            'type': 'object',
            'properties': {
                'city': {'type': 'string'}
            },
            'required': ['city']
        }
    },
    {
        'name': 'search_web',
        'description': 'Search the web for information',
        'parameters': {
            'type': 'object',
            'properties': {
                'query': {'type': 'string'},
                'max_results': {'type': 'integer', 'default': 5}
            },
            'required': ['query']
        }
    },
    {
        'name': 'calculate',
        'description': 'Calculate math expression',
        'parameters': {
            'type': 'object',
            'properties': {
                'expression': {'type': 'string'}
            },
            'required': ['expression']
        }
    },
    {
        'name': 'translate',
        'description': 'Translate text between languages',
        'parameters': {
            'type': 'object',
            'properties': {
                'text': {'type': 'string'},
                'source_lang': {'type': 'string'},
                'target_lang': {'type': 'string'}
            },
            'required': ['text', 'target_lang']
        }
    }
]

# LLM выбирает функцию на основе контекста

Function Selection Logic

User: "Какая погода?"
  → LLM: get_weather({"city": ...})

User: "Найди новости про AI"
  → LLM: search_web({"query": "новости AI"})

User: "Сколько будет 25 * 48?"
  → LLM: calculate({"expression": "25 * 48"})

User: "Переведи 'hello' на французский"
  → LLM: translate({"text": "hello", "target_lang": "fr"})

User: "Привет!"
  → LLM: прямой ответ (без функции)

Уровень 4: Local LLM Function Calling

Ollama Function Calling

import ollama

response = ollama.chat(
    model='llama3.1',
    messages=[{'role': 'user', 'content': 'Какая погода в Москве?'}],
    tools=[{
        'type': 'function',
        'function': {
            'name': 'get_weather',
            'description': 'Get current weather',
            'parameters': {
                'type': 'object',
                'properties': {
                    'city': {'type': 'string'}
                },
                'required': ['city']
            }
        }
    }]
)

# Response:
# {
#   'message': {
#     'role': 'assistant',
#     'content': '',
#     'tool_calls': [{
#       'function': {
#         'name': 'get_weather',
#         'arguments': {'city': 'Москва'}
#       }
#     }]
#   }
# }

llama.cpp Function Calling

from llama_cpp import Llama

llama = Llama(
    model_path="./llama3.gguf",
    n_ctx=2048,
    logits_all=True
)

output = llama.create_chat_completion(
    messages=[{'role': 'user', 'content': 'Какая погода в Москве?'}],
    tools=[weather_tool_spec],
    stream=False
)

# Response:
# {
#   'choices': [{
#     'message': {
#       'role': 'assistant',
#       'tool_calls': [{
#         'function': {
#           'name': 'get_weather',
#           'arguments': '{"city": "Москва"}'
#         }
#       }]
#     }
#   }]
# }

Уровень 5: Tool Use (OpenAI Format)

Новый Tool Use Format

# OpenAI использует новый format (2024+)
response = openai.chat.completions.create(
    model='gpt-4',
    messages=[
        {'role': 'user', 'content': 'Какая погода в Москве?'}
    ],
    tools=[{
        'type': 'function',
        'function': {
            'name': 'get_weather',
            'description': 'Get current weather',
            'parameters': {
                'type': 'object',
                'properties': {
                    'city': {'type': 'string'}
                },
                'required': ['city']
            }
        }
    }]
)

# Response:
tool_call = response.choices[0].message.tool_calls[0]
print(tool_call.function.name)      # "get_weather"
print(tool_call.function.arguments) # '{"city": "Москва"}'

Handling Multiple Tool Calls

# LLM может вызвать несколько функций одновременно
response = openai.chat.completions.create(
    model='gpt-4',
    messages=[
        {'role': 'user', 'content': 'Какая погода в Москве и Париже?'}
    ],
    tools=[weather_tool],
)

# Response:
# tool_calls: [
#   {function: {name: "get_weather", arguments: '{"city": "Москва"}'}},
#   {function: {name: "get_weather", arguments: '{"city": "Париж"}'}}
# ]

# Execute both in parallel
results = await asyncio.gather(
    get_weather("Москва"),
    get_weather("Париж")
)

# Send results back
messages = [
    *response_messages,
    {
        'role': 'tool',
        'tool_call_id': tool_call.id,
        'content': json.dumps(result1)
    },
    {
        'role': 'tool',
        'tool_call_id': tool_call2.id,
        'content': json.dumps(result2)
    }
]

response2 = openai.chat.completions.create(
    model='gpt-4',
    messages=messages,
    tools=[weather_tool],
)

Уровень 6: Agent Loop

Simple Agent Loop

def agent_loop(messages, tools, max_turns=5):
    for turn in range(max_turns):
        # Step 1: Ask LLM
        response = llm.chat(
            messages=messages,
            tools=tools
        )
        
        # Step 2: Check if LLM wants to call a tool
        if response.tool_calls:
            for tool_call in response.tool_calls:
                # Execute the tool
                result = execute_tool(
                    tool_call.function.name,
                    json.loads(tool_call.function.arguments)
                )
                
                # Add result to messages
                messages.append({
                    'role': 'tool',
                    'tool_call_id': tool_call.id,
                    'content': json.dumps(result)
                })
        else:
            # No more tool calls, return final answer
            return response.message.content
    
    return "Max turns reached"

def execute_tool(name, args):
    tool_map = {
        'get_weather': get_weather,
        'search_web': search_web,
        'calculate': calculate,
    }
    return tool_map[name](**args)

ReAct Pattern

ReAct = Reasoning + Acting

Цикл:
  Thought → Action → Observation → Thought → Action → Observation → Answer

Пример:
  Thought: Мне нужно узнать погоду в Москве
  Action: get_weather(city="Москва")
  Observation: {"temp": 25, "condition": "sunny"}
  Thought: Теперь я могу ответить
  Answer: В Москве сейчас 25°C, солнечно

Уровень 7: Structured Output

JSON Mode

# OpenAI JSON mode - всегда возвращает JSON
response = openai.chat.completions.create(
    model='gpt-4-turbo',
    messages=[
        {'role': 'user', 'content': 'Назови 3 столицы Европы'}
    ],
    response_format={
        'type': 'json_schema',
        'json_schema': {
            'name': 'capitals',
            'schema': {
                'type': 'object',
                'properties': {
                    'capitals': {
                        'type': 'array',
                        'items': {'type': 'string'}
                    }
                },
                'required': ['capitals']
            }
        }
    }
)

# Результат:
# {"capitals": ["Париж", "Берлин", "Мадрид"]}

Pydantic Integration

from pydantic import BaseModel, Field
from typing import List

class WeatherResponse(BaseModel):
    city: str
    temperature: float
    units: str
    conditions: str

# Using litellm or other libraries
response = parse_response(WeatherResponse, llm_response)
# WeatherResponse(
#   city="Москва",
#   temperature=25.0,
#   units="celsius",
#   conditions="sunny"
# )

Уровень 8: Best Practices

Schema Design Tips

✅ ДЕЛАЙ:
- Пиши подробные descriptions для каждой функции
- Используй enum для фиксированных значений
- Добавляй examples в descriptions
- Валидируй args на своей стороне
- Обрабатывай ошибки gracefully

❌ НЕ ДЕЛАЙ:
- Не делай слишком много функций (до 20-30)
- Не используй ambiguous names
- Не передавай sensitive data через tools
- Не забывай про error handling

Prompt Engineering for Tools

# Good system prompt
system_prompt = """
Вы помощник с доступом к инструментам.

Доступные инструменты:
- get_weather: Получить погоду для города
- search_web: Поиск в интернете
- calculate: Математические вычисления

Если пользователь спрашивает о погоде, используйте get_weather.
Если вопрос не требует инструмента, ответьте напрямую.
"""

Уровень 9: Evaluation & Testing

Testing Function Calling

import unittest

class TestFunctionCalling(unittest.TestCase):
    
    def test_weather_function(self):
        messages = [
            {'role': 'user', 'content': 'Какая погода в Москве?'}
        ]
        response = llm.chat(messages=messages, tools=[weather_tool])
        
        self.assertIsNotNone(response.tool_calls)
        self.assertEqual(
            response.tool_calls[0].function.name,
            'get_weather'
        )
    
    def test_no_tool_needed(self):
        messages = [
            {'role': 'user', 'content': 'Привет, как дела?'}
        ]
        response = llm.chat(messages=messages, tools=[weather_tool])
        
        self.assertIsNone(response.tool_calls)

if __name__ == '__main__':
    unittest.main()

Benchmarking

Metrics для Function Calling:
1. Tool Accuracy: % правильных вызовов функций
2. Arg Accuracy: % правильных аргументов
3. Latency: время до первого tool call
4. Multi-turn: % успешных multi-turn сценариев
5. Hallucination Rate: % ложных вызовов

Уровень 10: Future of Function Calling

Multimodal Tools

Будущее:
- Image generation tools
- Code execution tools
- Video analysis tools
- Audio processing tools
- Multi-modal agents

Пример:
  User: "Создай изображение заката"
  LLM: { "function": "generate_image", "args": {"prompt": "sunset"} }
  
  User: "Проанализируй этот скриншот"
  LLM: { "function": "analyze_image", "args": {"image": "..."} }

Agentic Workflows

Agent 1: Planner - разбивает задачу на подзадачи
Agent 2: Researcher - ищет информацию
Agent 3: Coder - пишет код
Agent 4: Reviewer - проверяет результат

Координация через:
- Shared memory
- Tool registry
- Communication protocols

Заключение

Function Calling — это мост между LLM и реальным миром.

Ключевые выводы:

  1. Function Schema — основа всего
  2. Local LLM (llama3, mistral) поддерживают FC
  3. Agent Loop — основной паттерн использования
  4. Structured Output — следующий шаг
  5. Будущее за multi-agent системами

Ресурсы: