Все уроки · 44
Путь 3 · Дорожная карта AI-инженера·Урок 21 из 33

Инструменты и вызов функций

Как модель вызывает ваш код: схемы инструментов, полный круг вызова функции, работающий бот погоды и практики, которые делают агентов надёжными.

УровеньСреднийВремя~15 мин

Начните отсюда: вызов инструмента — это текст

Если вы никогда этим не занимались, начните с одной фразы, потому что из неё следует всё остальное:

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

Ничего экзотического не происходит. Нет никакой системы плагинов, никакого исполнения кода внутри модели, никакого тайного канала к вашей базе данных. Модель пишет «Хочу вызвать get_weather(city="Prague")», а API отдаёт вам этот запрос в аккуратном поле, чтобы не приходилось вылавливать его из предложения. После этого модель ждёт и ничего не делает, пока вы не вызовете её снова с ответом.

Представьте коллегу в комнате без окон с блокнотом для записок. Он умеет читать, думать и писать — и всё. Вы просовываете под дверь сообщение; он в ответ просовывает записку: «Пожалуйста, запустите get_weather(city='Prague') и скажите, что получилось». Вы идёте и запускаете. Пишете результат на листке, просовываете его обратно под дверь — и только тогда он продолжает. Любой агентный фреймворк — это машина для передачи записок под этой дверью.

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

  • Модель не может понять, что произошло в реальности. Реальная температура, значение из кэша, строка с ошибкой, которую вы написали вручную, — для модели всё это текст. Поэтому, если вернуть «Error: city not found» (вместо падения), модель сможет исправиться на следующем шаге.
  • Модель ничего не решает об исполнении. Она может попросить о чём угодно, у чего есть имя. Будет ли запрос выполнен вообще, с чьими учётными данными и с какими ограничениями — целиком решение вашего кода; это и есть граница безопасности.

Модель не запускает ваши функции

Модель никогда не запускает ваши функции. Она не может. У неё нет рук, доступа к сети, файловой системы — это ящик, превращающий текст в текст. Всё, что происходит в мире, происходит в вашем коде.

Картинка, которой я пользуюсь: модель — это консьерж в отеле, который не может отойти от стойки. Консьерж может решить, что нужно сделать, и аккуратно записать это на бланке — «полотенца в номер 412», — но отнести их сам не может. Кто-то другой читает бланк и делает работу. Если бланк расплывчатый, происходит не то; если у консьержа сто форм на выбор, он чаще выбирает не ту. Ваша задача — дать ему небольшое число ясных форм.

Это сердце любого агента и одновременно граница безопасности: модель предлагает, ваш код решает.

Когда мы говорим, что модель «вызывает инструмент», происходит вот что:

  1. Вы описываете доступные инструменты в запросе.
  2. Модель выдаёт структурированный JSON: «Хочу вызвать get_weather с такими аргументами».
  3. Ваш код видит этот JSON, запускает настоящую функцию и отправляет результат обратно.
  4. Модель читает результат и продолжает.

Модель — это планировщик, способный выдавать только текст и JSON. Каждая реальная возможность — поиск, база данных, файловая система, платежи — живёт в вашем коде, за функцией, которую модель может попросить вас запустить. Это граница безопасности, и обходить её незачем. Модель может запрашивать действия, а ваш код решает и выполняет их.

Описание инструмента: схема

Определение инструмента состоит из трёх частей: имени, описания и JSON Schema для аргументов. Схема — это просто бланк: в нём перечислены поля, которые модель должна заполнить, тип каждого поля и то, какие из них обязательны. Описание — единственное, на что модель опирается, решая, когда и как вызывать инструмент.

weather_bot.py — схема инструмента
1import json
2import requests
3 
4tools = [
5 {
6 #98c379">"type": "function",
7 #98c379">"function": {
8 #98c379">"name": "get_weather",
9 #98c379">"description": (
10 #98c379">"Get current weather for a city. Use for any question "
11 #98c379">"about temperature, rain, wind or forecast."
12 ),
13 #98c379">"parameters": {
14 #98c379">"type": "object",
15 #98c379">"properties": {
16 #98c379">"city": {
17 #98c379">"type": "string",
18 #98c379">"description": "City name, e.g. 'Prague'"
19 },
20 #98c379">"units": {
21 #98c379">"type": "string",
22 #98c379">"enum": ["celsius", "fahrenheit"],
23 #98c379">"description": "Temperature units, default celsius"
24 }
25 },
26 #98c379">"required": ["city"]
27 }
28 }
29 }
30]
31 
32def get_weather(city: str, units: str = #98c379">"celsius") -> str:
33 # НАСТОЯЩАЯ функция — обычный код, без магии
34 r = requests.get(
35 #98c379">"https://wttr.in/" + city,
36 params={#98c379">"format": "j1"},
37 timeout=10,
38 )
39 data = r.json()[#98c379">"current_condition"][0]
40 temp = data[#98c379">"temp_C"] if units == "celsius" else data["temp_F"]
41 return json.dumps({
42 #98c379">"city": city,
43 #98c379">"temp": temp,
44 #98c379">"desc": data["weatherDesc"][0]["value"],
45 })

Обратите внимание на enum у units: схемы позволяют ограничить выбор модели, а это устраняет целые классы ошибок ещё до того, как они возникнут. enum просто значит «ответ должен быть одним из этих значений», а required — «это поле пропускать нельзя».

Вот часть со схемой отдельно — её можно вставить в собственный запрос:

Готовая схема инструмента — скопируйте и адаптируйте
{
"type": "function",
"function": {
  "name": "get_weather",
  "description": "Get current weather for a city. Use for any question about temperature, rain, wind or forecast. Returns temp and a short description.",
  "parameters": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "City name, e.g. 'Prague'"
      },
      "units": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "Temperature units, default celsius"
      }
    },
    "required": ["city"]
  }
}
}

