Function Calling: даём LLM инструменты

Function calling — ключ к AI-агентам. Три провайдера, боевые паттерны.

📊 Средний ⏱ 12 мин

# 1. БАЗОВЫЙ FUNCTION CALLING — OPENAI

import openai
import json
from typing import Any

client = openai.OpenAI()

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current weather for a city",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["city"]
        }
    }
}]

response = client.chat.completions.create(
    model="gpt-4o", messages=[{"role": "user", "content": "Какая погода в Москве?"}],
    tools=tools, tool_choice="auto"
)

tool_call = response.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
print(f"Called: {tool_call.function.name} with {args}")

# 2. ANTHROPIC TOOL USE — С ВАЛИДАЦИЕЙ

import anthropic
from pydantic import BaseModel, Field

ac = anthropic.Anthropic()

class SearchParams(BaseModel):
    query: str = Field(..., min_length=1, max_length=500)
    limit: int = Field(default=5, ge=1, le=20)

claude_tools = [{
    "name": "web_search",
    "description": "Search the web for current information",
    "input_schema": SearchParams.model_json_schema(),
}]

resp = ac.messages.create(
    model="claude-sonnet-4-20250514", max_tokens=1024,
    tools=claude_tools,
    messages=[{"role": "user", "content": "Найди последние новости про AI-агентов"}],
)

for block in resp.content:
    if block.type == "tool_use":
        validated = SearchParams(**block.input)
        print(f"Validated search: {validated.query}, limit={validated.limit}")

# 3. ПАРАЛЛЕЛЬНЫЕ ВЫЗОВЫ И RETRY С ВАЛИДАЦИЕЙ

import asyncio

async def call_with_retry(client, messages, tools, max_retries=3):
    for attempt in range(max_retries):
        try:
            resp = await client.chat.completions.create(
                model="gpt-4o", messages=messages,
                tools=tools, tool_choice="required",
            )
            tc = resp.choices[0].message.tool_calls[0]
            args = json.loads(tc.function.arguments)
            assert "city" in args, "Missing required param"
            return args
        except Exception as e:
            print(f"Retry {attempt+1}/{max_retries}: {e}")
    raise RuntimeError("Failed after all retries")

# 4. ПАРАЛЛЕЛЬНЫЕ FUNCTION CALLS — ОДНОВРЕМЕННО НЕСКОЛЬКО ИНСТРУМЕНТОВ

multi_tools = [
    {"type": "function", "function": {
        "name": "get_weather", "parameters": {
            "type": "object", "properties": {"city": {"type": "string"}},
            "required": ["city"]
    }}},
    {"type": "function", "function": {
        "name": "get_time", "parameters": {
            "type": "object", "properties": {"timezone": {"type": "string"}},
            "required": ["timezone"]
    }}},
]

def execute_tools(tool_calls):
    results = {}
    for tc in tool_calls:
        name = tc.function.name
        args = json.loads(tc.function.arguments)
        if name == "get_weather":
            results[name] = {"temp": 22, "condition": "sunny", "city": args["city"]}
        elif name == "get_time":
            results[name] = {"time": "14:30", "tz": args["timezone"]}
    return results

resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Погода в Москве и время в Токио?"}],
    tools=multi_tools,
)
results = execute_tools(resp.choices[0].message.tool_calls)
print("Parallel results:", results)

# 5. ПРОДАКШЕН-АГЕНТ С FULL TOOL LOOP

class ToolAgent:
    def __init__(self, client, tools, handlers):
        self.client = client
        self.tools = tools
        self.handlers = handlers

    def run(self, user_msg, max_turns=5):
        messages = [{"role": "user", "content": user_msg}]
        for turn in range(max_turns):
            resp = self.client.chat.completions.create(
                model="gpt-4o", messages=messages,
                tools=self.tools, tool_choice="auto",
            )
            msg = resp.choices[0].message
            if not msg.tool_calls:
                return msg.content  # Финальный ответ
            messages.append(msg)
            for tc in msg.tool_calls:
                result = self.handlers[tc.function.name](**json.loads(tc.function.arguments))
                messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": json.dumps(result),
                })
        return "Max turns exceeded"

agent = ToolAgent(client, tools, {
    "get_weather": lambda city, unit="celsius": {"temp": 18, "city": city, "unit": unit},
})
print(agent.run("Какая погода в Берлине? Переведи в фаренгейты."))

🔗 Полезные ссылки

📖 OpenAI Tools📖 Anthropic Tools🦙 Ollama Tools