AI-чат: отложенная загрузка инструментов (deferred tools + load_tools) — разгрузить контекст от неиспользуемых схем #332
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Проблема
Встроенный AI-агент (
core/ai-chat/) отправляет определения всех 41 инструмента (24 inline вai-chat-tools.service.ts+ 17 из shared-реестраSHARED_TOOL_SPECS+ внешние MCP типа Tavily) в каждый вызов модели на каждом шаге каждого хода. Описания намеренно подробные (одинtransformPage— ~350 слов), и большинству ходов 90% этих схем не нужны: корректор живёт на комментариях и чтении, но таскает с собой полные схемы таблиц, шаринга, истории и transform.Последствия: раздутое окно контекста, разбавление внимания модели, лишняя стоимость на провайдерах без prefix-кэша.
Паттерн решения — как deferred tools / ToolSearch в Claude Code: модель видит компактный каталог, а полную схему инструмента получает только когда решает им воспользоваться. Плата — один лишний раунд-трип при первом использовании отложенного тула.
Техническая основа (проверено по докам AI SDK)
Проект на
ai@^6.prepareStepумеет возвращатьactiveToolsпошагово («If provided, only these tools are enabled/available for this step»), есть top-levelactiveToolsи хелперfilterActiveTools. В кодеprepareStepуже врезан вstreamText(ai-chat.service.ts→prepareAgentStep, сейчас только финальный шаг:toolChoice:'none'+ инструкция синтеза) — точка подключения готова.Дизайн
Имена ниже — реальные camelCase-имена in-app агента (ключи tools-объекта в
ai-chat-tools.service.ts); внешний MCP-транспорт с его snake_case-именами не затрагивается.Два яруса + мета-тул
Ярус 1 «горячий» (всегда активен), 13 тулов + мета. Критерий: частый ИЛИ крошечный (для тулов с однострочным описанием откладывание — чистый минус: экономии нет, раунд-трип есть):
searchPages,listPages,listSpaces,getWorkspace,getCurrentPage,getPage,getOutline,getNode,createComment,getComment,listComments,resolveComment,editPageText+ мета-тулloadTools.Состав заточен под роли-редакторы: чтение + комментарии + точечная правка текста — типовой сценарий корректора/фактчекера проходит без единого
loadTools.Ярус 2 «отложенный», 28 тулов. Толстые и/или редкие:
transformPage,updatePageJson,updatePageContent,getPageJson,patchNode,insertNode,deleteNode,getTable,tableInsertRow,tableUpdateCell,tableDeleteRow,createPage,renamePage,movePage,deletePage,copyPageContent,listSidebarPages,getPageHistory,listPageHistory,diffPageVersions,restorePageVersion,exportPageMarkdown,importPageMarkdown,stashPage,sharePage,unsharePage,listShares,checkNewComments+ все внешние MCP-тулы автоматом. В контексте от них остаётся только строка в каталоге.Мета-тул
loadTools(names[]): добавляет имена в набор активированных, возвращает{loaded: [...]}. Полные схемы в ответ НЕ кладутся — на следующем шагеprepareStepвернёт расширенныйactiveTools, и SDK сам отправит модели полные определения. Неизвестное имя → понятная ошибка со списком валидных имён (конвенция форка об ошибках).Дословное описание мета-тула:
Механика
SHARED_TOOL_SPECS(packages/mcp/src/tool-specs.ts) добавляются поляtier: 'core' | 'deferred'иcatalogLine: string(рукописный однострочник для каталога — НЕ автогенерация из первого предложения описания: первые предложения для этого не писались и дают мусор); inline-тулы AI-чата размечаются на месте теми же полями; внешние MCP — deferred по умолчанию.activatedTools— per-turn, в памяти (замыкание вокруг вызоваstreamText). Без миграций: новый ход начинается «холодным». Если модель будет грузить одно и то же каждый ход — v2 персистит набор вai_chats(JSONB), но не в v1.prepareAgentStepостаётся чистой юнит-тестируемой функцией (расширяется сигнатура: + activatedTools, + флаг тоггла).Системный промпт — дословный текст
В
ai-chat.prompt.tsдобавляется билдерbuildToolCatalogBlock(...)(по образцуbuildMcpToolingBlock), рендерится внутри safety-сэндвича в context-секции, рядом с<mcp_tooling>, ТОЛЬКО при включённом тоггле. Вставляемый блок, дословно (каталожные строки — изcatalogLine, состав ниже — итоговый для v1):Для внешних MCP-серверов в конец каталога добавляется по строке на сервер (имена тулов берутся из фактически подключившихся):
<mcp_tooling>тем самым начинает играть роль каталога для внешних тулов, а не довеска к их полным схемам.Тоггл
Настройка воркспейса (Workspace settings → AI), например
settings.ai.deferredTools. По умолчанию ВКЛЮЧЕН. Выключение = текущее поведение (все тулы активны, каталог не рендерится) — это же аварийный откат, если какая-то модель начнёт систематически «не уметь».Риски / трейдоффы (осознанные)
Вне скоупа
/mcp-сервер: сворачивание схем — забота MCP-клиента (Claude Code это уже делает сам), на сервере ничего не меняется.План работ
buildToolCatalogBlock+ дословный блок выше вai-chat.prompt.ts(рендер только при тоггле).loadTools+ разметкаtier/catalogLine(SHARED_TOOL_SPECS, inline-тулы, внешние MCP → deferred).prepareAgentStep→activeToolsс per-turn состоянием.loadTools(следующий шаг видит полную схему); финальный шаг перекрывает (toolChoice:'none'); неизвестное имя вloadTools→ ошибка со списком; tool-call неактивного тула в истории; роль-корректор проходит типовой сценарий комментирования без единогоloadTools(проверка состава горячего яруса); тоггл off = поведение как сейчас; каталог перечисляет РОВНО множество deferred-тулов (guard-тест против рассинхрона tier ↔ каталог, по образцуserver-instructions.test.mjs).