中文翻译暂不可用,显示俄语原文。

Реализовать Tool System с JSON Schema

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать Tool System с JSON Schema

1. Цель задачи

Спроектировать и реализовать систему регистрации инструментов (tools) для LLM-агента, где каждый инструмент описывает свои аргументы с помощью JSON Schema. Встроить слой валидации аргументов перед вызовом инструмента, чтобы неверные или неполные аргументы отклонялись с понятной ошибкой, не доходя до выполнения.

Ключевой результат Работающий прототип ToolRegistry, который позволяет регистрировать инструменты со схемой аргументов, вызывать их с валидацией, и возвращает ошибку валидации при несоответствии схеме.


2. Исходные данные

Что нужноОткуда взять
Python 3.10+ окружениеЛокальная среда или Colab / DevContainer
Базовая структура инструмента (функция с аннотациями)Написать самостоятельно
Библиотека для работы с JSON Schemapydantic или jsonschema (стандартная)
Примеры JSON Schema для аргументовСгенерировать самостоятельно (см. ниже)

Если нет реального инструмента — симулируем:

  1. Создай следующий набор инструментов-заглушек (их тело может просто выводить аргументы):
    • get_weather(city: str, date: Optional[str] = None) — вернёт фейковую погоду
    • send_email(to: str, subject: str, body: str, priority: int = 1) — имитирует отправку
    • calculate(expression: str, precision: int = 2) — вычисляет выражение с заданной точностью
  2. Для каждого инструмента напиши JSON Schema, описывающую его аргументы (типы, обязательность, ограничения).

3. Технологический стек

КомпонентИнструментыНазначение
ЯзыкPython 3.10+Основной язык реализации
Валидация схемjsonschema (из стандартной библиотеки) или pydanticПроверка аргументов на соответствие JSON Schema
Хранение мета-информацииdict / dataclassРегистрация имени, функции, схемы
Тестированиеpytest + unittest.mockПроверка корректности валидации
Логированиеlogging / structlog (опционально)Фиксация вызовов и ошибок

4. Этапы выполнения

Этап 1: Проектирование ToolRegistry (30 минут)

Действия

  1. Спроектировать структуру регистрации
    Каждый инструмент должен хранить:

    • name — уникальное имя (строка)
    • func — вызываемая функция
    • schema — JSON Schema аргументов в виде dict
    • description — описание инструмента (строка)
  2. Определить класс или dataclass для инструмента:

    from dataclasses import dataclass
    from typing import Callable, Dict, Any
    
    @dataclass
    class Tool:
        name: str
        description: str
        func: Callable[..., Any]
        schema: Dict[str, Any]
    
  3. Определить исключение для ошибок валидации

    class ToolValidationError(Exception):
        def __init__(self, tool_name: str, errors: list):
            self.tool_name = tool_name
            self.errors = errors
            super().__init__(f"Validation failed for tool '{tool_name}': {errors}")
    

Ожидаемый результат этапа
Файл tool_system.py с заготовками классов Tool и ToolValidationError.


Этап 2: Реализация ToolRegistry (1 час)

