English translation is not available yet. Showing Russian content.
Реализовать Tool System с JSON Schema
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать Tool System с JSON Schema
1. Цель задачи
Спроектировать и реализовать систему регистрации инструментов (tools) для LLM-агента, где каждый инструмент описывает свои аргументы с помощью JSON Schema. Встроить слой валидации аргументов перед вызовом инструмента, чтобы неверные или неполные аргументы отклонялись с понятной ошибкой, не доходя до выполнения.
Ключевой результат Работающий прототип ToolRegistry, который позволяет регистрировать инструменты со схемой аргументов, вызывать их с валидацией, и возвращает ошибку валидации при несоответствии схеме.
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| Python 3.10+ окружение | Локальная среда или Colab / DevContainer |
| Базовая структура инструмента (функция с аннотациями) | Написать самостоятельно |
| Библиотека для работы с JSON Schema | pydantic или jsonschema (стандартная) |
| Примеры JSON Schema для аргументов | Сгенерировать самостоятельно (см. ниже) |
Если нет реального инструмента — симулируем:
- Создай следующий набор инструментов-заглушек (их тело может просто выводить аргументы):
- Для каждого инструмента напиши JSON Schema, описывающую его аргументы (типы, обязательность, ограничения).
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык | Python 3.10+ | Основной язык реализации |
| Валидация схем | jsonschema (из стандартной библиотеки) или pydantic | Проверка аргументов на соответствие JSON Schema |
| Хранение мета-информации | dict / dataclass | Регистрация имени, функции, схемы |
| Тестирование | pytest + unittest.mock | Проверка корректности валидации |
| Логирование | logging / structlog (опционально) | Фиксация вызовов и ошибок |
4. Этапы выполнения
Этап 1: Проектирование ToolRegistry (30 минут)
Действия
-
Спроектировать структуру регистрации
Каждый инструмент должен хранить:name— уникальное имя (строка)func— вызываемая функция- schema — JSON Schema аргументов в виде dict
description— описание инструмента (строка)
-
Определить класс или 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] -
Определить исключение для ошибок валидации
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 час)
Действия
-
Реализовать класс 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]) -
Добавить вспомогательный метод для создания схемы из Python-аннотаций (опционально): Можно реализовать отдельную функцию
infer_schema_from_function(func), которая строит схему на основе inspect и type hints, но в данной задаче схемы пишутся руками. -
Написать заглушки инструментов
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}" -
Зарегистрировать инструменты с ручными схемами
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 минут)
Действия
-
Проверить, что неверные аргументы вызывают ToolValidationError:
try: registry.call("get_weather", {"city": 123}) # city ожидается string except ToolValidationError as e: print(f"Caught: {e}") -
Добавить поддержку дополнительных ограничений в схему:
Пример для
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"] } -
Написать тесты с использованием pytest
test_valid_call— корректные аргументыtest_missing_required— отсутствует обязательный аргументtest_wrong_type— аргумент неверного типаtest_extra_argument— передан лишний аргумент (схема должна допускать или нет? решить и задокументировать)test_out_of_range— число вне допустимого диапазона
Ожидаемый результат этапа
Тесты покрывают все сценарии. ToolValidationError содержит понятные сообщения.
Этап 4: Интеграция с LLM-агентом (симуляция) (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"}} -
Создать функцию-диспетчер
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}" -
Протестировать несколько сценариев с разными user_query:
- Запрос погоды (валидный)
- Запрос отправки письма без
to(невалидный) - Несуществующий инструмент
-
Добавить логирование вызовов и ошибок в файл:
import logging logging.basicConfig(filename='tool_calls.log', level=logging.INFO)
Ожидаемый результат этапа
Полный цикл: user_query → LLM (симуляция) → валидация → выполнение или ошибка. Всё логируется.
Этап 5: Генерация схем из аннотаций (опциональное расширение) (30 минут)
Действия
-
Реализовать функцию
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 } -
Протестировать на
get_weather
schema_from_function(get_weather)должна вернуть схему сcity(string, required) иdate(string, default None). -
Сравнить ручную схему и автоматическую убедиться, что валидация работает одинаково.
Ожидаемый результат этапа
Автоматическая генерация схемы из аннотаций (упрощённая, не покрывает все типы 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: Проектирование ToolRegistry | 30 минут |
| Этап 2: Реализация ToolRegistry | 1 час |
| Этап 3: Валидация и обработка ошибок | 45 минут |
| Этап 4: Интеграция с LLM-агентом | 1 час |
| Этап 5: Генерация схем из аннотаций (опционально) | 30 минут |
| Итого (без опционального этапа) | 3 часа 15 минут |
| Итого (с опциональным этапом) | 3 часа 45 минут |
Примечание Для первого раза заложите +30% на отладку и чтение документации.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 12 | JSON Schema: основные типы и ключевые слова |
| 45 | Валидация аргументов в LLM-агентах |
| 78 | Регистрация инструментов и dependency injection |
| 134 | Обработка ошибок при вызове инструментов |
| 201 | Pydantic 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без ошибок (опционально). - Я создал демо-скрипт, который показывает правильную и неправильную работу.