Read in:
Русский

expand: послойная навигация по оглавлению

expand — инструмент MCP для навигации по оглавлению заметки уровень за уровнем (progressive disclosure). Агент спускается по дереву секций сверху вниз и читает только нужный лист — не загружая ни всю заметку, ни её полное плоское оглавление.

Зачем: это и есть экономия токенов. Прочитать одну секцию вместо всей заметки — во много раз дешевле (порядок величины и воспроизводимый замер — token-economy-bench), и ответ не тонет в хвосте длинного контекста, где у модели падает recall. Подробнее о принципе — Token Economy.

Как работает

expand возвращает прямых детей одного узла оглавления:

  • Опусти toc_path (или передай []) — получишь секции верхнего уровня.
  • Передай toc_path узла — получишь его подсекции.

Каждый ребёнок несёт: title, level, path (breadcrumb для следующего вызова) и has_children (есть ли вложенность).

Что вернётся, зависит от секции, которую называет toc_path:

  • Секция с подсекциями возвращает список детей, как описано выше.
  • Секция без подсекций возвращает своё содержимое — тот же текст, что дал бы note_html(path, toc_path), плюс section_html в структурированном ответе. Второй вызов, чтобы прочитать лист, не нужен: спуск заканчивается чтением.
  • Флага, который это отключает, нет, и клиенту, которому нужна только структура, он и не нужен: родительский список уже помечает каждого ребёнка через has_children, так что достаточно остановиться на has_children: false — и ничего не будет прочитано.

Аргументы. Один из идентификаторов заметки (pid, note_id, path или href — берутся из результатов search) + опциональные toc_path, first и last:

{ "name": "expand", "arguments": { "pid": 42, "toc_path": ["Goroutines"] } }

Ответ — структурированный список детей плюс короткая текстовая сводка («N subsection(s)»); для листа вместо этого приходит содержимое секции.

Рабочий цикл

1. search(query)
   → результаты со сниппетом, breadcrumb и matches[].toc_path

2. expand(pid=N)            # верхний уровень оглавления
   expand(pid=N, toc_path=[...])  # проваливаемся в нужную ветку
   → повторяем, выбирая ребёнка по смыслу; лист приходит целиком

3. note_html(pid=N, toc_path=[...])
   → читаем секцию, путь к которой уже известен, без спуска

Можно и короче: если search уже вернул точный matches[].toc_path, читай секцию сразу через note_html(toc_path=...). expand нужен, когда хочется осмотреть структуру и навигировать вглубь, не загружая лишнего.

Длинный список: только края

Заметка, в которую что-то дописывает секцию каждый день, через год несёт их сотни, и expand перечислит все. На заметке с 365 датированными секциями это ответ на 41 700 знаков — ради того, чтобы узнать имя последней.

first и last берут только края:

{ "name": "expand", "arguments": { "path": "log.md", "last": 30 } }
{ "name": "expand", "arguments": { "path": "log.md", "first": 5, "last": 30 } }
  • last: N — N новейших секций. Где тема стоит сейчас.
  • first: N — N старейших. С чего она начиналась.
  • Вместе — оба края, середина не перечисляется.

Сводка говорит, что список неполный: «newest 30 of 365 subsection(s)», а при обоих краях ставит между ними строку … 330 subsection(s) not listed …. В структурном ответе то же самое числами: total_children и omitted. Список, который выглядит целым, отправил бы вас с уверенностью, что старых секций нет.

Края, которые сошлись или перекрылись, — это просто всё: секция не попадёт в ответ дважды.

Раньше search вываливал полный плоский toc каждой заметки. Теперь результаты «тонкие»: структура раскрывается по требованию через expand. Не платишь токенами за оглавление целиком — берёшь ровно тот уровень, что нужен.

expand vs. альтернативы

  • vs «прочитать всю заметку». Лист-секция в разы дешевле полной заметки, выигрыш растёт с её размером (числа — замер). Главное — ответ остаётся вверху контекста, а не в «мёртвой зоне» внизу.
  • vs grep -A -B. grep отдаёт ±N строк вокруг лексического совпадения; expand отдаёт цельную секцию по структуре заголовков, а нужную находит по смыслу (через векторный search), а не по строке. И главное — expand работает по MCP над базой, которую grep в принципе не достанет: хостед, расшаренной команде, удалённой, чужой. Для одного человека и локального волта grep честно достаточно; expand нужен, когда база большая, общая или удалённая.

Доступ

expand уважает права на заметку: агент видит структуру только того, что ему разрешено читать (per-note access). Лишнего оглавления он не получит.

Федерация

federated_expand делает то же по связанным базам — навигируешь структуру удалённой базы знаний через один MCP-эндпоинт. См. MCP Federation.

Проверь сам

Без зависимостей, только Python 3:

python3 scripts/expand_check.py

Скрипт бьёт по https://trip2g.com/_system/mcp, идёт по оглавлению через expand, читает нужную секцию и печатает таблицу «навигация + чтение vs вся заметка».

Связанное