Перейти к содержимому
ПереводыOpenAI и DeepLearning.AI

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 --quiet
import json
from openai import OpenAI
from tenacity import retry, wait_random_exponential, stop_after_attempt
from 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_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, 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_calls
assistant_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.

Сначала определим несколько полезных функций для извлечения данных из базы SQLite.

import sqlite3
conn = sqlite3.connect("data/Chinook.db")
print("Opened database successfully")
Opened database successfully
def 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».

Теперь реализуем функцию, которая будет на самом деле выполнять запросы к базе данных.

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: …») — так модель может прочитать ошибку и исправить запрос на следующем шаге.

Шаг 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].message
messages.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_calls
if 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), и ограничить время и объём результата.

  1. Запустите рецепт с локальной моделью через Ollama. Сколько из пяти вопросов о погоде модель отработала так же, как GPT-5? Где она «додумала» аргументы вопреки системному промпту?
  2. Перепишите описание ask_database по-русски и задайте вопросы о базе по-русски. Изменилось ли качество SQL?
  3. Откройте базу только на чтение и попросите модель «удалить таблицу Album». Что вернёт ask_database и что модель ответит пользователю?
  4. Допишите run_agent до вопроса, который требует двух запросов подряд (например, «какой исполнитель у альбома с наибольшим числом треков?»), и посмотрите, как модель использует результат первого запроса во втором.