Параллельный вызов функций и обработка сложных зависимостей
Введение: Когда одной функции недостаточно
Здравствуйте, коллеги. В предыдущих модулях мы научились учить Gemini нажимать на «кнопки» — вызывать отдельные функции. Это база. Но в реальной архитектуре энтерпрайз-уровня задачи редко бывают линейными и атомарными.
Представьте, что вы строите ассистента для финансового аналитика. Пользователь спрашивает: «Сравни цены акций Apple, Google и Microsoft и скажи, стоит ли мне перекладываться».
Если мы будем действовать по старинке (последовательно), диалог будет выглядеть как мучительное ожидание:
- Модель вызывает
get_stock_price('AAPL')-> Ждем API -> Ответ. - Модель вызывает
get_stock_price('GOOGL')-> Ждем API -> Ответ. - Модель вызывает
get_stock_price('MSFT')-> Ждем API -> Ответ. - Модель анализирует.
Это медленно. Это дорого (лишние токены на вход/выход). И это скучно для пользователя. Сегодня мы разберем параллельный вызов функций (Parallel Function Calling), который позволяет модели запросить все три котировки в одном ответе, и как работать со сложными зависимостями, когда результат одной функции критически необходим для запуска другой.
Анатомия параллелизма в Gemini
Современные версии модели (начиная с 1.5 Pro и переходя к нашему контексту Gemini 3) обладают достаточным «интеллектуальным ресурсом», чтобы предвидеть необходимость нескольких действий сразу.
Как это выглядит под капотом?
Вместо того чтобы вернуть один объект function_call, модель возвращает список (массив) таких объектов в рамках одного Candidate. Ваша задача как архитектора решения — не просто принять этот список, но и корректно его обработать.
Ключевые преимущества:
- Снижение латентности: Вы делаете сетевые запросы к своим бэкендам асинхронно (одновременно), а не последовательно. Время ответа равно времени самой медленной функции, а не сумме всех функций.
- Целостность контекста: Модель видит полную картину сразу, получив пакет данных, что снижает риск галлюцинаций при сравнении данных.
Давайте посмотрим, как это реализуется в коде.
import google.generativeai as genai
from google.protobuf import struct_pb2
# 1. Определение инструментов (Tools)
# Допустим, у нас есть функция получения погоды и новостей
def get_current_weather(location: str):
"""Возвращает текущую погоду в заданном городе."""
# Имитация API запроса
print(f"[API CALL] Запрос погоды для: {location}")
return {"temperature": 25, "condition": "Sunny", "location": location}
def get_local_news(location: str):
"""Возвращает заголовки новостей для города."""
# Имитация API запроса
print(f"[API CALL] Запрос новостей для: {location}")
return {"headline": "Открытие нового парка", "location": location}
tools_list = [get_current_weather, get_local_news]
# 2. Инициализация модели с инструментами
model = genai.GenerativeModel(
model_name='gemini-1.5-pro-latest', # Используем актуальную версию
tools=tools_list
)
# 3. Запуск чата с автоматическим вызовом функций (режим auto)
chat = model.start_chat(enable_automatic_function_calling=True)
# 4. Запрос, требующий параллельности
# Обратите внимание: мы спрашиваем про два города сразу
response = chat.send_message(
"Какая погода сейчас в Лондоне и в Токио? И есть ли интересные новости в Токио?"
)
# В консоли мы увидим:
# [API CALL] Запрос погоды для: London
# [API CALL] Запрос погоды для: Tokyo
# [API CALL] Запрос новостей для: Tokyo
print(response.text)
Обработка «вручную»: Когда автоматики недостаточно
Пример выше использует enable_automatic_function_calling=True. Это удобно для прототипов. Но как Архитекторы ИИ-решений, вы часто будете работать в ситуациях, где вызов функции — это триггер для сложной бизнес-логики, требующей валидации, логирования или обращения к закрытым базам данных.
Когда вы обрабатываете вызовы вручную, алгоритм меняется:
- Отправляем промпт.
- Проверяем, содержит ли ответ части
function_call. - ВАЖНО: Итерируемся по ВСЕМ частям вызова функций. Модель может вернуть список
[Weather(London), Weather(Tokyo), News(Tokyo)]. - Выполняем их (желательно асинхронно/параллельно на уровне вашего кода, например, через
asyncio.gatherв Python). - Формируем список ответов
function_response. - Отправляем этот список обратно модели.
Если вы отправите ответы не в том порядке или забудете один из них, модель может запутаться («потерять контекст») или выдать ошибку.
# Продвинутый пример: Ручная обработка параллельных вызовов
response = chat.send_message("Сравни погоду в Париже и Берлине")
part = response.candidates[0].content.parts[0]
# Проверка на наличие вызова функций
if part.function_call:
# В реальности response.parts может содержать несколько function_call
# Нам нужно собрать их все
function_calls = []
for p in response.candidates[0].content.parts:
if p.function_call:
function_calls.append(p.function_call)
print(f"Модель хочет вызвать: {p.function_call.name} с аргументами {p.function_call.args}")
# ЭТАП ВЫПОЛНЕНИЯ (здесь должна быть ваша бизнес-логика)
# Для примера выполняем синхронно, но в продакшене тут нужен asyncio
responses = []
for fc in function_calls:
result = get_current_weather(fc.args['location'])
# Формируем ответ для Gemini
# Важно вернуть имя функции, чтобы модель знала, к чему относится ответ
responses.append(
genai.protos.Part(function_response=genai.protos.FunctionResponse(
name=fc.name,
response={'result': result}
))
)
# Отправляем результаты обратно модели
final_response = chat.send_message(responses)
print(final_response.text)
Сложные зависимости: Последовательность против Параллелизма
Параллелизм — это прекрасно, но не всегда применимо. Существует класс задач, называемый зависимыми цепочками (Dependency Chains).
Пример: «Найди последний заказ пользователя john_doe и оформи возврат по этому заказу».
Здесь мы не можем запустить refund_order(order_id) одновременно с get_last_order(user_id), потому что у нас еще нет order_id. Это классическая проблема курицы и яйца, если пытаться распараллелить всё подряд.
Паттерн «Мыслитель» (Reasoning Loop)
Для решения таких задач модель должна работать итеративно. Gemini 3 отлично справляется с этим, если правильно настроить системный промпт или структуру инструментов.
Как модель понимает, что делать?
1. Шаг 1: Модель видит запрос. Понимает, что для возврата нужен ID. Видит функцию поиска ID. Вызывает get_last_order.
2. Шаг 2: Вы возвращаете результат (например, {"id": "ORD-123", "status": "delivered"}).
3. Шаг 3: Модель получает контекст. Теперь у неё есть аргумент для следующей функции. Она вызывает refund_order("ORD-123").
Главная ошибка новичков — пытаться заставить модель «угадать» ID или пытаться сжать два шага в один промпт без промежуточного возврата управления.
# Пример зависимых функций
# База данных (mock)
users_db = {"alice": "user_999"}
orders_db = {"user_999": ["order_55a", "order_77b"]}
def find_user_id(username: str):
"""Находит ID пользователя по имени."""
return users_db.get(username, None)
def get_user_orders(user_id: str):
"""Возвращает список заказов по ID пользователя."""
if not user_id:
return "Error: User ID required"
return orders_db.get(user_id, [])
# Сценарий:
# User: "Какие заказы были у Alice?"
# 1. Модель не может вызвать get_user_orders('alice'), так как нужен ID.
# 2. Модель должна сначала вызвать find_user_id('alice').
# В Gemini это происходит естественно в цикле чата.
# Если enable_automatic_function_calling=True, библиотека сама сделает 2 цикла (turn).
chat = model.start_chat(enable_automatic_function_calling=True)
response = chat.send_message("Покажи заказы пользователя Alice")
# Под капотом:
# -> Call find_user_id('alice')
# <- Return 'user_999'
# -> Call get_user_orders('user_999')
# <- Return ['order_55a', 'order_77b']
# -> Final Answer
Обработка ошибок в сложных цепочках
Что произойдет, если в цепочке из 5 параллельных функций одна упадет с ошибкой 500? Или если в последовательной цепочке пользователь не найден?
Как архитектор, вы должны реализовать стратегии отказоустойчивости:
- Partial Failure (Частичный отказ): При параллельном вызове, если
get_weather('Tokyo')вернул ошибку, аget_weather('London')успешен, не нужно крашить весь чат. Верните модели JSON с ошибкой для Токио. Gemini достаточно умна, чтобы сказать: «Вот погода в Лондоне, а данные по Токио сейчас недоступны». - Graceful Degradation (Плавная деградация): В последовательных цепях возвращайте понятные описания ошибок, а не стектрейсы. Вместо
NullReferenceExceptionверните{"error": "User not found", "suggestion": "Ask for correct spelling"}. Это позволит модели попросить пользователя уточнить имя, вместо того чтобы галлюцинировать.
Создайте систему для планирования командировки. Вам нужно определить 3 функции (mock): 1. `get_flight_price(city)` 2. `get_hotel_price(city)` 3. `calculate_total(flight, hotel)`. <br><br>Задача: Напишите код (структуру), который обрабатывает запрос пользователя 'Сколько будет стоить поездка в Париж?'. <br><br>Учтите, что: <br>- Функции 1 и 2 должны вызываться параллельно (они независимы). <br>- Функция 3 (если вы решите доверить расчет модели, а не делать это внутри) или финальный ответ модели зависят от результатов первых двух.
У вас есть сценарий: Ассистент должен забронировать столик в ресторане, но сначала нужно проверить наличие свободных мест. Функция `book_table` требует `reservation_token`, который возвращает функция `check_availability`. Какой тип вызова функций выберет Gemini и почему?