Создание динамических инструментов на основе контекста пользователя
Введение: Проблема статических инструментов
Приветствую! Мы продолжаем углубляться в возможности 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, мы создадим Реестр Инструментов. Каждый инструмент в этом реестре будет иметь метаданные, указывающие, кто имеет право его использовать.
Алгоритм действий:
- Идентифицируем пользователя (например, получаем
user_idи его роль из базы данных). - Пробегаем по нашему реестру функций.
- Отбираем только те функции, которые разрешены для текущей роли.
- Генерируем JSON-схемы только для отобранных функций.
- Отправляем в Gemini «чистый» список, специально собранный под этого пользователя.
Это решает проблему безопасности: модель даже не узнает о существовании административных функций, если с ней общается обычный пользователь.
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и вызывает реальную функцию с полным набором аргументов.
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 для инструментов).
Логика работы:
- Пользователь отправляет запрос: "Перезагрузи сервер production-app-1".
- Приложение не отправляет этот запрос сразу в основную LLM с инструментами.
- Сначала выполняется векторный поиск запроса пользователя по базе описаний всех 2000 инструментов.
- Система находит топ-5 наиболее релевантных функций (например,
restart_instance,stop_instance,get_instance_status). - Мы динамически формируем объект
tools, содержащий только эти 5 функций. - Теперь отправляем запрос пользователя + эти 5 инструментов в Gemini.
Этот метод позволяет создавать агентов с бесконечным набором возможностей, сохраняя при этом фокус модели и экономя токены. Gemini получает только те «отвертки», которые нужны для конкретной задачи.
Спроектируйте функцию-обертку 'Dynamic Tool Wrapper'. Ваша задача: написать Python-код (псевдокод или использование словарей), который принимает словарь `user_context` (например, {'region': 'US'}) и список определений инструментов. Система должна автоматически скрывать параметр 'region' из схемы инструмента, передаваемой в LLM, если этот параметр присутствует в `user_context`. Если параметра нет в контексте, он должен оставаться в схеме для LLM.
Почему при использовании динамических инструментов важно скрывать параметры, такие как user_id, из определения функции, передаваемого модели?