Суть
Классическая схема tool calling передаёт модели массив определений всех инструментов на каждом вызове. Пока инструментов пять — это нормально. Полноценная продакшен-система тянет за собой больше сотни, и тогда схема упирается в «эффект свалки»: описания занимают контекст, а точность выбора падает.
Решение — не показывать всё сразу. Перед вызовом модели сырой запрос пользователя превращается в плотный вектор, по близости отбираются подходящие инструменты, и только их описания уходят в промпт.
Зачем это нужно
Два независимых выигрыша. Первый очевидный — экономия контекста: сто схем не конкурируют с полезными данными. Второй важнее: чем меньше вариантов, тем легче выбор. Модель, которой дали три релевантных инструмента, ошибается реже, чем модель, которой дали сто, включая три релевантных.
Порог отсечения подбирается эвристикой; рабочий ориентир — оставлять 3–5 вариаций, и три лучше пяти, если тулинг разросся до сотни и больше.
Обратная сторона: если инструмент один, вся эта обвязка избыточна. Она окупается там, где каталог реально большой.
Как работает
Векторизация запроса. Сырой пользовательский запрос → эмбеддинг. Локальная модель эмбеддингов здесь предпочтительна: это горячий путь, и платить сетевым вызовом за каждый запрос дорого.
Отбор по близости. Косинусное сходство между вектором запроса и векторами описаний инструментов, отсечение по порогу, топ-N в контекст.
Фильтрация по правам. Отбор удобно совмещать с RBAC: инструменты, на которые у текущего пользователя нет прав, не должны попадать в контекст вообще — так модель не сможет их даже предложить.
Динамический инжект схем. В промпт уходят описания только отобранных инструментов, а не весь каталог.
Что делать с битым выводом
Отбор снижает вероятность ошибки, но не устраняет её. Дальше выстраивается каскад обработки, где переход на следующий уровень дороже предыдущего:
- Строгая схема на входе. Аргументы валидируются Pydantic-схемой, ошибка парсинга перехватывается и не улетает выше по стеку.
- Ремонт вместо перегенерации. Повреждённый JSON — пропущенная кавычка или скобка — чинится библиотекой
json_repairна лету, без повторного платного вызова модели. - Один ретрай с конкретным фидбеком. Модель получает описание ошибки и исправляет аргументы. При грамотной обвязке этого хватает; второй ретрай обычно не окупается.
- Мягкая деградация. Сетевые сбои, таймауты БД и 5xx внешних сервисов преобразуются внутри перехватчика в валидный выходной контракт с флагом ошибки — чтобы оркестратор мог продолжить диалог, а не упасть.
- Эскалация человеку. Если модель зациклилась, сессия уходит оператору.
Важное ограничение: пункт 3 работает только на моделях с приличным признаковым пространством. Слабые и сильно квантованные локальные модели зацикливаются на одной и той же ошибке независимо от того, что им подкладывать в промпт, — для них сразу нужен fallback.
Три шлюза вокруг инструмента
Отбор решает, какой инструмент позвать. Отдельный слой решает, что через него проходит — и этих точек контроля три, а не одна:
- Входной шлюз — сбор, санитаризация и строгая типизация аргументов от планировщика. Некорректные форматы отсекаются здесь, до обращения к базе.
- Сам вызов — бэкенд или БД, зажатые между двумя шлюзами.
- Выходной шлюз — структурирование и нормализация ответа. Задача не только привести форму, но и не выпустить наружу лишнее: системные трейсбеки и чувствительные данные не должны попадать в контекст модели, а оттуда пользователю.
Выходной шлюз обычно забывают, и зря: именно через него утекает то, что бэкенд вернул «для отладки».
Побочная выгода строгой схемы: среда выполнения компилирует классы в спецификации для протоколов вроде MCP автоматически, без ручного написания JSON-манифестов.
Экономика отбора
Инъекция только топ-3 кандидатов экономит до 90% затрат на токены против передачи всего реестра, и позволяет держать каталог в тысячи инструментов без потери скорости — потому что векторный отбор считается за микросекунды на локальных эмбеддингах.
Важная деталь на случай неуверенного отбора: если ни один кандидат не набирает достаточной близости, запрос мягко деградирует в обычный текстовый чат, а не превращается в вызов наугад.
Фильтрация реестра при этом совмещается с правами: инструменты, недоступные роли пользователя или не относящиеся к активной транзакции, отсекаются до векторного отбора — так модель физически не может предложить то, на что нет прав.
Что векторизовать и что ломается на составных задачах
Что класть в индекс. Проблема в асимметрии: пользователь пишет запрос на языке задачи, а докстрока написана на языке реализации, и косинус между ними мал даже когда инструмент подходит. Отсюда практический порядок: имя с докстрокой — минимальный вариант, работает на очевидных случаях; примеры вызовов добавляют мало, потому что это снова язык реализации; синтетические запросы-парафразы дают наибольший прирост, поскольку индекс начинает содержать формулировки того же вида, что и запрос. Дешёвый способ их получить — сгенерировать моделью по описанию инструмента 5-10 вариантов «как пользователь мог бы это попросить» и индексировать их вместе с описанием.
Составные задачи — известная слабость отбора. Если запрос требует инструментов из разных смысловых кластеров, топ-K по одному вектору запроса заполняется ближайшим кластером и второй не попадает вовсе. Никакое увеличение K это надёжно не лечит: оно лишь добавляет соседей того же кластера. Рабочие обходы: декомпозировать запрос до отбора и делать выборку под каждую подзадачу (Plan and Execute); либо отбирать не плоско, а по кластерам — брать топ-N из каждой смысловой группы инструментов. Признак, что вы попали в эту ловушку: агент уверенно решает половину задачи и не замечает, что вторая половина осталась без инструмента.
Связано с
- Tool Calling — базовая механика передачи инструментов модели
- MCP Business Facade — альтернативный путь: не отбирать из сотни, а свести к семи
- Schema Guided Reasoning — строгая спецификация вызова как контракт
- Embeddings — чем считается близость запроса и описания инструмента
- Agent Failure Modes — куда встраивается каскад обработки ошибок