Создание динамических инструментов на основе контекста пользователя

55 минут Урок 15

Введение: Проблема статических инструментов

Приветствую! Мы продолжаем углубляться в возможности Gemini 3 API. В предыдущих уроках мы научились объявлять функции и получать от модели структурированные запросы на их выполнение. Обычно это выглядит так: вы заранее описываете список всех возможных функций (get_weather, send_email, query_database) и передаете его модели при инициализации чата.

Но что происходит, когда ваше приложение масштабируется? Представьте, что вы строите корпоративного ассистента, у которого есть доступ к сотням различных инструментов. Или представьте систему, где набор доступных действий зависит от того, кто именно общается с ботом — стажер или администратор.

Проблемы статического подхода:

  • Переполнение контекстного окна: Передача описаний 500 функций съедает токены и деньги, даже если 99% из них не нужны в данный момент.
  • Снижение точности (галлюцинации): Чем больше инструментов вы даете модели, тем выше шанс, что она выберет не тот или запутается в похожих названиях.
  • Безопасность: Если вы передаете определение функции delete_user в контекст, но не планируете давать пользователю право её вызывать, вы полагаетесь только на "честность" модели, что является плохой практикой.

В этом уроке мы перейдем к архитектуре динамических инструментов. Мы научимся формировать список tools на лету, опираясь на контекст пользователя, состояние сессии и даже семантический поиск.

Стратегия 1: Фильтрация по правам доступа (RBAC)

Самый простой и надежный способ сделать инструменты динамическими — это внедрить систему контроля доступа на основе ролей (Role-Based Access Control) ещё до того, как вы сформируете запрос к API Gemini.

Вместо того чтобы хардкодить список инструментов в вызове model.generate_content, мы создадим Реестр Инструментов. Каждый инструмент в этом реестре будет иметь метаданные, указывающие, кто имеет право его использовать.

Алгоритм действий:

  1. Идентифицируем пользователя (например, получаем user_id и его роль из базы данных).
  2. Пробегаем по нашему реестру функций.
  3. Отбираем только те функции, которые разрешены для текущей роли.
  4. Генерируем JSON-схемы только для отобранных функций.
  5. Отправляем в Gemini «чистый» список, специально собранный под этого пользователя.

Это решает проблему безопасности: модель даже не узнает о существовании административных функций, если с ней общается обычный пользователь.

python
import inspect
from typing import Callable, List, Dict, Any

# 1. Создаем декоратор для регистрации инструментов с правами доступа
TOOL_REGISTRY = {}

def register_tool(roles: List[str]):
    def decorator(func):
        TOOL_REGISTRY[func.__name__] = {
            "func": func,
            "roles": roles,
            "schema": generate_schema_for(func) # Условная функция генерации JSON-схемы
        }
        return func
    return decorator

# 2. Определяем функции с разными уровнями доступа

@register_tool(roles=["user", "admin"])
def get_product_price(product_name: str):
    """Возвращает цену товара."""
    # Логика получения цены
    return "100$"

@register_tool(roles=["admin"])
def change_product_price(product_name: str, new_price: int):
    """Изменяет цену товара. Только для админов."""
    # Логика изменения цены
    return "Price updated"

# 3. Функция для динамического формирования tools

def get_tools_for_user(user_role: str) -> List[Dict]:
    allowed_tools = []
    for tool_name, tool_info in TOOL_REGISTRY.items():
        if user_role in tool_info["roles"]:
            # Добавляем схему инструмента, понятную для Gemini
            allowed_tools.append(tool_info["schema"])
    return allowed_tools

# Пример использования
current_user_role = "user"
tools_for_gemini = get_tools_for_user(current_user_role)

# В tools_for_gemini попадет только get_product_price,
# модель физически не сможет вызвать change_product_price.
print(f"Инструменты для роли {current_user_role}: {len(tools_for_gemini)}")

Стратегия 2: Контекстная инъекция параметров

