Параллельный вызов функций и сложные цепочки зависимостей
Введение: Когда одной функции недостаточно
Приветствую, коллеги. В предыдущих уроках мы научили Gemini нажимать на «кнопки» — вызывать отдельные функции для получения погоды или текущего времени. Это база. Но реальный мир разработки редко бывает линейным и однозадачным.
Представьте, что вы строите ассистента для путешествий. Пользователь пишет: «Спланируй мне выходные в Париже: найди билеты, забронируй отель и посмотри, какая будет погода».
Если мы будем действовать по старинке, процесс будет мучительно медленным:
- Модель вызывает поиск билетов... (ждем API)... получает ответ.
- Модель вызывает поиск отелей... (ждем API)... получает ответ.
- Модель вызывает прогноз погоды... (ждем API)... получает ответ.
Это называется последовательная блокировка. В Gemini 3 API мы можем (и должны!) использовать параллельный вызов функций (Parallel Function Calling). Это позволяет модели запросить выполнение нескольких действий одновременно в рамках одного шага генерации.
Сегодня мы разберем не только параллелизм, но и более хитрую тему — сложные цепочки зависимостей. Это ситуации, когда результат выполнения функции А необходим для запуска функции Б, но функции В и Г могут работать независимо.
Анатомия параллельного вызова
На техническом уровне разница между одиночным и параллельным вызовом кроется в структуре ответа модели. В ранних версиях LLM, если модель хотела вызвать несколько инструментов, она часто галлюцинировала или пыталась впихнуть все в один вызов. Gemini 3 нативно поддерживает список вызовов.
Как это работает под капотом:
- Вы передаете список доступных инструментов (`tools`) при инициализации чата.
- Модель анализирует промпт и понимает, что для ответа нужно несколько независимых кусков данных.
- Вместо одного объекта `function_call`, модель возвращает список (массив) таких объектов.
- Ваша задача как разработчика — пройтись по этому списку, выполнить функции (желательно тоже асинхронно или параллельно) и вернуть список результатов `function_response` в том же порядке.
Давайте посмотрим на код.
import google.generativeai as genai
from google.protobuf import struct_pb2
import time
# Настройка заглушек для функций (Mock functions)
# В реальности здесь были бы запросы к внешним API
def search_flights(destination, date):
"""Ищет авиабилеты в указанное место на дату."""
print(f"[API] Поиск билетов в {destination} на {date}...")
return {"flight": "AF123", "price": "300 EUR", "status": "available"}
def search_hotels(location, date):
"""Ищет отели в указанной локации."""
print(f"[API] Поиск отелей в {location} на {date}...")
return {"hotel": "Grand Hotel", "price": "150 EUR/night", "rating": 4.8}
def get_weather_forecast(city, date):
"""Получает прогноз погоды."""
print(f"[API] Запрос погоды для {city}...")
return {"temp": "18C", "condition": "Partly Cloudy"}
# Регистрация инструментов
tools_list = [search_flights, search_hotels, get_weather_forecast]
# Инициализация модели
model = genai.GenerativeModel(
model_name='gemini-1.5-pro-latest', # Используем актуальную версию, поддерживающую function calling
tools=tools_list
)
chat = model.start_chat(enable_automatic_function_calling=True)
# В библиотеке Python SDK параметр enable_automatic_function_calling=True
# берет на себя всю "грязную работу":
# 1. Получает запрос от модели с несколькими вызовами.
# 2. Выполняет их (последовательно или параллельно внутри библиотеки).
# 3. Отправляет результаты обратно модели.
response = chat.send_message(
"Я хочу полететь в Париж 15 мая. Найди билеты, отель и скажи, какая там будет погода."
)
print("\n--- Ответ Модели ---")
print(response.text)
Ручное управление: когда автоматика не справляется
Использование `enable_automatic_function_calling=True` — это удобно, но настоящему инженеру нужно понимать, как обрабатывать вызовы вручную. Это критически важно для:
- Обработки ошибок (если один API упал, мы не хотим, чтобы упал весь чат).
- Оптимизации (реального асинхронного выполнения запросов через `asyncio`).
- Сложной логики валидации перед отправкой в API.
При ручной обработке вы получите объект `part`, содержащий `function_call`. Если модель решила вызвать несколько функций, вы увидите несколько таких частей в ответе.
Ключевое правило: Порядок ответов должен соответствовать порядку вызовов, или же ответы должны быть четко именованы, чтобы Gemini сопоставила результат с запросом.
Цепочки зависимостей (Dependency Chains)
Параллелизм — это прекрасно, но что делать, если данные нужны последовательно? Это классическая проблема "Курица и яйцо" в контексте LLM.
Пример сценария:
«Проверь статус моего последнего заказа и, если он доставлен, отправь email с просьбой оставить отзыв на товары из этого заказа».
Здесь явная зависимость:
1. Сначала нужно получить ID последнего заказа и его статус.
2. ТОЛЬКО ЕСЛИ статус == 'delivered', нужно получить список товаров.
3. Затем отправить email.
Gemini не может вызвать функцию отправки email сразу, так как она не знает, о каких товарах писать. Здесь модель должна проявить рассуждение (reasoning).
Паттерн ReAct (Reason + Act):
Модель делает шаг, получает результат, думает, делает следующий шаг. В Gemini это реализуется через multi-turn conversation (многоходовой диалог).
# Пример ручной обработки цепочки зависимостей
# Инструменты
def get_latest_order_id(user_id):
return "ORDER-999"
def get_order_status(order_id):
if order_id == "ORDER-999":
return "delivered"
return "processing"
def send_feedback_email(order_id):
return f"Email sent for {order_id}"
funcs = {
'get_latest_order_id': get_latest_order_id,
'get_order_status': get_order_status,
'send_feedback_email': send_feedback_email
}
# Симуляция цикла обработки (упрощенная)
messages = []
messages.append({'role': 'user', 'parts': ["Проверь мой последний заказ (user_123) и если доставлен, отправь письмо с просьбой отзыва."]})
# ШАГ 1: Модель понимает, что сначала нужен ID
# Response Model: function_call: get_latest_order_id(user_id='user_123')
# ШАГ 2: Мы исполняем и возвращаем результат
# Tool Output: "ORDER-999"
# ШАГ 3: Модель получает "ORDER-999". Теперь она понимает, что нужно проверить статус.
# Response Model: function_call: get_order_status(order_id='ORDER-999')
# ШАГ 4: Мы исполняем
# Tool Output: "delivered"
# ШАГ 5: Модель видит "delivered". Согласно инструкции, теперь можно слать email.
# Response Model: function_call: send_feedback_email(order_id='ORDER-999')
# ШАГ 6: Финальный ответ пользователю.
# Model: "Я проверила ваш заказ ORDER-999. Он доставлен, поэтому я отправила запрос на отзыв."
Графы выполнения (DAG) и гибридный подход
Высший пилотаж — это комбинирование параллельных и последовательных вызовов. Это превращает процесс выполнения не в линию, а в Направленный Ациклический Граф (DAG).
Представьте задачу финансового аналитика:
«Сравни выручку Apple и Microsoft за 2023 год и построй график».
- Этап 1 (Параллельный): Можно одновременно вызвать `get_revenue('AAPL', 2023)` и `get_revenue('MSFT', 2023)`. Они независимы.
- Этап 2 (Агрегация): Получив оба числа, модель переходит к следующему шагу.
- Этап 3 (Зависимый): Вызов функции `plot_comparison_chart(data_aapl, data_msft)`. Эта функция не может быть вызвана на Этапе 1.
Совет эксперта:
Чтобы Gemini успешно строила такие сложные планы, критически важны Docstrings (описания функций). Вы должны явно указывать в описании аргументов, что они зависят от других данных. Например: "param sales_data: Результат выполнения функции get_revenue". Это подсказывает модели, что нужно дождаться данных.
Создайте сценарий для 'Умного дома'. У вас есть три функции:<br>1. `get_temperature(room)` - возвращает текущую температуру.<br>2. `set_ac_mode(room, mode)` - включает кондиционер ('cool', 'heat', 'off').<br>3. `send_alert(message)` - отправляет уведомление на телефон.<br><br>ЗАДАЧА: Напишите логику (псевдокод или python), которая обрабатывает запрос: «Проверь температуру в гостиной и спальне. Если где-то выше 25 градусов, включи охлаждение в этой комнате и уведоми меня».<br>Обратите внимание: проверку температур нужно сделать параллельно, а включение кондиционера — только по условию.
Лучшие практики и подводные камни
- Ограничение контекста: При параллельном вызове 10 функций вы получите огромный JSON с результатами. Следите за размером контекстного окна (Token Limit), хотя в Gemini 1.5/Pro оно огромное, лишний мусор лучше фильтровать.
- Атомарность функций: Делайте функции маленькими и конкретными. Лучше иметь `get_weather` и `get_time` отдельно, чем `get_info`, которая делает всё сразу непредсказуемым образом.
- Обработка отказов: Если в параллельном батче одна функция вернула ошибку API (например, 500 Server Error), передавайте это в модель как текст ошибки: `{"error": "Service unavailable"}`. Gemini достаточно умна, чтобы попробовать снова или извиниться перед пользователем, не ломая остальные ветки диалога.
В каком случае использование параллельного вызова функций (Parallel Function Calling) НЕВОЗМОЖНО или приведет к ошибке логики?