Параллельный вызов функций и сложные цепочки зависимостей

40 минут Урок 12

Введение: Когда одной функции недостаточно

Приветствую, коллеги. В предыдущих уроках мы научили Gemini нажимать на «кнопки» — вызывать отдельные функции для получения погоды или текущего времени. Это база. Но реальный мир разработки редко бывает линейным и однозадачным.

Представьте, что вы строите ассистента для путешествий. Пользователь пишет: «Спланируй мне выходные в Париже: найди билеты, забронируй отель и посмотри, какая будет погода».

Если мы будем действовать по старинке, процесс будет мучительно медленным:

  1. Модель вызывает поиск билетов... (ждем API)... получает ответ.
  2. Модель вызывает поиск отелей... (ждем API)... получает ответ.
  3. Модель вызывает прогноз погоды... (ждем API)... получает ответ.

Это называется последовательная блокировка. В Gemini 3 API мы можем (и должны!) использовать параллельный вызов функций (Parallel Function Calling). Это позволяет модели запросить выполнение нескольких действий одновременно в рамках одного шага генерации.

Сегодня мы разберем не только параллелизм, но и более хитрую тему — сложные цепочки зависимостей. Это ситуации, когда результат выполнения функции А необходим для запуска функции Б, но функции В и Г могут работать независимо.

Анатомия параллельного вызова

На техническом уровне разница между одиночным и параллельным вызовом кроется в структуре ответа модели. В ранних версиях LLM, если модель хотела вызвать несколько инструментов, она часто галлюцинировала или пыталась впихнуть все в один вызов. Gemini 3 нативно поддерживает список вызовов.

Как это работает под капотом:

  • Вы передаете список доступных инструментов (`tools`) при инициализации чата.
  • Модель анализирует промпт и понимает, что для ответа нужно несколько независимых кусков данных.
  • Вместо одного объекта `function_call`, модель возвращает список (массив) таких объектов.
  • Ваша задача как разработчика — пройтись по этому списку, выполнить функции (желательно тоже асинхронно или параллельно) и вернуть список результатов `function_response` в том же порядке.

Давайте посмотрим на код.

python
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 (многоходовой диалог).

python
# Пример ручной обработки цепочки зависимостей

# Инструменты
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. Этап 1 (Параллельный): Можно одновременно вызвать `get_revenue('AAPL', 2023)` и `get_revenue('MSFT', 2023)`. Они независимы.
  2. Этап 2 (Агрегация): Получив оба числа, модель переходит к следующему шагу.
  3. Этап 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) НЕВОЗМОЖНО или приведет к ошибке логики?