Вторая важная концепция динамических инструментов — это скрытие контекстных параметров. Часто функции требуют данные, которые модель не должна спрашивать у пользователя, потому что система их уже знает.

Рассмотрим пример функции get_order_history(user_id: str).

Если вы передадите эту сигнатуру в Gemini как есть, модель может спросить пользователя: "Пожалуйста, укажите ваш user_id". Это плохой UX и дыра в безопасности (пользователь может назваться чужим ID).

Решение: Мы изменяем определение функции для модели, удаляя из неё user_id, но оставляем его в реальной функции Python. Когда модель вызывает функцию, мы «подмешиваем» (inject) ID текущего пользователя программно.

Как это реализуется архитектурно:

  • Определение для LLM: get_order_history() — без аргументов.
  • Реальная функция: get_order_history(user_id).
  • Прослойка (Middleware): Перехватывает вызов от модели, видит, что не хватает user_id, берет его из session_state и вызывает реальную функцию с полным набором аргументов.

python
from google.generativeai.types import FunctionDeclaration, Tool

# Предположим, у нас есть контекст текущей сессии
session_context = {
    "user_id": "u_12345",
    "location": "Europe/Moscow"
}

# Реальная функция, которая требует user_id
def fetch_orders_backend(user_id: str, status: str = "active"):
    print(f"Запрос к БД для пользователя {user_id} со статусом {status}")
    return ["Order #1", "Order #2"]

# 1. Создаем 'урезанную' декларацию для Gemini
# Мы говорим модели, что user_id ей знать не нужно
fetch_orders_tool = FunctionDeclaration(
    name="fetch_orders",
    description="Получить список заказов текущего пользователя",
    parameters={
        "type": "object",
        "properties": {
            "status": {
                "type": "string",
                "description": "Статус заказа (active, delivered)"
            }
        },
        "required": ["status"]
    }
)

# 2. Обработчик вызова (Dispatcher)
def handle_tool_call(function_call):
    fn_name = function_call.name
    fn_args = function_call.args
    
    if fn_name == "fetch_orders":
        # ИНЪЕКЦИЯ: Добавляем user_id из безопасного контекста, а не от LLM
        result = fetch_orders_backend(
            user_id=session_context["user_id"],
            status=fn_args.get("status")
        )
        return result

# Модель видит только параметр 'status', но бэкенд получает всё необходимое.

Стратегия 3: Just-in-Time (JIT) инструменты через RAG

Самый продвинутый уровень — когда инструментов слишком много даже для фильтрации по ролям. Например, у вас есть API облачной платформы с 2000 эндпоинтов. Вы не можете загрузить их все в контекст.

Здесь мы применяем подход RAG for Tools (Retrieval Augmented Generation для инструментов).

Логика работы:

  1. Пользователь отправляет запрос: "Перезагрузи сервер production-app-1".
  2. Приложение не отправляет этот запрос сразу в основную LLM с инструментами.
  3. Сначала выполняется векторный поиск запроса пользователя по базе описаний всех 2000 инструментов.
  4. Система находит топ-5 наиболее релевантных функций (например, restart_instance, stop_instance, get_instance_status).
  5. Мы динамически формируем объект tools, содержащий только эти 5 функций.
  6. Теперь отправляем запрос пользователя + эти 5 инструментов в Gemini.

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

Упражнение

Спроектируйте функцию-обертку 'Dynamic Tool Wrapper'. Ваша задача: написать Python-код (псевдокод или использование словарей), который принимает словарь `user_context` (например, {'region': 'US'}) и список определений инструментов. Система должна автоматически скрывать параметр 'region' из схемы инструмента, передаваемой в LLM, если этот параметр присутствует в `user_context`. Если параметра нет в контексте, он должен оставаться в схеме для LLM.

Вопрос

Почему при использовании динамических инструментов важно скрывать параметры, такие как user_id, из определения функции, передаваемого модели?