Логика Function Calling: От определений до выполнения
Введение: Выход за пределы текстового окна
Добро пожаловать в третий модуль. До этого момента мы общались с Gemini как с невероятно начитанным, но изолированным собеседником. Представьте себе профессора, запертого в комнате без окон, интернета и телефона. Он знает всё, что было написано в книгах до 2023 года, но если вы спросите его: «Какая сейчас погода в Токио?» или «Сколько стоит биткоин прямо сейчас?», он либо разведет руками, либо начнет фантазировать (галлюцинировать).
Function Calling (вызов функций) — это проведение интернета и телефонной линии в эту комнату. Это механизм, который позволяет модели не просто генерировать текст, а действовать: запрашивать данные из внешних баз, управлять умным домом, отправлять письма или выполнять сложные вычисления.
В этом уроке мы разберем логику этого процесса «от» и «до». Мы не просто научимся копировать код из документации, а поймем фундаментальную архитектуру: как модель «понимает», что нужно вызвать функцию, и что происходит в черном ящике между вашим запросом и ответом AI.
Самое важное заблуждение
Прежде чем мы напишем первую строчку кода, давайте разрушим самый популярный миф среди новичков.
Миф: Gemini сама выполняет код вашей функции внутри своих серверов Google.
Реальность: Gemini никогда не выполняет ваш код. Она генерирует структурированный запрос (обычно в формате JSON), в котором говорит: «Эй, разработчик! Пожалуйста, выполни функцию с именем X вот с такими параметрами, а потом верни мне результат».
Весь процесс Function Calling — это игра в пинг-понг между вашей программой (Client) и моделью (Model). Ваша задача — быть надежным исполнителем воли модели.
Анатомия процесса
Цикл вызова функции состоит из четырех четких этапов:
- Определение (Definition): Вы описываете модели, какие инструменты у нее есть (например, «функция для получения погоды»).
- Решение (Decision): Модель анализирует вопрос пользователя («Нужен ли зонт в Лондоне?») и решает, что для ответа ей нужен инструмент. Она возвращает не текст, а структуру вызова функции.
- Исполнение (Execution): Ваше приложение перехватывает этот запрос, запускает реальный Python-код, делает запрос к API погоды и получает ответ («Дождь, +12°C»).
- Синтез (Response): Вы отправляете этот технический ответ обратно модели. Она читает его и формирует финальный ответ на человеческом языке: «В Лондоне идет дождь, так что зонт вам точно пригодится».
import google.generativeai as genai
import os
# Настройка API ключа
# genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
# 1. Определяем реальную функцию (инструмент)
# Это обычная Python-функция. Модель не видит её код, только сигнатуру.
def get_stock_price(ticker: str):
"""
Возвращает текущую цену акции для заданного тикера.
Args:
ticker: Биржевой тикер компании (например, AAPL, GOOGL).
"""
# Имитация запроса к API биржи
mock_database = {
"AAPL": 150.25,
"GOOGL": 2800.50,
"MSFT": 310.10
}
return mock_database.get(ticker, "Unknown Ticker")
# 2. Создаем словарь инструментов
tools_list = [get_stock_price]
# 3. Инициализируем модель с знанием об этих инструментах
model = genai.GenerativeModel(
model_name='gemini-1.5-flash',
tools=tools_list # Передаем функции напрямую
)
print("Инструменты успешно подключены к модели.")
Как модель «видит» функции?
Обратите внимание на код выше. Мы передали функцию get_stock_price в параметр tools. Но как Gemini узнает, что делает эта функция? Она не читает код внутри (строки с mock_database ей не видны). Она читает Docstrings (документационные строки) и аннотации типов.
В мире LLM, описание функции — это часть промпта. Если вы назовете функцию func1(a, b) и не добавите описание, модель будет гадать, как её использовать, и скорее всего ошибется. Если же вы напишете: Возвращает текущую цену акции..., модель поймет семантический смысл инструмента.
Совет эксперта: Относитесь к написанию docstring'ов для Function Calling так же ответственно, как к написанию системного промпта. Чем точнее вы опишете, что функция делает и (важно!) чего она не делает, тем надежнее будет работать ваш агент.
Этап принятия решения (Decision)
Давайте посмотрим, что происходит, когда мы задаем вопрос.
# Задаем вопрос, требующий использования инструмента
chat = model.start_chat(enable_automatic_function_execution=False)
# Мы специально отключили автоматическое выполнение, чтобы увидеть "внутренности"
response = chat.send_message("Сколько сейчас стоят акции Apple?")
# Смотрим, что вернула модель
print(f"Текст ответа: {response.text}") # Скорее всего будет пустым или nil
# Проверяем наличие вызова функции
if response.parts[0].function_call:
fc = response.parts[0].function_call
print(f"Модель хочет вызвать функцию: {fc.name}")
print(f"С аргументами: {fc.args}")
# Пример вывода:
# Модель хочет вызвать функцию: get_stock_price
# С аргументами: {'ticker': 'AAPL'}
Разбор полетов: FunctionCall Object
В примере выше мы использовали режим enable_automatic_function_execution=False. Это критически важно для понимания. В ответ на вопрос про акции Apple модель не вернула текст. Она вернула объект FunctionCall внутри списка parts.
Gemini проанализировала контекст:
- Пользователь хочет узнать цену.
- У меня есть инструмент
get_stock_price. - В описании сказано, что он принимает
ticker. - В запросе пользователя "Apple" соответствует тикеру "AAPL" (да, модель достаточно умна, чтобы сопоставить название компании и её тикер благодаря своим внутренним знаниям).
В результате она сформировала JSON-подобный запрос: { "name": "get_stock_price", "args": { "ticker": "AAPL" } }.
Этап исполнения и возврата (The Feedback Loop)
Теперь мяч на нашей стороне. Модель «подвисла» в ожидании. Мы должны:
- Взять имя функции и аргументы.
- Найти соответствующую Python-функцию в нашем коде.
- Запустить её.
- Вернуть результат в виде объекта
FunctionResponse.
Если вы используете автоматический режим (по умолчанию в некоторых SDK или при явном указании), библиотека сделает это за вас. Но для сложных агентов часто приходится управлять этим вручную (например, если функция требует асинхронного выполнения или подтверждения пользователя).
from google.protobuf import struct_pb2
# ... (продолжение предыдущего кода)
if response.parts[0].function_call:
fc = response.parts[0].function_call
# 1. Выполняем логику на нашей стороне
# В реальном приложении здесь был бы динамический диспетчер функций
if fc.name == 'get_stock_price':
ticker = fc.args['ticker']
api_result = get_stock_price(ticker)
# 2. Формируем ответ для модели
# Нам нужно отправить результат обратно в чат
response_part = genai.protos.Part(
function_response=genai.protos.FunctionResponse(
name='get_stock_price',
response={'price': api_result} # Результат должен быть словарем (JSON)
)
)
# 3. Отправляем результат модели, чтобы она сформулировала итоговый ответ
final_response = chat.send_message([response_part])
print(f"Финальный ответ AI: {final_response.text}")
# Ожидаемый вывод:
# Финальный ответ AI: На данный момент акции Apple (AAPL) стоят 150.25.
Почему это мощнее, чем просто код?
Вы можете спросить: «Зачем такие сложности? Я мог бы просто написать if 'цена' in text: run_function()».
Сила Gemini 3 API в гибкости и контексте.
- Понимание естественного языка: Пользователь может спросить «Почём нынче яблоки на бирже?» или «Че там с котировками купертиновцев?». Жесткая логика
if/elseсломается. LLM поймет намерение. - Цепочки рассуждений: Модель может решить вызвать несколько функций подряд. Например: «Сравни погоду в Москве и Париже». Она вызовет
get_weather(Moscow), получит ответ, потомget_weather(Paris), получит ответ, и только потом сравнит их в тексте. - Извлечение параметров: Модель нормализует данные. Вы просите дату «в следующую пятницу», а модель передаст в функцию строку
2024-10-25.
Практические рекомендации
- Чистота данных: Возвращайте функции минимально необходимый набор данных. Если API погоды возвращает JSON на 500 строк, а модели нужна только температура, отфильтруйте лишнее перед отправкой
function_response. Это экономит токены и снижает «шум». - Обработка ошибок: Если ваша функция упала (например, тикер не найден), не ломайте программу. Верните модели JSON с ошибкой:
{"error": "Ticker not found"}. Модель прочитает это и вежливо ответит пользователю: «К сожалению, я не нашла такой акции».
Создайте систему управления 'Умным домом'.<br><br>1. Напишите функцию `set_light_color(room: str, color: str, brightness: int)`, которая принимает название комнаты, цвет и яркость (от 0 до 100). Функция должна просто возвращать словарь с подтверждением действия (имитация).<br>2. Инициализируйте модель Gemini, подключив эту функцию как инструмент.<br>3. Отправьте модели запрос: 'Сделай в гостиной поуютнее, включи какой-нибудь теплый свет, но не слишком ярко, процентов на 30'.<br>4. Реализуйте ручную обработку (как в примере урока): получите вызов функции, выведите аргументы, которые выбрала модель, и отправьте результат обратно для получения финального текста.
Кто фактически выполняет программный код функции (например, делает HTTP-запрос к внешнему API) при использовании Function Calling в Gemini?