Схемы работают благодаря трём привычкам: называйте инструмент по действию (get_weather, а не weather), пишите описание так, будто инструктируете нового сотрудника, и держите список полей достаточно коротким, чтобы ничего не было двусмысленным.

Полный круг вызова

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

вызов функций шаг за шагом
press the button below to send the first message
sent to the modelnothing yet — every message is sent in full

Третье сообщение удивляет людей больше всего: это сообщение ассистента, содержимое которого — запрос к инструменту; обычного текста в нём нет. В мире пока ничего не произошло — модель лишь написала записку-заявку. Четвёртое — результат инструмента, добавленный в разговор как сообщение с ролью tool: так ваш код отчитывается. Затем модель вызывают снова, и теперь она видит всю историю и пишет ответ для человека.

Вот как этот ответ выглядит буквально в момент поступления и что ваш код отправляет в следующем вызове — только JSON на входе, текст на выходе:

сырой ответ и ваш следующий запрос
1// Что вернула модель. Обратите внимание: content ПУСТ, а
2// запрос лежит в tool_calls. Это текст — модель
3// ничего не выполнила.
4{
5#98c379">"role": "assistant",
6#98c379">"content": null,
7#98c379">"tool_calls": [
8 {
9 #98c379">"id": "call_8Fk2",
10 #98c379">"type": "function",
11 #98c379">"function": {
12 #98c379">"name": "get_weather",
13 #98c379">"arguments": "{\"city\":\"Prague\",\"units\":\"celsius\"}"
14 }
15 }
16]
17}
18 
19// Что дальше делает ВАШ код: запускает настоящую функцию, затем
20// добавляет результат как сообщение с ролью "tool" и тем же
21// id. Значение — СТРОКА, то есть опять текст.
22{
23#98c379">"role": "tool",
24#98c379">"tool_call_id": "call_8Fk2",
25#98c379">"content": "{\"city\": \"Prague\", \"temp\": \"14\", \"desc\": \"Light rain shower\"}"
26}

Произнесите это вслух один раз: модель просит, ваш код действует, модель читает, что вернулось. Любой агентный фреймворк, которым вы когда-нибудь воспользуетесь, — это обёртка вокруг этой фразы.

Вот этот цикл в виде кода:

минимальный цикл агента
1from openai import OpenAI
2import json
3 
4client = OpenAI()
5messages = [
6 {#98c379">"role": "system", "content": "You are a weather assistant."},
7 {#98c379">"role": "user", "content": "Should I take an umbrella in Prague today?"},
8]
9 
10while True:
11 response = client.chat.completions.create(
12 model=#98c379">"gpt-4o-mini",
13 messages=messages,
14 tools=tools,
15 )
16 msg = response.choices[0].message
17 messages.append(msg)
18 
19 if not msg.tool_calls: # инструмент не запрошен -> итоговый ответ
20 print(msg.content)
21 break
22 
23 for call in msg.tool_calls: # выполняем каждый запрошенный инструмент
24 args = json.loads(call.function.arguments)
25 result = get_weather(**args)
26 messages.append({
27 #98c379">"role": "tool",
28 #98c379">"tool_call_id": call.id,
29 #98c379">"content": result,
30 })
31 # цикл: модель теперь видит результат инструмента и продолжает

Этот цикл while и есть агент. Всё остальное — удобства.

Практики, которые делают всё надёжным

  • Пишите описания для нового сотрудника. «Получить погоду» — слабое описание. Скажите, для чего инструмент («используй для любого вопроса о температуре, дожде, ветре»), что он возвращает и когда им не стоит пользоваться. Описание — это ваш промпт для модели об этом инструменте.
  • Держите набор инструментов небольшим. Десять хорошо описанных инструментов лучше сорока расплывчатых. Когда вариантов слишком много, модели чаще выбирают не тот инструмент или выдумывают аргументы.
  • Возвращайте ошибки строками, а не исключениями. Если вызов API не удался, верните в цикл «Error: city not found» — модель часто сама исправится. Упавший процесс восстановиться не может, а цикл — может.
  • Ограничивайте схемой. Перечисления (enum), обязательные поля и типы превращают «модель может написать что угодно» в «модель должна выбрать из этого».
  • Логируйте каждый вызов и результат. Когда агент ведёт себя странно, запись вызовов инструментов — ваши данные для отладки; без неё вы гадаете.
Проверьте себя

Что делает модель, когда «вызывает инструмент»?

Инструмент постоянно вызывают с неверными аргументами. Первое, что нужно улучшить:

Вызов API внутри инструмента завершился ошибкой. Что должен сделать ваш код?

Итог

Теперь вы можете замкнуть цикл: описать инструменты, позволить модели запрашивать их, а затем самим запускать запрошенное и возвращать результаты. Этот цикл while — весь двигатель. Единственное, что стоит прочитать дальше, — MCP: открытый стандарт, который позволяет опубликовать инструменты так, чтобы их находила и использовала любая модель, не только ваша.