OpenAI Cookbook: как вызывать функции из чат-моделей
Полный перевод рецепта OpenAI о вызове функций: описание инструментов JSON-схемой, уточняющие вопросы, принудительный выбор функции и запрет на вызов, параллельные вызовы и полный цикл с выполнением SQL-запроса к базе данных.
Как вызывать функции из чат-моделей
Заголовок раздела «Как вызывать функции из чат-моделей»Этот ноутбук рассказывает, как использовать Chat Completions API вместе с внешними функциями, чтобы расширить возможности моделей GPT.
tools — необязательный параметр Chat Completions API, через который можно передать спецификации функций. Его цель —
дать моделям возможность генерировать аргументы функций, соответствующие переданным спецификациям. Заметьте, что API
сам не выполняет никаких вызовов функций. Выполнять вызовы функций по выходу модели — задача разработчиков.
Если в параметре tools переданы функции, то по умолчанию модель сама решает, когда уместно использовать одну из
них. API можно заставить использовать конкретную функцию, задав параметр tool_choice равным
{"type": "function", "function": {"name": "my_function"}}. Можно и запретить использовать какую-либо функцию, задав
tool_choice равным "none". Если функция используется, в ответе будет "finish_reason": "tool_calls", а также
объект tool_calls с именем функции и сгенерированными аргументами.
Ноутбук состоит из двух разделов:
- Как генерировать аргументы функций: задаём набор функций и с помощью API генерируем для них аргументы.
- Как вызывать функции с аргументами, сгенерированными моделью: замыкаем цикл, по-настоящему выполняя функции с аргументами от модели.
Как генерировать аргументы функций
Заголовок раздела «Как генерировать аргументы функций»!pip install scipy --quiet!pip install tenacity --quiet!pip install tiktoken --quiet!pip install termcolor --quiet!pip install openai --quietimport jsonfrom openai import OpenAIfrom tenacity import retry, wait_random_exponential, stop_after_attemptfrom termcolor import colored
GPT_MODEL = "gpt-5"client = OpenAI()Вспомогательные функции
Заголовок раздела «Вспомогательные функции»Сначала определим несколько вспомогательных функций для вызовов Chat Completions API и для хранения и отслеживания состояния разговора.
@retry(wait=wait_random_exponential(multiplier=1, max=40), stop=stop_after_attempt(3))def chat_completion_request(messages, tools=None, tool_choice=None, model=GPT_MODEL): try: response = client.chat.completions.create( model=model, messages=messages, tools=tools, tool_choice=tool_choice, ) return response except Exception as e: print("Unable to generate ChatCompletion response") print(f"Exception: {e}") return eПояснение переводчика: декоратор @retry из библиотеки tenacity повторяет запрос до трёх раз со случайной
экспоненциально растущей паузой (не больше 40 секунд) — приём из раздела «Надёжность» урока
8.1.1. Правда, внутри функции исключение перехватывается и
возвращается как значение, а @retry повторяет вызов только при выброшенном исключении — поэтому повтор здесь
фактически не срабатывает. В своём коде не перехватывайте исключение внутри функции с @retry.
def pretty_print_conversation(messages): role_to_color = { "system": "red", "user": "green", "assistant": "blue", "function": "magenta", }
for message in messages: if message["role"] == "system": print(colored(f"system: {message['content']}\n", role_to_color[message["role"]])) elif message["role"] == "user": print(colored(f"user: {message['content']}\n", role_to_color[message["role"]])) elif message["role"] == "assistant" and message.get("tool_calls"): print(colored(f"assistant: {message['tool_calls']}\n", role_to_color[message["role"]])) elif message["role"] == "assistant" and not message.get("tool_calls"): print(colored(f"assistant: {message['content']}\n", role_to_color[message["role"]])) elif message["role"] == "function": print(colored(f"function ({message['name']}): {message['content']}\n", role_to_color[message["role"]]))Основные понятия
Заголовок раздела «Основные понятия»Создадим спецификации функций для работы с гипотетическим API погоды. Передадим эти спецификации в Chat Completions API, чтобы получить аргументы функций, соответствующие спецификации.
tools = [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, "format": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "The temperature unit to use. Infer this unit from the forecast location.", }, }, "required": ["location", "format"], }, } }, { "type": "function", "function": { "name": "get_n_day_weather_forecast", "description": "Get an N-day weather forecast", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, "format": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "The temperature unit to use. Infer this unit from the forecast location.", }, "num_days": { "type": "integer", "description": "The number of days to forecast", } }, "required": ["location", "format", "num_days"] }, } },]Перевод описаний: get_current_weather — «получить текущую погоду»; get_n_day_weather_forecast — «получить прогноз
погоды на N дней». Параметры: location — «город и штат, например San Francisco, CA»; format — «единица температуры.
Определи её по месту прогноза»; num_days — «на сколько дней прогноз». Поле required перечисляет обязательные
параметры, enum — допустимые значения.
Если спросить модель о текущей погоде, она ответит уточняющими вопросами.
messages = []messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})messages.append({"role": "user", "content": "What's the weather like today"})chat_response = chat_completion_request( messages, tools=tools)messages.append(chat_response.choices[0].message.to_dict())pretty_print_conversation(messages)system: Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous.
user: What's the weather like today
assistant: Sure—what city and state (or country) should I check? Also, do you prefer Celsius or Fahrenheit?Перевод: system — «Не додумывай значения для аргументов функций. Если запрос пользователя неоднозначен, попроси уточнить». user — «Какая сегодня погода?» assistant — «Конечно. Для какого города и штата (или страны) посмотреть? И вы предпочитаете градусы Цельсия или Фаренгейта?»
Когда мы сообщим недостающие сведения, модель сгенерирует для нас подходящие аргументы функции.
messages.append({"role": "user", "content": "I'm in Glasgow, Scotland."})chat_response = chat_completion_request( messages, tools=tools)messages.append(chat_response.choices[0].message.to_dict())pretty_print_conversation(messages)system: Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous.
user: What's the weather like today
assistant: Sure—what city and state (or country) should I check? Also, do you prefer Celsius or Fahrenheit?
user: I'm in Glasgow, Scotland.
assistant: [{'id': 'call_k2QgGc9GT9WjxD76GvR0Ot8q', 'function': {'arguments': '{"location": "Glasgow, Scotland", "format": "celsius"}', 'name': 'get_current_weather'}, 'type': 'function'}, {'id': 'call_RtnXV5t49lqbWwhvGoEPZ7KY', 'function': {'arguments': '{"location": "Glasgow, Scotland", "format": "celsius", "num_days": 1}', 'name': 'get_n_day_weather_forecast'}, 'type': 'function'}]Пояснение переводчика: на «Я в Глазго, Шотландия» модель не ответила текстом, а вернула два вызова сразу: текущая
погода и прогноз на один день, оба в градусах Цельсия — единицу она вывела из места, как просило описание параметра.
Обратите внимание: arguments — это строка с JSON, её ещё нужно разобрать json.loads.
Если сформулировать запрос иначе, можно направить модель к другой функции, о которой мы ей рассказали.
messages = []messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})messages.append({"role": "user", "content": "what is the weather going to be like in Glasgow, Scotland over the next x days"})chat_response = chat_completion_request( messages, tools=tools)messages.append(chat_response.choices[0].message.to_dict())pretty_print_conversation(messages)system: Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous.
user: what is the weather going to be like in Glasgow, Scotland over the next x days
assistant: How many days would you like the forecast for in Glasgow, Scotland? For example: 3, 5, 7, 10, or 14.Перевод: user — «какая будет погода в Глазго, Шотландия, в ближайшие x дней»; assistant — «На сколько дней вам нужен прогноз для Глазго? Например: 3, 5, 7, 10 или 14».
И снова модель просит уточнения, потому что ей пока не хватает сведений. В этом случае место прогноза она уже знает, но ей нужно знать, на сколько дней нужен прогноз.
messages.append({"role": "user", "content": "5 days"})chat_response = chat_completion_request( messages, tools=tools)messages.append(chat_response.choices[0].message.to_dict())pretty_print_conversation(messages)system: Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous.
user: what is the weather going to be like in Glasgow, Scotland over the next x days
assistant: How many days would you like the forecast for in Glasgow, Scotland? For example: 3, 5, 7, 10, or 14.
user: 5 days
assistant: [{'id': 'call_lNzOVLrNSaSVjL3O3bN110af', 'function': {'arguments': '{"location":"Glasgow, Scotland","format":"celsius","num_days":5}', 'name': 'get_n_day_weather_forecast'}, 'type': 'function'}]Пояснение переводчика: после ответа «5 дней» модель вызывает get_n_day_weather_forecast с num_days: 5.
Принудительный вызов конкретной функции или запрет на вызов
Заголовок раздела «Принудительный вызов конкретной функции или запрет на вызов»Модель можно заставить использовать конкретную функцию, например get_n_day_weather_forecast, с помощью аргумента
function_call. Так мы вынуждаем модель самой додумать, как её использовать.
# в этой ячейке мы заставляем модель использовать get_n_day_weather_forecastmessages = []messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})messages.append({"role": "user", "content": "Give me a weather report for Toronto, Canada."})chat_response = chat_completion_request( messages, tools=tools, tool_choice={"type": "function", "function": {"name": "get_n_day_weather_forecast"}})messages.append(chat_response.choices[0].message.to_dict())pretty_print_conversation(messages)system: Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous.
user: Give me a weather report for Toronto, Canada.
assistant: [{'id': 'call_3hoMjl55OQ7LxfwhFyjxwv1T', 'function': {'arguments': '{"location":"Toronto, Canada","format":"celsius","num_days":5}', 'name': 'get_n_day_weather_forecast'}, 'type': 'function'}]Пояснение переводчика: на «Дай сводку погоды для Торонто, Канада» модель, которую заставили вызвать прогноз,
сама выбрала num_days: 5 — хотя системный промпт просил ничего не додумывать. Принуждение сильнее инструкции.
# если не заставлять модель использовать get_n_day_weather_forecast, она может её и не выбратьmessages = []messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})messages.append({"role": "user", "content": "Give me a weather report for Toronto, Canada."})chat_response = chat_completion_request( messages, tools=tools)messages.append(chat_response.choices[0].message.to_dict())pretty_print_conversation(messages)system: Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous.
user: Give me a weather report for Toronto, Canada.
assistant: [{'id': 'call_wv5mdjEQJnBPuSci3xw09Tom', 'function': {'arguments': '{"location":"Toronto, ON","format":"celsius"}', 'name': 'get_current_weather'}, 'type': 'function'}]Можно и вовсе запретить модели использовать функции. Так мы не даём ей сделать настоящий вызов функции.
messages = []messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})messages.append({"role": "user", "content": "Give me the current weather (use Celcius) for Toronto, Canada."})chat_response = chat_completion_request( messages, tools=tools, tool_choice="none")messages.append(chat_response.choices[0].message.to_dict())pretty_print_conversation(messages)system: Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous.
user: Give me the current weather (use Celcius) for Toronto, Canada.
assistant: I don’t have live access to pull the current conditions right now. To get Toronto’s current weather in Celsius, check any of these quickly:- Environment Canada: weather.gc.ca (search “Toronto”)- The Weather Network: theweathernetwork.com/ca/weather/ontario/toronto- Google: search “Toronto weather” (shows °C by default in Canada)- AccuWeather or Weather.com (set units to °C)
If you paste the current readings here (temperature, feels-like, wind, precipitation), I can interpret them and advise on what to wear or plan for.Перевод: user — «Дай текущую погоду (в градусах Цельсия) для Торонто, Канада». assistant — «Прямо сейчас у меня нет доступа к текущим данным. Чтобы быстро узнать погоду в Торонто в градусах Цельсия, загляните на любой из этих ресурсов: Environment Canada, The Weather Network, Google («Toronto weather»), AccuWeather или Weather.com. Если вставите сюда текущие показатели (температура, ощущаемая температура, ветер, осадки), я помогу их истолковать и подскажу, как одеться».
Параллельный вызов функций
Заголовок раздела «Параллельный вызов функций»Новые модели, такие как gpt-5, gpt-4.1 или gpt-4o, могут вызвать несколько функций за один ход.
messages = []messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})messages.append({"role": "user", "content": "what is the weather going to be like in San Francisco and Glasgow over the next 4 days"})chat_response = chat_completion_request( messages, tools=tools, model="gpt-4o")
assistant_message = chat_response.choices[0].message.tool_callsassistant_message[ChatCompletionMessageFunctionToolCall(id='call_KlZ3Fqt3SviC6o66dVMYSa2Q', function=Function(arguments='{"location": "San Francisco, CA", "format": "fahrenheit", "num_days": 4}', name='get_n_day_weather_forecast'), type='function'), ChatCompletionMessageFunctionToolCall(id='call_YAnH0VRB3oqjqivcGj3Cd8YA', function=Function(arguments='{"location": "Glasgow, UK", "format": "celsius", "num_days": 4}', name='get_n_day_weather_forecast'), type='function')]Пояснение переводчика: на вопрос «какая будет погода в Сан-Франциско и Глазго в ближайшие 4 дня» модель вернула два
вызова: для Сан-Франциско в градусах Фаренгейта (США), для Глазго — в градусах Цельсия. У каждого вызова свой id:
отвечая модели, результат каждого вызова нужно привязать к его id.
Как вызывать функции с аргументами, сгенерированными моделью
Заголовок раздела «Как вызывать функции с аргументами, сгенерированными моделью»В следующем примере мы покажем, как выполнять функции, входные данные для которых сгенерировала модель, и с помощью этого сделаем агента, который отвечает нам на вопросы о базе данных. Для простоты возьмём демонстрационную базу данных Chinook.
Примечание: генерация SQL в продакшене может быть рискованной, потому что модели не идеально надёжно генерируют правильный SQL.
Задаём функцию, выполняющую SQL-запросы
Заголовок раздела «Задаём функцию, выполняющую SQL-запросы»Сначала определим несколько полезных функций для извлечения данных из базы SQLite.
import sqlite3
conn = sqlite3.connect("data/Chinook.db")print("Opened database successfully")Opened database successfullydef get_table_names(conn): """Возвращает список имён таблиц.""" table_names = [] tables = conn.execute("SELECT name FROM sqlite_master WHERE type='table';") for table in tables.fetchall(): table_names.append(table[0]) return table_names
def get_column_names(conn, table_name): """Возвращает список имён столбцов.""" column_names = [] columns = conn.execute(f"PRAGMA table_info('{table_name}');").fetchall() for col in columns: column_names.append(col[1]) return column_names
def get_database_info(conn): """Возвращает список словарей с именем таблицы и её столбцами для каждой таблицы в базе.""" table_dicts = [] for table_name in get_table_names(conn): columns_names = get_column_names(conn, table_name) table_dicts.append({"table_name": table_name, "column_names": columns_names}) return table_dictsТеперь с помощью этих функций можно получить представление схемы базы данных.
database_schema_dict = get_database_info(conn)database_schema_string = "\n".join( [ f"Table: {table['table_name']}\nColumns: {', '.join(table['column_names'])}" for table in database_schema_dict ])Как и раньше, зададим спецификацию функции, для которой API должен генерировать аргументы. Обратите внимание: мы вставляем схему базы данных прямо в спецификацию функции. Модели важно её знать.
tools = [ { "type": "function", "function": { "name": "ask_database", "description": "Use this function to answer user questions about music. Input should be a fully formed SQL query.", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": f""" SQL query extracting info to answer the user's question. SQL should be written using this database schema: {database_schema_string} The query should be returned in plain text, not in JSON. """, } }, "required": ["query"], }, } }]Перевод описаний: функция — «Используй эту функцию, чтобы отвечать на вопросы пользователя о музыке. На вход —
полностью составленный SQL-запрос». Параметр query — «SQL-запрос, извлекающий данные для ответа на вопрос
пользователя. SQL нужно писать для этой схемы базы данных: … Запрос нужно вернуть простым текстом, а не в JSON».
Выполнение SQL-запросов
Заголовок раздела «Выполнение SQL-запросов»Теперь реализуем функцию, которая будет на самом деле выполнять запросы к базе данных.
def ask_database(conn, query): """Функция для запроса к базе SQLite с переданным SQL-запросом.""" try: results = str(conn.execute(query).fetchall()) except Exception as e: results = f"query failed with error: {e}" return resultsПояснение переводчика: ошибка не роняет программу, а возвращается модели текстом («query failed with error: …») — так модель может прочитать ошибку и исправить запрос на следующем шаге.
Шаги вызова функции через Chat Completions API
Заголовок раздела «Шаги вызова функции через Chat Completions API»Шаг 1. Отправить модели запрос, в ответ на который она может выбрать инструмент. Описание инструментов (имя
функции и сигнатура) задаётся в списке tools и передаётся модели в вызове API. Если инструмент выбран, имя функции
и параметры будут в ответе.
Шаг 2. Программно проверить, хотела ли модель вызвать функцию. Если да — перейти к шагу 3.
Шаг 3. Извлечь из ответа имя функции и параметры, вызвать функцию с этими параметрами. Добавить результат в сообщения.
Шаг 4. Вызвать Chat Completions API со списком сообщений, чтобы получить ответ.
# Шаг 1: отправляем запрос, который может привести к вызову функции. В этом случае модель может понять,# что нужные пользователю сведения, возможно, есть в базе данных, схему которой мы передали в описании инструментов.messages = [{ "role":"user", "content": "What is the name of the album with the most tracks?"}]
response = client.chat.completions.create( model=GPT_MODEL, messages=messages, tools=tools, tool_choice="auto")
# Добавляем сообщение в список сообщенийresponse_message = response.choices[0].messagemessages.append(response_message.to_dict())pretty_print_conversation(messages)user: What is the name of the album with the most tracks?
assistant: [{'id': 'call_pGRtZZGfd2o41GHlZcEdB9he', 'function': {'arguments': '{"query":"WITH track_counts AS (\\n SELECT a.AlbumId, a.Title, COUNT(t.TrackId) AS track_count\\n FROM Album a\\n JOIN Track t ON t.AlbumId = a.AlbumId\\n GROUP BY a.AlbumId, a.Title\\n)\\nSELECT Title\\nFROM track_counts\\nWHERE track_count = (SELECT MAX(track_count) FROM track_counts);"}', 'name': 'ask_database'}, 'type': 'function'}]Пояснение переводчика: вопрос — «Как называется альбом с наибольшим числом треков?». Модель написала SQL: посчитать
треки по альбомам (COUNT ... GROUP BY) и выбрать альбом(ы) с максимальным числом.
# Шаг 2: проверяем, есть ли в ответе модели вызов инструмента.tool_calls = response_message.tool_callsif tool_calls: # Если да, модель вернёт имя инструмента / функции и аргумент(ы) tool_call_id = tool_calls[0].id tool_function_name = tool_calls[0].function.name tool_query_string = json.loads(tool_calls[0].function.arguments)['query']
# Шаг 3: вызываем функцию и получаем результат. Добавляем результат в список сообщений. if tool_function_name == 'ask_database': results = ask_database(conn, tool_query_string)
messages.append({ "role":"tool", "tool_call_id":tool_call_id, "name": tool_function_name, "content":results })
# Шаг 4: вызываем Chat Completions API, добавив ответ функции в список сообщений # Заметьте: сообщения с ролью 'tool' должны быть ответом на предшествующее сообщение с 'tool_calls' model_response_with_function_call = client.chat.completions.create( model=GPT_MODEL, messages=messages, ) # получаем новый ответ модели, в котором она видит ответ функции print(f"Result found in database: {model_response_with_function_call.choices[0].message.content}") else: print(f"Error: function {tool_function_name} does not exist")else: # Модель не выбрала функцию для вызова, результат можно вернуть пользователю print(response_message.content)Result found in database: Greatest HitsПеревод: «Результат найден в базе данных: Greatest Hits».
Что дальше
Заголовок раздела «Что дальше»Смотрите наш другой ноутбук, который показывает, как с помощью Chat Completions API и функций для поиска знаний вести разговор с базой знаний.
Разбор и упражнения (добавлено переводчиком)
Заголовок раздела «Разбор и упражнения (добавлено переводчиком)»Что здесь не хватает до настоящего агента
Заголовок раздела «Что здесь не хватает до настоящего агента»Шаги 1–4 выполняются ровно один раз. Настоящий агент повторяет их в цикле, пока модель не перестанет просить
инструменты, — это цикл из урока 8.3.1. Кроме того, код обрабатывает
только первый вызов (tool_calls[0]), хотя, как мы видели, модель может вернуть несколько вызовов сразу; ответить
нужно на каждый, иначе API вернёт ошибку.
# Набросок цикла агента поверх функций рецепта: обрабатываем все вызовы и повторяем,# пока модель не ответит текстом или не кончится лимит шагов.def run_agent(messages, tools, max_steps=5): for _ in range(max_steps): response = client.chat.completions.create(model=GPT_MODEL, messages=messages, tools=tools) msg = response.choices[0].message messages.append(msg.to_dict()) if not msg.tool_calls: # модель ответила текстом — готово return msg.content for call in msg.tool_calls: # отвечаем на КАЖДЫЙ вызов, привязывая по id args = json.loads(call.function.arguments) if call.function.name == "ask_database": result = ask_database(conn, args["query"]) else: result = f"Error: unknown function {call.function.name}" messages.append({"role": "tool", "tool_call_id": call.id, "content": result}) return "Превышен лимит шагов"Безопасность
Заголовок раздела «Безопасность»Модель пишет произвольный SQL, а программа его выполняет. Вопрос пользователя «удали все таблицы» или инъекция
в данных может превратиться в DROP TABLE. Минимальная защита из урока 8.3.2
(раздел «Безопасность инструментов») — принцип наименьших привилегийпринцип наименьших привилегийEN principle of least privilege: открыть базу
только на чтение, например sqlite3.connect("file:data/Chinook.db?mode=ro", uri=True), и ограничить время и объём
результата.
Упражнения
Заголовок раздела «Упражнения»- Запустите рецепт с локальной моделью через Ollama. Сколько из пяти вопросов о погоде модель отработала так же, как GPT-5? Где она «додумала» аргументы вопреки системному промпту?
- Перепишите описание
ask_databaseпо-русски и задайте вопросы о базе по-русски. Изменилось ли качество SQL? - Откройте базу только на чтение и попросите модель «удалить таблицу Album». Что вернёт
ask_databaseи что модель ответит пользователю? - Допишите
run_agentдо вопроса, который требует двух запросов подряд (например, «какой исполнитель у альбома с наибольшим числом треков?»), и посмотрите, как модель использует результат первого запроса во втором.