Все уроки · 44
Инструменты и вызов функций
Как модель вызывает ваш код: схемы инструментов, полный круг вызова функции, работающий бот погоды и практики, которые делают агентов надёжными.
Начните отсюда: вызов инструмента — это текст
Если вы никогда этим не занимались, начните с одной фразы, потому что из неё следует всё остальное:
«Вызов инструмента» — это текст, который пишет модель и в котором просит вас запустить функцию. Ваш код запускает функцию и вставляет результат обратно — тоже в виде текста.
Ничего экзотического не происходит. Нет никакой системы плагинов, никакого
исполнения кода внутри модели, никакого тайного канала к вашей базе данных. Модель
пишет «Хочу вызвать get_weather(city="Prague")», а API отдаёт вам этот запрос в
аккуратном поле, чтобы не приходилось вылавливать его из предложения. После этого
модель ждёт и ничего не делает, пока вы не вызовете её снова с ответом.
Представьте коллегу в комнате без окон с блокнотом для записок. Он умеет читать,
думать и писать — и всё. Вы просовываете под дверь сообщение; он в ответ
просовывает записку: «Пожалуйста, запустите get_weather(city='Prague') и
скажите, что получилось». Вы идёте и запускаете. Пишете результат на листке,
просовываете его обратно под дверь — и только тогда он продолжает. Любой агентный
фреймворк — это машина для передачи записок под этой дверью.
Два следствия, которые стоит запомнить:
- Модель не может понять, что произошло в реальности. Реальная температура, значение из кэша, строка с ошибкой, которую вы написали вручную, — для модели всё это текст. Поэтому, если вернуть «Error: city not found» (вместо падения), модель сможет исправиться на следующем шаге.
- Модель ничего не решает об исполнении. Она может попросить о чём угодно, у чего есть имя. Будет ли запрос выполнен вообще, с чьими учётными данными и с какими ограничениями — целиком решение вашего кода; это и есть граница безопасности.
Модель не запускает ваши функции
Модель никогда не запускает ваши функции. Она не может. У неё нет рук, доступа к сети, файловой системы — это ящик, превращающий текст в текст. Всё, что происходит в мире, происходит в вашем коде.
Картинка, которой я пользуюсь: модель — это консьерж в отеле, который не может отойти от стойки. Консьерж может решить, что нужно сделать, и аккуратно записать это на бланке — «полотенца в номер 412», — но отнести их сам не может. Кто-то другой читает бланк и делает работу. Если бланк расплывчатый, происходит не то; если у консьержа сто форм на выбор, он чаще выбирает не ту. Ваша задача — дать ему небольшое число ясных форм.
Это сердце любого агента и одновременно граница безопасности: модель предлагает, ваш код решает.
Когда мы говорим, что модель «вызывает инструмент», происходит вот что:
- Вы описываете доступные инструменты в запросе.
- Модель выдаёт структурированный JSON: «Хочу вызвать
get_weatherс такими аргументами». - Ваш код видит этот JSON, запускает настоящую функцию и отправляет результат обратно.
- Модель читает результат и продолжает.
Модель — это планировщик, способный выдавать только текст и JSON. Каждая реальная возможность — поиск, база данных, файловая система, платежи — живёт в вашем коде, за функцией, которую модель может попросить вас запустить. Это граница безопасности, и обходить её незачем. Модель может запрашивать действия, а ваш код решает и выполняет их.
Описание инструмента: схема
Определение инструмента состоит из трёх частей: имени, описания и JSON Schema для аргументов. Схема — это просто бланк: в нём перечислены поля, которые модель должна заполнить, тип каждого поля и то, какие из них обязательны. Описание — единственное, на что модель опирается, решая, когда и как вызывать инструмент.
1import json2import requests3 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), пишите описание так, будто инструктируете нового
сотрудника, и держите список полей достаточно коротким, чтобы ничего не было
двусмысленным.
Полный круг вызова
Теперь посмотрите на сам разговор — с числом токенов, чтобы видеть, во что обходится каждый шаг:
Третье сообщение удивляет людей больше всего: это сообщение ассистента,
содержимое которого — запрос к инструменту; обычного текста в нём нет. В мире пока
ничего не произошло — модель лишь написала записку-заявку. Четвёртое — результат
инструмента, добавленный в разговор как сообщение с ролью 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 OpenAI2import json3 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].message17 messages.append(msg)18 19 if not msg.tool_calls: # инструмент не запрошен -> итоговый ответ20 print(msg.content)21 break22 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:
открытый стандарт, который позволяет опубликовать инструменты так, чтобы их находила
и использовала любая модель, не только ваша.