Управление галлюцинациями при работе с внешними инструментами

35 минут Урок 14

Введение: Когда модель «нажимает» несуществующие кнопки

Добро пожаловать в один из самых критически важных уроков курса. Если в предыдущих модулях мы обсуждали, как научить Gemini 3 пользоваться инструментами, то сегодня мы поговорим о том, как запретить ей пользоваться ими неправильно. Галлюцинации в текстовой генерации — это неприятно (например, модель выдумала факт из биографии исторической личности). Но галлюцинации в Function Calling — это опасно.

Представьте, что вы разрабатываете банковского ассистента. Пользователь пишет: «Переведи 500 рублей маме». Модель вызывает функцию transfer_money. Но если она «додумает» аргумент currency='USD' вместо rub, потому что в обучающей выборке доллары встречались чаще, вы получите очень рассерженного клиента. Или, что хуже, модель может вызвать функцию delete_account, которой вообще нет в текущем контексте, но она «помнит» её из какого-то другого диалога.

В этом уроке мы разберем анатомию галлюцинаций при работе с инструментами (Tools) и выстроим эшелонированную оборону: от проектирования схемы данных до валидации на стороне клиента.

Типология галлюцинаций при вызове функций

Чтобы бороться с врагом, его нужно знать в лицо. В контексте Gemini 3 API и внешних инструментов, ошибки делятся на три основные категории:

  • Галлюцинация существования (Phantom Function Calls): Модель пытается вызвать функцию, которую вы ей не передавали в списке tools. Это часто случается, если имя функции интуитивно понятно (например, send_email), и модель решает, что такая функция должна быть.
  • Галлюцинация аргументов (Parameter Hallucination): Самый частый тип. Модель вызывает правильную функцию, но придумывает значения аргументов.
    • Пример: Функция требует дату в формате YYYY-MM-DD, а модель отправляет завтра или next_tuesday.
    • Пример: Модель вставляет значение в необязательный параметр, основываясь на догадках, а не на запросе пользователя.
  • Игнорирование вывода (Output Neglect): Модель получает результат от инструмента (например, «На счете недостаточно средств»), но в итоговом ответе пользователю игнорирует это и пишет: «Перевод успешно выполнен».

Gemini 3 значительно лучше следует инструкциям, чем предыдущие поколения, но вероятностную природу LLM никто не отменял. Ваша задача — свести вероятность ошибки к статистической погрешности.

Стратегия №1: Железная Схема (Schema Definition)

Многие разработчики совершают ошибку, относясь к описанию функций как к вторичной задаче. Они пишут: param: query, type: string, description: поисковый запрос. Этого недостаточно.

Для LLM описание функции и её параметров — это единственная инструкция по эксплуатации. Чем точнее вы ограничите область допустимых значений на уровне схемы, тем меньше пространства для фантазии останется у модели.

Ключевые приемы в Pydantic / JSON Schema:

  1. Enums (Перечисления): Никогда не используйте string, если количество вариантов конечно. Если статус может быть только active или pending, жестко задайте это.
  2. Подробные описания (Docstrings): Описывайте не что это за поле, а зачем оно нужно и какой формат ожидается.
  3. Примеры в описании: Прямо в поле description указывайте: "Например: 'Москва', не 'MSK'".

python
import enum
from typing import Optional
from pydantic import BaseModel, Field

# ❌ ПЛОХОЙ ПРИМЕР: Слишком общая схема
# Модель может отправить "Moscow", "MOW", "г. Москва" или вообще выдуманный город.
class WeatherRequestBad(BaseModel):
    city: str
    days: int

# ✅ ХОРОШИЙ ПРИМЕР: Строгая типизация и контекст
class CityChoice(str, enum.Enum):
    MOSCOW = "Moscow"
    LONDON = "London"
    NEW_YORK = "New York"
    TOKYO = "Tokyo"

class WeatherRequestGood(BaseModel):
    city: CityChoice = Field(
        ..., 
        description="Город, для которого запрашивается прогноз. Выбирай только из доступного списка."
    )
    days: int = Field(
        ..., 
        ge=1, 
        le=7, 
        description="Количество дней для прогноза. Целое число от 1 до 7. Если пользователь просит 'неделю', ставь 7."
    )
    include_hourly: bool = Field(
        default=False,
        description="Установи True ТОЛЬКО если пользователь явно просит почасовой прогноз."
    )

# При передаче в Gemini, Pydantic схема конвертируется в JSON Schema,
# где ограничения ge (greater or equal) и le (less or equal) станут четкими инструкциями.

print(WeatherRequestGood.model_json_schema())

Стратегия №2: Валидация и петля самокоррекции (Self-Correction Loop)

Даже с идеальной схемой модель может ошибиться. Здесь в игру вступает программный код. Никогда не доверяйте вводу от LLM напрямую в вашу бизнес-логику.

