Обработка ошибок и галлюцинаций при вызове инструментов
Введение: Когда «Счастливый путь» заканчивается
Добро пожаловать в третий модуль. К этому моменту вы уже научились определять инструменты и подключать их к модели Gemini. В идеальном мире (или в демо-роликах) пользователь всегда вводит четкий запрос, модель идеально извлекает параметры, API отвечает за миллисекунды, и все счастливы. Это называется Happy Path.
Но как архитектор ИИ-решений, вы знаете: реальность хаотична. Пользователи пишут с опечатками, требуют невозможного, а внешние API падают или меняют свои контракты без предупреждения. Что происходит, когда Gemini пытается вызвать функцию get_weather(city), а пользователь спрашивает погоду в «Городе, которого нет»? Или когда модель решает, что в функцию нужно передать параметр date, которого в вашем коде даже не существует?
В этом уроке мы разберем темную сторону Function Calling: обработку ошибок и галлюцинаций. Мы перейдем от простого скриптинга к созданию устойчивых (resilient) агентных систем, способных к самовосстановлению.
Что мы разберем:
- Типология сбоев: От невалидного JSON до логических галлюцинаций.
- Превентивная защита: Как писать схемы инструментов так, чтобы у модели не было шанса ошибиться.
- Паттерн «Петля обратной связи» (Error Feedback Loop): Как заставить Gemini исправлять свои же ошибки.
- Борьба с галлюцинациями: Техники заземления (grounding) при вызове функций.
Анатомия ошибки при вызове инструментов
Прежде чем лечить, нужно поставить диагноз. Ошибки при работе с Function Calling в Gemini можно разделить на три большие категории. Понимание категории определяет стратегию защиты.
1. Синтаксические и структурные ошибки (Validation Errors)
Это ситуация, когда модель генерирует аргументы, которые не соответствуют вашей схеме (JSON Schema/Pydantic).
Примеры:
- Ожидается
int, модель присылает строку"два". - Ожидается выбор из
enum["USD", "EUR"], модель присылает"RUB". - Модель пропускает обязательный аргумент.
Хорошая новость: SDK Google Generative AI и современные библиотеки (вроде LangChain или LlamaIndex) часто перехватывают это на этапе валидации Pydantic.
2. Галлюцинации инструментов (Tool Hallucinations)
Самый коварный тип. Модель синтаксически формирует верный запрос, но семантически он ложен.
Примеры:
- Выдуманные параметры: Модель добавляет аргумент
include_comments=Trueв функцию, которая этого не поддерживает, потому что «подумала», что так будет лучше. - Выдуманные функции: Модель пытается вызвать функцию
send_email, хотя вы предоставили ей толькоread_email. - Галлюцинации значений: Пользователь просит найти заказ «от вчера», а модель подставляет произвольную дату, не проверив текущее число.
3. Ошибки исполнения (Runtime Errors)
Модель все сделала правильно, но внешний мир подвел.
Примеры:
- API вернуло 404 или 500.
- База данных недоступна.
- Превышен лимит времени (timeout).
Ключевая ошибка начинающих разработчиков — просто выбрасывать исключение и прерывать диалог. Архитектор поступает иначе: он превращает ошибку в контекст.
import google.generativeai as genai
from google.protobuf import struct_pb2
# Типичный сценарий: Модель пытается вызвать инструмент с несуществующим аргументом
# Представим, что у нас есть функция поиска товаров, но модель "додумывает" фильтр
# Определяем инструмент (упрощенно)
tools_schema = {
"function_declarations": [
{
"name": "search_products",
"description": "Ищет товары в каталоге по названию",
"parameters": {
"type": "OBJECT",
"properties": {
"query": {"type": "STRING", "description": "Название товара"}
# Обратите внимание: параметра 'max_price' здесь НЕТ
},
"required": ["query"]
}
}
]
}
model = genai.GenerativeModel('gemini-1.5-pro-latest', tools=[tools_schema])
chat = model.start_chat()
# Пользовательский запрос, провоцирующий галлюцинацию параметра
response = chat.send_message("Найди мне ноутбук дешевле 1000 долларов")
# В ответе мы можем получить вызов функции с аргументом 'max_price',
# которого нет в схеме, если модель недостаточно строго следует инструкции.
# Задача архитектора — перехватить это ДО того, как код упадет.
Стратегия 1: Оборона через определение (Schema Engineering)
Лучший способ исправить ошибку — не допустить её. Качество работы Gemini с инструментами напрямую зависит от качества описаний (Docstrings) и схем параметров. Это и есть Schema Engineering.
Многие разработчики пишут:
def get_data(id: str):
"""Получает данные."""Это катастрофа для LLM. Модель не понимает, что такое id, каков его формат (UUID? Int? Email?) и что вернет функция. Когда модель находится в состоянии неопределенности, она начинает «фантазировать», чтобы заполнить пробелы.
Принципы надежной схемы:
- Описания (Description) обязательны: Для каждого поля, даже очевидного. Объясняйте не только «что» это, но и «в каком формате».
Плохо:date
Хорошо:date: str - Дата транзакции в формате ISO 8601 (YYYY-MM-DD). - Используйте Enums: Если значений ограниченное количество, жестко задайте их через Enum. Это физически ограничивает пространство для галлюцинаций.
- Сложные инструкции в описании функции: Если у функции есть неочевидные ограничения (например, «не вызывать для городов с населением меньше 1000 человек»), напишите это прямо в docstring функции. Gemini читает это как часть системного промпта.
from pydantic import BaseModel, Field
from enum import Enum
# --- ПЛОХОЙ ПРИМЕР ---
def book_flight_bad(destination: str, seat: str):
"""Бронирует рейс."""
pass
# --- ХОРОШИЙ ПРИМЕР (Архитектурный подход) ---
class SeatType(str, Enum):
ECONOMY = "economy"
BUSINESS = "business"
FIRST = "first_class"
class FlightBookingParams(BaseModel):
destination: str = Field(
...,
description="Трехбуквенный IATA код аэропорта назначения (например, JFK, LHR, SVO). Не название города!"
)
seat_type: SeatType = Field(
SeatType.ECONOMY,
description="Класс обслуживания. По умолчанию эконом."
)
passengers: int = Field(
1,
ge=1,
le=9,
description="Количество пассажиров. Максимум 9 человек в одном бронировании."
)
def book_flight_good(params: FlightBookingParams):
"""
Бронирует авиабилет.
ВАЖНО: Перед вызовом убедитесь, что пользователь указал конкретную дату вылета в чате.
Если даты нет, спросите пользователя, не вызывайте функцию.
"""
# Логика бронирования...
pass
Стратегия 2: Петля самокоррекции (Self-Correction Loop)
Даже с идеальной схемой ошибки случаются. API погоды вернет ошибку, если города не существует. База данных может отвалиться по таймауту.
В классическом программировании мы логируем ошибку и говорим пользователю: «Что-то пошло не так». В разработке ИИ-агентов мы используем ошибку как топливо для размышлений.
Алгоритм Self-Correction:
- Модель присылает запрос на вызов функции (Function Call).
- Ваш код пытается выполнить функцию.
- Возникает исключение (Exception) или валидация не проходит.
- Вместо того чтобы прерывать диалог, вы перехватываете ошибку.
- Вы формируете сообщение с ролью
function(илиuserв некоторых контекстах), содержащее текст ошибки. - Вы отправляете эту историю обратно в Gemini.
Gemini видит: «Я попыталась вызвать get_weather('Narnia'), но получила ответ Error: City not found».
Реакция модели: «Ага, значит, такого города нет. Нужно извиниться перед пользователем и уточнить название».
Это превращает «глупого» бота в «умного» помощника, который может исправлять свои же опечатки в параметрах или менять стратегию на ходу.
# Пример реализации цикла самокоррекции
def safe_tool_executor(chat_session, user_prompt, max_retries=3):
# Отправляем исходный запрос пользователя
response = chat_session.send_message(user_prompt)
retries = 0
# Пока модель хочет вызывать функции...
while response.parts[0].function_call and retries < max_retries:
call = response.parts[0].function_call
function_name = call.name
args = call.args
print(f"🤖 Попытка вызова: {function_name} с аргументами {args}")
try:
# Симуляция выполнения функции
if function_name == "get_weather":
if "Narnia" in args.get("city", ""):
raise ValueError("Error 404: City 'Narnia' does not exist in the database.")
api_result = {"temperature": 25, "condition": "Sunny"}
else:
raise NotImplementedError(f"Function {function_name} is not defined")
# Успешный вызов - возвращаем результат модели
response = chat_session.send_message(
genai.protos.Content(
parts=[genai.protos.Part(function_response=
genai.protos.FunctionResponse(
name=function_name,
response={"result": api_result}
)
)]
)
)
except Exception as e:
# МАГИЯ ЗДЕСЬ: Мы не падаем, мы скармливаем ошибку обратно модели
error_message = f"SystemError: Execution failed. {str(e)}. Try to fix arguments or ask user for clarification."
print(f"⚠️ Ошибка перехвачена, отправляем модели: {error_message}")
# Отправляем результат функции как ошибку
response = chat_session.send_message(
genai.protos.Content(
parts=[genai.protos.Part(function_response=
genai.protos.FunctionResponse(
name=function_name,
response={"error": error_message} # Важно вернуть это как JSON
)
)]
)
)
retries += 1
return response.text
Стратегия 3: Обработка галлюцинаций (Когда модели "кажется")
Иногда модель не ошибается технически, но ошибается логически. Например, пользователь спрашивает: «Какой у меня баланс?», а модель вызывает функцию get_balance(user_id='12345'), просто выдумав ID, потому что он обязателен в схеме.
Как бороться с галлюцинацией параметров:
- Сделайте параметры опциональными (Optional): В Pydantic или JSON Schema пометьте поля как необязательные, если их может не быть в контексте.
- Значение «Неизвестно»: Явно пропишите в System Instruction: «Если пользователь не предоставил значение для аргумента Х, передай строку 'UNKNOWN' или null, не выдумывай значение».
- Проверка «Здравого смысла» (Sanity Check): Перед выполнением критически важных действий (перевод денег, удаление данных) добавьте слой логики на Python, который проверяет аргументы. Если аргумент выглядит подозрительно (например, дата в будущем для события в прошлом), выбросьте ошибку валидации и верните её модели через цикл самокоррекции.
Итоговый чек-лист архитектора
- ✅ Все функции имеют подробные docstrings.
- ✅ Сложные параметры описаны через Enums.
- ✅ Реализован цикл
try-except-repromptдля отправки стектрейсов обратно в LLM. - ✅ Системный промпт явно запрещает угадывание критических параметров.
- ✅ Установлен лимит на количество попыток самокоррекции (чтобы избежать бесконечных циклов ошибок).
Создайте отказоустойчивый агент калькулятор. У вас есть функция `divide(a, b)`. Модель должна обработать запрос пользователя «Подели 10 на 0». <br><br>Требования:<br>1. Реализуйте функцию деления, которая выбрасывает исключение при делении на ноль.<br>2. Реализуйте цикл обработки вызовов.<br>3. Когда модель вызовет деление на ноль, ваш код должен вернуть ошибку модели.<br>4. Модель должна получить ошибку и сгенерировать естественный ответ пользователю: «К сожалению, на ноль делить нельзя».
Почему при возникновении исключения (Exception) во время выполнения инструмента рекомендуется отправлять текст ошибки обратно в модель, а не просто выводить его в лог?