Действия

  1. Реализовать класс ToolRegistry

    from typing import Dict, List
    
    class ToolRegistry:
        def __init__(self):
            self._tools: Dict[str, Tool] = {}
        
        def register(self, tool: Tool) -> None:
            if tool.name in self._tools:
                raise ValueError(f"Tool '{tool.name}' already registered")
            # необязательно: проверить корректность схемы на этапе регистрации
            self._validate_schema(tool.schema)
            self._tools[tool.name] = tool
        
        def call(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
            tool = self._tools.get(tool_name)
            if tool is None:
                raise KeyError(f"Tool '{tool_name}' not found")
            self._validate_arguments(tool, arguments)
            return tool.func(**arguments)
        
        def list_tools(self) -> List[Tool]:
            return list(self._tools.values())
        
        def _validate_schema(self, schema: Dict[str, Any]) -> None:
            # базовая проверка, что schema — допустимый JSON Schema Draft 7
            from jsonschema import Draft7Validator
            Draft7Validator.check_schema(schema)
        
        def _validate_arguments(self, tool: Tool, arguments: Dict[str, Any]) -> None:
            from jsonschema import validate, ValidationError
            try:
                validate(instance=arguments, schema=tool.schema)
            except ValidationError as e:
                raise ToolValidationError(tool.name, [e.message])
    
  2. Добавить вспомогательный метод для создания схемы из Python-аннотаций (опционально): Можно реализовать отдельную функцию infer_schema_from_function(func), которая строит схему на основе inspect и type hints, но в данной задаче схемы пишутся руками.

  3. Написать заглушки инструментов

    def get_weather(city: str, date: str = None) -> str:
        if date:
            return f"Weather in {city} on {date}: sunny, +22°C"
        return f"Weather in {city} now: cloudy, +18°C"
    
    def send_email(to: str, subject: str, body: str, priority: int = 1) -> str:
        return f"Email sent to {to} with priority {priority}"
    
    def calculate(expression: str, precision: int = 2) -> float:
        try:
            result = eval(expression)   # только для демонстрации, не используйте в production
            return round(result, precision)
        except Exception as e:
            return f"Error: {e}"
    
  4. Зарегистрировать инструменты с ручными схемами

    registry = ToolRegistry()
    
    registry.register(Tool(
        name="get_weather",
        description="Get weather for a city",
        func=get_weather,
        schema={
            "type": "object",
            "properties": {
                "city":       {"type": "string", "description": "City name"},
                "date":       {"type": "string", "description": "Date in YYYY-MM-DD", "default": None}
            },
            "required": ["city"]
        }
    ))
    
    # аналогично для send_email и calculate
    

Ожидаемый результат этапа
Работающий ToolRegistry с тремя инструментами. Успешный вызов registry.call("get_weather", {"city": "Moscow"}) возвращает строку погоды.


Этап 3: Валидация и обработка ошибок (45 минут)

Действия

  1. Проверить, что неверные аргументы вызывают ToolValidationError:

    try:
        registry.call("get_weather", {"city": 123})   # city ожидается string
    except ToolValidationError as e:
        print(f"Caught: {e}")
    
  2. Добавить поддержку дополнительных ограничений в схему:

    Пример для send_email: приоритет от 1 до 5.

    schema = {
        "type": "object",
        "properties": {
            "to":       {"type": "string", "minLength": 5},
            "subject":  {"type": "string", "minLength": 1},
            "body":     {"type": "string"},
            "priority": {"type": "integer", "default": 1, "minimum": 1, "maximum": 5}
        },
        "required": ["to", "subject", "body"]
    }
    
  3. Написать тесты с использованием pytest

    • test_valid_call — корректные аргументы
    • test_missing_required — отсутствует обязательный аргумент
    • test_wrong_type — аргумент неверного типа
    • test_extra_argument — передан лишний аргумент (схема должна допускать или нет? решить и задокументировать)
    • test_out_of_range — число вне допустимого диапазона

Ожидаемый результат этапа
Тесты покрывают все сценарии. ToolValidationError содержит понятные сообщения.


Этап 4: Интеграция с LLM-агентом (симуляция) (1 час)

Действия

  1. Написать симуляцию LLM, которая генерирует JSON-вызовы инструментов:

    import random
    
    def simulate_llm(tools_description: str, user_query: str) -> Dict[str, Any]:
        """Возвращает tool_name и arguments (симулирует LLM)"""
        # В реальном проекте здесь был бы вызов LLM
        return {"tool_name": "get_weather", "arguments": {"city": "London"}}
    
  2. Создать функцию-диспетчер

    def execute_tool_call(llm_output: Dict[str, Any]) -> str:
        tool_name = llm_output.get("tool_name")
        arguments = llm_output.get("arguments", {})
        try:
            result = registry.call(tool_name, arguments)
            return f"Success: {result}"
        except KeyError:
            return f"Error: Tool '{tool_name}' not found"
        except ToolValidationError as e:
            return f"Validation Error: {e}"
    
  3. Протестировать несколько сценариев с разными user_query:

    • Запрос погоды (валидный)
    • Запрос отправки письма без to (невалидный)
    • Несуществующий инструмент
  4. Добавить логирование вызовов и ошибок в файл:

    import logging
    logging.basicConfig(filename='tool_calls.log', level=logging.INFO)
    

Ожидаемый результат этапа
Полный цикл: user_query → LLM (симуляция) → валидация → выполнение или ошибка. Всё логируется.


Этап 5: Генерация схем из аннотаций (опциональное расширение) (30 минут)

Действия

  1. Реализовать функцию schema_from_function с помощью inspect и typing:

    import inspect
    from typing import get_type_hints, Optional
    
    def schema_from_function(func) -> Dict[str, Any]:
        hints = get_type_hints(func)
        sig = inspect.signature(func)
        properties = {}
        required = []
        for name, param in sig.parameters.items():
            type_ = hints.get(name, str)
            prop = {"type": "string"}   # упрощённый маппинг, в реальности используйте типизацию
            if type_ == int:
                prop["type"] = "integer"
            elif type_ == float:
                prop["type"] = "number"
            elif type_ == bool:
                prop["type"] = "boolean"
            if param.default is inspect.Parameter.empty:
                required.append(name)
            else:
                prop["default"] = param.default
            properties[name] = prop
        return {
            "type": "object",
            "properties": properties,
            "required": required
        }
    
  2. Протестировать на get_weather
    schema_from_function(get_weather) должна вернуть схему с city (string, required) и date (string, default None).

  3. Сравнить ручную схему и автоматическую убедиться, что валидация работает одинаково.

Ожидаемый результат этапа
Автоматическая генерация схемы из аннотаций (упрощённая, не покрывает все типы Python, но демонстрирует идею).


5. Критерии приемки (Definition of Done)

  • ToolRegistry позволяет зарегистрировать инструмент с JSON Schema и получить его по имени.
  • При вызове registry.call() с аргументами, не соответствующими схеме, выбрасывается ToolValidationError.
  • Ошибка валидации содержит имя инструмента и понятное описание проблемы.
  • Если вызван несуществующий инструмент, выбрасывается KeyError.
  • Поддерживаются как минимум следующие типы в схеме: string, integer, number, boolean, array, object.
  • Схема корректно обрабатывает required поля и значения по умолчанию.
  • Покрытие тестами (pytest) не менее 80% строк кода (проверить pytest --cov).
  • Написана документация (docstrings для всех классов и методов) и пример использования в README.md (опционально).

6. Ожидаемый результат

Основной артефакт
Пакет tool_system/ (или один файл tool_system.py) содержащий:

  • Tool (dataclass)
  • ToolRegistry (class)
  • ToolValidationError (exception)
  • Три зарегистрированных инструмента с ручными схемами
  • Функцию-диспетчер для симуляции LLM
  • Набор тестов (test_tool_system.py)

Содержание файла tool_system.py
Логически организованный код с комментариями. Константы и импорты в начале. Отдельные функции для регистрации и вызова.

Дополнительные артефакты (опционально):

  • Файл лога вызовов tool_calls.log
  • Скрипт-демонстрация demo.py с последовательностью вызовов (валидных и невалидных)
  • requirements.txt с зависимостями (jsonschema, pytest, pytest-cov)

7. Возможные сложности и их решение

СложностьРешение
JSON Schema Draft не указан, валидатор может ожидать другую версиюЯвно указать "$schema": "http://json-schema.org/draft-07/schema#" в схеме или использовать Draft7Validator
Аргументы с дефолтными значениями не передаются из LLM, а схема требует их обязательнымиПрописать "default" в схеме для необязательных полей и проверять, что валидатор не ругается на отсутствие
LLM может передать лишние аргументы, не описанные в схемеРешить: запретить ("additionalProperties": false) или разрешить. В задаче рекомендуется запретить и выдавать ошибку валидации
Функция инструмента принимает аргументы с другими именами, чем в схемеСинхронизировать имена; при автоматической генерации схемы использовать inspect
Производительность при большом количестве инструментовРегистрация – однократная операция; валидация быстрая (миллисекунды). Для масштабирования можно кэшировать экземпляры валидаторов

8. Бюджет времени (оценка)

ЭтапВремя
Этап 1: Проектирование ToolRegistry30 минут
Этап 2: Реализация ToolRegistry1 час
Этап 3: Валидация и обработка ошибок45 минут
Этап 4: Интеграция с LLM-агентом1 час
Этап 5: Генерация схем из аннотаций (опционально)30 минут
Итого (без опционального этапа)3 часа 15 минут
Итого (с опциональным этапом)3 часа 45 минут

Примечание Для первого раза заложите +30% на отладку и чтение документации.


9. Связанные вопросы из базы знаний

ВопросТема
12JSON Schema: основные типы и ключевые слова
45Валидация аргументов в LLM-агентах
78Регистрация инструментов и dependency injection
134Обработка ошибок при вызове инструментов
201Pydantic vs jsonschema: сравнение
267Генерация схемы из Python type hints
312Паттерн Tool Calling в LangChain и OpenAI
398Проектирование ToolRegistry для production
445Логирование в AI-системах
502Тестирование валидации с неожиданными данными

10. Чек-лист самопроверки

  • Я зарегистрировал минимум 3 инструмента с JSON Schema и убедился, что они вызываются.
  • Я проверил, что при передаче неверного типа (например, int вместо string) выбрасывается ToolValidationError.
  • Я проверил, что при отсутствии обязательного аргумента выбрасывается ToolValidationError.
  • Я проверил, что при вызове несуществующего инструмента выбрасывается KeyError.
  • Я написал как минимум 5 тестов (pytest), которые покрывают основные сценарии.
  • Я убедился, что сообщение об ошибке валидации содержит имя инструмента и описание проблемы.
  • Код проходит flake8 или pylint без ошибок (опционально).
  • Я создал демо-скрипт, который показывает правильную и неправильную работу.