Паттерн Self-Correction работает так:

  1. Модель присылает запрос на вызов функции (Function Call).
  2. Ваш код проверяет аргументы (валидация типов, диапазонов значений, бизнес-правил).
  3. Если валидация не проходит, вы не показываете ошибку пользователю.
  4. Вы отправляете ошибку обратно модели в роль function_response или user с сообщением: «Ошибка аргумента: дней должно быть не больше 7. Попробуй снова».
  5. Модель, видя ошибку, генерирует исправленный вызов функции.

Это скрывает технические детали от конечного пользователя и использует интеллект Gemini для исправления собственных ошибок.

python
import google.generativeai as genai
from google.protobuf.struct_pb2 import Struct

# Имитация функции отправки заказа
def validate_and_process_order(item_id: str, quantity: int):
    # Слой валидации (Guardrails)
    if quantity <= 0:
        return {"error": "Quantity must be a positive integer."}
    if quantity > 50:
        return {"error": "Bulk orders over 50 items must be processed via manager. Reduce quantity."}
    
    # Если всё ок — имитируем успех
    return {"status": "success", "order_id": "12345", "message": f"Ordered {quantity} of {item_id}"}

# Пример цикла обработки (упрощенная логика)
def chat_with_validation(model, chat_session, user_prompt):
    response = chat_session.send_message(user_prompt)
    
    # Проверяем, хочет ли модель вызвать функцию
    if response.candidates[0].content.parts[0].function_call:
        fc = response.candidates[0].content.parts[0].function_call
        fname = fc.name
        args = fc.args
        
        print(f"🤖 Модель пытается вызвать: {fname} с аргументами {args}")
        
        # Выполняем валидацию
        if fname == "order_item":
            result = validate_and_process_order(args['item_id'], int(args['quantity']))
            
            # Если валидация вернула ошибку, скармливаем её обратно модели
            if "error" in result:
                print(f"⚠️ Обнаружена ошибка валидации: {result['error']}. Запрашиваем исправление...")
                
                # Отправляем результат работы функции (с ошибкой) обратно в чат
                # Gemini 3 проанализирует ошибку и попытается вызвать функцию снова с новыми параметрами
                function_response_part = genai.protos.Part(
                    function_response=genai.protos.FunctionResponse(
                        name=fname,
                        response={"result": result} # Передаем JSON с ошибкой
                    )
                )
                
                # Рекурсивный вызов или продолжение диалога
                correction_response = chat_session.send_message([function_response_part])
                return correction_response.text
            else:
                return f"Заказ успешно создан: {result}"
                
    return response.text

# Обратите внимание: Мы не падаем с исключением, мы даем модели шанс исправиться.

Стратегия №3: Системные инструкции и «Невозможный» вариант

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

Решение: Явно разрешите модели отказывать.

В system_instruction добавьте:

«У тебя есть доступ к инструментам A, B и C. Если запрос пользователя не может быть решен с помощью этих инструментов, НЕ пытайся выдумывать параметры. Просто ответь, что ты не можешь выполнить это действие. Твоя приоритетная задача — безопасность данных, а не выполнение команды любой ценой.»

Паттерн "No-Op Tool":
В некоторых сложных сценариях полезно добавить фиктивную функцию escalate_to_human(reason: str). Это дает модели легальный «выход» из ситуации, когда она не уверена в параметрах. Вместо галлюцинации она вызовет эту функцию, и вы сможете переключить диалог на оператора.

Управление галлюцинациями в ответе (Response Hallucinations)

Предположим, функция сработала корректно и вернула JSON: {"status": "error", "code": 404, "msg": "Товар не найден"}. Галлюцинация ответа возникает, когда модель, получив это, говорит пользователю: «Я нашла товар, оформляю заказ».

Это происходит из-за того, что внимание модели (Attention) смещается на первоначальный запрос пользователя («найди товар»), игнорируя технический ответ инструмента.

Методы борьбы:

  • Четкие имена полей в ответе: Возвращайте не просто {"result": "no"}, а семантически нагруженный ответ: {"search_status": "not_found", "action_taken": "none", "suggested_reply": "Сообщи пользователю, что товара нет на складе"}. Вы буквально вкладываете в уста модели нужную фразу через JSON ответа.
  • Снижение Temperature: Для задач, связанных с точным репортингом действий инструментов, используйте температуру близкую к 0 (например, 0.1 или 0.2). Это снижает креативность в интерпретации фактов.

Упражнение

У вас есть бот для бронирования столиков. В нём есть функция `book_table(time: str, guests: int)`. Пользователи часто пишут: «Забронируй столик на вечер для большой компании». <br><br>Модель часто галлюцинирует, подставляя `time='18:00'` и `guests=5`, хотя пользователь этого не уточнял. <br><br>Задача: Напишите (на бумаге или в редакторе) определение Pydantic схемы для этой функции так, чтобы заставить модель переспросить пользователя, если данные не точны. Используйте `Optional` и описание полей.

Вопрос

Какая стратегия наиболее эффективна для предотвращения галлюцинаций значений аргументов (например, когда модель придумывает несуществующий ID товара)?