ДокументацияПрактические руководства (Cookbooks)Выбор навыков для ИИ-агентов

Выбор навыков для ИИ-агентов

Picks at most one skill for an agent turn out of the 182 in Nous Research's Hermes catalog: one TypeSafe request ranks every skill and asks whether the turn needs one at all, a second reads the top three properly and can reject all of them. The winner's name goes into a single line of the agent's sy…

Выбирает не более одного навыка для шага агента из 182 доступных в каталоге Hermes от Nous Research: один запрос TypeSafe ранжирует все навыки и определяет, требуется ли навык вообще, второй запрос подробно анализирует топ-3 и может отклонить их все. Имя победителя добавляется в единственную строку системного промпта агента, снижая более чем вдвое как число ошибочно загруженных навыков, так и загрузки в ситуациях, когда ни один навык не подходит.

Агенты обычно выбирают навыки, усекая их описания и загружая весь список в системный промпт, что увеличивает затраты, снижает точность выбора навыка и приводит к деградации контекста (context rot) на всю оставшуюся сессию. Мы решаем эту проблему, используя два запроса TypeSafe на каждый шаг агента — один для ранжирования навыков и второй для проверки выбора, сокращая количество некорректных загрузок навыков более чем вдвое.

Агент с большим каталогом навыков принимает решение практически вслепую. Каталог поступает к нему в виде индекса: одна строка на навык, при этом описание обрезается, чтобы полный текст не вытеснял саму беседу. Фреймворк Hermes, используемый здесь в качестве агентной среды, по умолчанию обрезает описание до 60 символов. Например, при такой длине навык, который редактирует файлы .pptx, выглядит почти так же, как навык, который создает их с нуля. Попросите подготовить питч-дек, и агент легко может загрузить не тот инструмент. А на шаге, где ни один навык вообще не нужен, он все равно может что-то загрузить, поскольку список названий провоцирует на угадывание.

В этом руководстве описания не сокращаются: вместо этого применяется принцип поэтапного раскрытия (progressive disclosure) — сначала недорогой обзор всех 182 навыков, а затем детальное изучение трех кандидатов. Перед принятием решения о загрузке навыка выполняются два запроса TypeSafe. Первый ранжирует все навыки каталога относительно запроса пользователя и определяет, нужен ли навык вообще. Второй заново оценивает только трех лидеров — теперь уже с их полными описаниями и началом инструкций — и имеет возможность отклонить их все.

Имя победителя подставляется в одну дополнительную строку системного промпта агента на текущем шаге:

PLAINTEXT api.wedstack.ru/v1
<skill_relevance>
Relevant to the current request: pptx-author. Ignore this if it does not fit what the user
actually asked for.
</skill_relevance>

Агент сохраняет свой полный индекс и свободу собственного суждения, а эта строка лишь подсказывает ему, на какую запись обратить внимание в первую очередь. Сам каталог в промпте никогда не меняется, благодаря чему кэширование префикса (prefix caching) сохраняется в полной мере. Результаты на 488 запросах к модели claude-haiku-4-5-20251001 с использованием навыков из каталога Hermes:

загружает неверный навык загружает навык, когда ничего не подходит
агент сам по себе, только со своим каталогом 16.8% 9.8%
агент с подсказкой TypeSafe 7.3% 4.0%
агенту передан правильный ответ (oracle) 2.5% 1.2%

Третья строка показывает, что базовый уровень ошибок не равен нулю: даже получив абсолютно точный навык, агент не всегда загружает его, и никакой метод подбора, сколь бы совершенен он ни был, не может преодолеть этот порог.

В итоге вы получаете функцию suggest(), возвращающую не более одного имени навыка, функцию suggestion_block(), форматирующую подсказку для системного промпта, и тестовый стенд, на котором получена таблица выше, готовый к работе с вашим собственным каталогом.

MERMAID ACTIONS={TRUE} THEME={NULL} api.wedstack.ru/v1
flowchart LR
    subgraph C1["Вызов 1 — беглый обзор всех 182 навыков"]
        direction TB
        Q1["<b>Choice:</b> какой навык подходит?<br/><i>все 182, по одной строке на каждый</i>"]
        N1["<b>Nouls:</b> нужен ли навык вообще?<br/>· воздействовать на данные пользователя?<br/>· следовать инструкциям?<br/>· или просто пообщаться?"]
        %% invisible link: without an edge these two share a rank, which in a TB
        %% subgraph puts them side by side instead of stacked
        Q1 ~~~ N1
    end
    subgraph C2["Вызов 2 — детальный анализ этих 3"]
        direction TB
        Q2["<b>Choice:</b> какой из 3?<br/><i>с подробными описаниями</i>"]
        N2["<b>Nouls:</b> действительно ли каждый<br/>решает задачу?"]
        Q2 ~~~ N2
    end
    REQ["запрос пользователя"] --> C1
    C1 -->|"топ-3"| C2
    C1 -->|"ничего<br/>не подходит"| STOP["ничего не<br/>предлагать"]
    C2 -->|"никто не подошел"| STOP
    C2 -->|"есть победитель"| OUT["предложить<br/>победителя"]

Настройка

  • Установите клиент TypeSafe, клиент Anthropic и вспомогательные модули руководства.
  • Задайте ключ API TypeSafe и ключ Anthropic для тестируемого агента.
BASH THEME={NULL} api.wedstack.ru/v1
pip install anthropic matplotlib ipython "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/
export TYPESAFE_API_KEY=your-key-here
export ANTHROPIC_API_KEY=your-key-here

Примечание: блоки кода ниже представляют собой единый скрипт. Чтобы выполнить его самостоятельно, сохраните их в один файл в указанном порядке.

Кэширование результатов

JsonCache сохраняет результаты каждого вызова по ключу входных данных, поэтому повторный запуск воспроизводит приведенные ниже цифры без обращения к API. Удалите json_cache.json, чтобы выполнить прогон в реальном времени. Опубликованный прогон использовал jev-1.12 и claude-haiku-4-5-20251001, выборка от 31.07.2026.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1

Шаг 1: загрузка каталога

Файл hermes_roster.json содержит 182 навыка из
NousResearch/hermes-agent (MIT) на определенном
коммите. Каждая запись содержит имя и категорию навыка, краткое описание для индекса,
полное описание и начало файла SKILL.md.

Приведенный ниже индекс и инструкции промпта над ним скопированы непосредственно из Hermes.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
PLAINTEXT api.wedstack.ru/v1
182 skills in 33 categories
roster prompt: 16,089 characters
index description: 54 characters on average, 60 at most

one category, as the agent reads it:
  apple:
    - apple-notes: Manage Apple Notes via memo CLI: create, search, edit.
    - apple-reminders: Apple Reminders via remindctl: add, list, complete.
    - findmy: Track Apple devices/AirTags via FindMy.app on macOS.
    - imessage: Send and receive iMessages/SMS via the imsg CLI on macOS.

Шаг 2: оценка базового агента без подсказок

requests.json содержит 488 одношаговых запросов: 315 из них покрываются ровно одним навыком,
а остальные 173 не покрываются ни одним.

Покрытые запросы были сгенерированы Claude Sonnet 5 на основе файлов SKILL.md каждого навыка,
поэтому разметка надежна, а сами запросы проще реальных пользовательских.

Все 173 непокрытых запроса составлены так, чтобы выявлять ложные срабатывания: 85 повседневных
запросов, 42 технических вопроса, для которых нет навыков (объясни, что такое монада),
и 46 запросов на специфические действия, отсутствующие в каталоге (например, опубликуй это в Mastodon
в каталоге, где есть только интеграция с X).

Оценка учитывает только первый ответ агента. Оба показателя являются долями ошибок, поэтому меньшее
значение лучше:

  • wrong load (ошибочная загрузка): доля покрытых запросов, в которых первый вызов skill_view
    загрузил не тот навык. Шаг, на котором ничего не загружено, считается промахом.
  • needless load (избыточная загрузка): доля непокрытых запросов, в которых агент вообще вызвал
    skill_view.
PYTHON THEME={NULL} api.wedstack.ru/v1
REQUESTS = json.loads(Path("requests.json").read_text(encoding="utf-8"))
POSITIVES = [p for p in REQUESTS if p["gold"]]
NEGATIVES = [p for p in REQUESTS if not p["gold"]]

print(
    f"{len(REQUESTS)} requests: {len(POSITIVES)} covered by a skill "
    f"({len({p['gold'] for p in POSITIVES})} distinct skills), {len(NEGATIVES)} covered by none"
)
print(f"\ncovered   [{POSITIVES[0]['gold']}]  {POSITIVES[0]['text']}")
print(f"uncovered  {NEGATIVES[0]['text']}")
PLAINTEXT api.wedstack.ru/v1
488 requests: 315 covered by a skill (171 distinct skills), 173 covered by none

covered   [1password]  I've got a config.yaml with `{{ op://app-prod/db/password }}` placeholders in it — can you set up my project to pull the real values in at runtime instead of hardcoding them?
uncovered  Add these three cards to our Trello backlog.

Подсказка помещается в отдельный блок системного промпта после каталога, а не внутри него,
благодаря чему текст каталога остается побайтово идентичным на каждом шаге, сохраняя кэширование префикса.

Агент располагает минимальным набором инструментов, включая skill_view для загрузки навыка по
текстовому имени. Для успешной загрузки имя должно точно совпадать с названием навыка.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1

Сначала агент запускается только со своим каталогом — так, как он работает сегодня. Полученные
две доли ошибок служат базовой линией (baseline), с которой сравниваются остальные результаты.

PYTHON THEME={NULL} api.wedstack.ru/v1
baseline = run_arm("baseline", {})
base_scores = summarise(baseline)
print(
    f"wrong loads    {base_scores['wrong_load']:.1%}   ({len(POSITIVES)} covered requests)"
)
print(
    f"needless loads {base_scores['needless_load']:.1%}   ({len(NEGATIVES)} uncovered requests)"
)

# where the wrong loads land: a neighbour of the right skill, or somewhere unrelated?
misses = [
    (p["gold"], baseline[p["text"]]["loaded"][0])
    for p in POSITIVES
    if baseline[p["text"]]["loaded"] and baseline[p["text"]]["loaded"][0] != p["gold"]
]
same_category = sum(
    1
    for gold, got in misses
    if got in BY_NAME and BY_NAME[got]["category"] == BY_NAME[gold]["category"]
)
print(
    f"\nof {len(misses)} wrong first picks, {same_category} came from the right skill's own "
    f"category"
)
PLAINTEXT api.wedstack.ru/v1
wrong loads    16.8%   (315 covered requests)
needless loads 9.8%   (173 uncovered requests)

of 36 wrong first picks, 10 came from the right skill's own category

Ошибочные загрузки попадают в ту же категорию, к которой относится правильный навык, несоизмеримо
чаще, чем это происходило бы при случайном выборе: основная сложность заключается в различении
похожих навыков внутри одной темы. Агент уже ищет примерно в правильном месте.

Шаг 3: ранжирование всего каталога

Один запрос объединяет два типа вопросов:

  • which — вопрос Choice
    по всем 182 именам навыков с кратким описанием из индекса в качестве критерия каждого
    варианта (тот же текст, который видит сам агент). Его вероятности образуют итоговый рейтинг.
  • три вопроса Noul о запросе пользователя
    (приведены ниже), каждый из которых под своим углом проверяет, требуется ли выполнение конкретного
    действия, а не просто разъяснение. Вопрос prose_suffices
    инвертирован. Их среднее значение определяет, стоит ли вообще предлагать навык: при значении
    ниже 0.30 подсказка не формируется.

Оба типа вопросов отправляются в одном запросе, поэтому ранжирование и проверка обходятся в одно
обращение к API.

Формулируйте эти три вопроса так, чтобы они спрашивали о потребности в действии. Вопрос о тематике
текста не отличит задачу «объясни, что такое монада» от запроса, требующего навыка разработки,
поскольку обе темы относятся к программированию.

Один вопрос Choice без труда вмещает каталог такого размера. Для каталогов в разы большего
объема его можно разбить на части, проранжировать каждую и затем применить тот же этап отбора
к лидерам.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
PLAINTEXT api.wedstack.ru/v1
"Can you save this recipe as a new note in my 'Recipes' folder in Notes.app so "
  needs a skill 0.75 -> suggest   (0.31s)
    0.990  apple-notes                           Manage Apple Notes via memo CLI: create, search, edit.
    0.010  computer-use                          Drive the user's desktop in the background — clicking, ty...
    0.000  concept-diagrams                      Generate flat, minimal educational SVG visuals as HTML.

"Can you put together a pitch deck skeleton (cover, situation overview, comps, "
  needs a skill 0.76 -> suggest   (0.16s)
    0.700  powerpoint                            Create, read, edit .pptx decks, slides, notes, templates.
    0.300  pptx-author                           Build PowerPoint decks headless with python-pptx.
    0.000  chroma                                Embedding database for RAG and semantic search.

"Post this announcement to my Mastodon account."
  needs a skill 0.78 -> suggest   (0.16s)
    0.550  xurl                                  X/Twitter via xurl CLI: raw post search, posting, DM, media.
    0.140  computer-use                          Drive the user's desktop in the background — clicking, ty...
    0.080  openhands                             Delegate coding to OpenHands CLI (model-agnostic, LiteLLM).

Запрос к Notes.app однозначен, и его главный кандидат выбран верно. При ранжировании запроса
про Mastodon ситуация сложнее: три вопроса показывают, что навык требуется (так как публикация
в соцсеть — это действие), а при наличии навыка для X и отсутствии для Mastodon побеждает ближайший
доступный вариант.

Остается запрос про презентацию. Оба лидера — навыки для .pptx, и на основе 60-символьного описания
широкий вопрос Choice ошибочно ставит навык редактирования выше навыка создания новой презентации.

Шаг 4: переранжирование трех лучших кандидатов

Три варианта оставляют достаточно места для полного описания и начала файла SKILL.md каждого
навыка, поэтому второй запрос оценивает кандидатов на основе гораздо более подробных данных:

  • which — вопрос Choice по шортлисту, где в качестве критериев вариантов выступает этот
    расширенный текст.
  • fits::{name} — по одному вопросу Noul на каждого кандидата: выполняет ли данный навык
    именно то действие, о котором просит пользователь? Каждый вопрос оценивается независимо,
    поэтому все они могут получить низкие баллы; шортлист, у которого наивысший балл ниже 0.30,
    отклоняется полностью.
PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
PLAINTEXT api.wedstack.ru/v1
"Can you save this recipe as a new note in my 'Recipes' folder in Notes.app so "
  was apple-notes -> apple-notes   (0.12s)
    fits 0.60  apple-notes
    fits 0.54  computer-use
    fits 0.01  concept-diagrams

"Can you put together a pitch deck skeleton (cover, situation overview, comps, "
  was powerpoint -> pptx-author   (0.09s)
    fits 0.73  powerpoint
    fits 0.38  pptx-author
    fits 0.02  chroma

"Post this announcement to my Mastodon account."
  was xurl -> xurl   (0.09s)
    fits 0.56  xurl
    fits 0.38  computer-use
    fits 0.05  openhands

Два навыка для .pptx четко разделяются, как только к оценке подключаются полные тексты:
запрос на презентацию переключается на навык создания (pptx-author).

Оценки fits (noul) и выбор Choice здесь расходятся: noul выше оценивает навык редактирования,
тогда как Choice выбирает создание. Они решают разные задачи: Choice определяет, какой именно
навык лучше, а noul — стоит ли вообще что-либо предлагать.

Запрос про Mastodon проходит обе проверки: его лучший noul fits оказывается выше 0.30, поэтому
алгоритм предлагает навык для X на запрос о Mastodon. Большинство подобных запросов отсекаются;
второй проход может отфильтровать только то, что передало ему широкое ранжирование, а здесь в
шортлист попали три близких варианта.

Приведенная ниже функция представляет собой весь рецепт: два запроса и два порога, возвращающие
не более одного имени навыка.

Чтобы применить ее к собственному каталогу, замените hermes_roster.json. Все вопросы выше читают
поля name, description, description_full и body из этого файла, и больше ничто в коде
не привязано к Hermes.

PYTHON THEME={NULL} api.wedstack.ru/v1
def suggest(request: str) -> tuple[str, ...]:
    """At most one skill name for a request, or () for "nothing here applies"."""
    wide = rank_wide(request)
    if wide["gate"] < GATE_THRESHOLD:
        return ()
    shortlist = tuple(name for name, _ in wide["ranked"][:SHORTLIST])
    result = rerank(request, shortlist, EXCERPT_CHARS)
    if max(result["fits"].values()) < FITS_THRESHOLD:
        return ()
    return (result["winner"],)


def suggestion_block(names: tuple[str, ...]) -> str:
    """What gets appended after the roster, in the suggestion.

    This string is a measured input rather than prose: it goes to the agent, so it is part
    of every graded turn's cache key. Editing a word here silently invalidates the shipped
    results and costs a live re-run to restore them.
    """
    body = (
        f"Relevant to the current request: {', '.join(names)}. Ignore this if it does not "
        "fit what the user actually asked for."
        if names
        else "No skill in the roster appears relevant to this request."
    )
    return f"\n\n<skill_relevance>\n{body}\n</skill_relevance>"


print(suggestion_block(suggest(DEMO[1])))
print(suggestion_block(suggest(DEMO[2])))
PLAINTEXT api.wedstack.ru/v1
<skill_relevance>
Relevant to the current request: pptx-author. Ignore this if it does not fit what the user actually asked for.
</skill_relevance>


<skill_relevance>
Relevant to the current request: xurl. Ignore this if it does not fit what the user actually asked for.
</skill_relevance>

Шаг 5: оценка эффективности подсказок

Каждый из 488 запросов отправляется агенту трижды — по одному тестовому шагу на вариант.
Запуски отличаются только тем, какая информация передается агенту:

что передается в системный промпт
агент без подсказок (baseline) ничего
агент с подсказкой TypeSafe результат, возвращенный suggest()
агент с идеальным ответом (oracle) точное имя нужного навыка или «ничего не подходит»

Третий вариант недостижим на практике — он служит теоретическим потолком для сравнения первых двух.

Формулировка подсказки решает две задачи. Она прямо указывает, что подсказку можно проигнорировать:
излишне настойчивая инструкция заставила бы агента соглашаться и с ошибочными подсказками, а неверный
навык хуже, чем отсутствие подсказки. Кроме того, если подсказывать нечего, агенту все равно
передается фраза о том, что подходящих навыков нет; пустое сообщение оставило бы без противовеса
собственную инструкцию каталога «в сомнительных случаях обязательно загружайте навык».

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
PLAINTEXT api.wedstack.ru/v1
run         wrong loads  needless loads
baseline          16.8%            9.8%
TypeSafe           7.3%            4.0%
oracle             2.5%            1.2%

baseline -> TypeSafe:  2.3x fewer wrong loads, 2.4x fewer needless ones
PYTHON THEME={NULL} api.wedstack.ru/v1
moved = [
    (
        baseline[p["text"]]["loaded"][:1] == [p["gold"]],
        run_turn(AGENT_MODEL, "TypeSafe", p["text"], arms["TypeSafe"][p["text"]])[
            "loaded"
        ][:1]
        == [p["gold"]],
    )
    for p in POSITIVES
]
print(
    f"of {len(POSITIVES)} covered requests: {sum(not b and a for b, a in moved)} the suggestion "
    f"fixed, {sum(b and not a for b, a in moved)} it broke"
)
PLAINTEXT api.wedstack.ru/v1
of 315 covered requests: 37 the suggestion fixed, 7 it broke

Подсказка исправляет гораздо больше запросов, чем ломает, однако в единичных случаях она сбивает
агента там, где он сам выбрал бы правильный вариант. Уверенная ошибочная подсказка обладает
внушающей силой, и это неизбежная плата за предварительное направление агента.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
output

Что показывают результаты

  • Доля ошибочных загрузок снизилась с 16.8% до 7.3%, а избыточных — с 9.8% до 4.0%, что покрывает
    большую часть разрыва между угадыванием по усеченному индексу и идеальным знанием правильного ответа.
  • Некоторые запросы, которые агент решал верно самостоятельно, оказываются ошибочными при наличии
    подсказки. Точные цифры приведены выше.

Используйте эту архитектуру, если ваш агент работает с обширным каталогом инструментов: недорогое
общее ранжирование всего списка, затем детальный анализ двух-трех лидеров. Любой из этапов может
закончиться решением ничего не загружать.

Открыть в песочнице

Сформируйте ссылку на песочницу для запроса о презентации из шага 4 с использованием полных
описаний кандидатов и фрагментов документации в качестве критериев.

PYTHON THEME={NULL} api.wedstack.ru/v1
demo_shortlist = tuple(name for name, _ in rank_wide(DEMO[1])["ranked"][:SHORTLIST])
playground_link = make_playground_link(
    build_state(DEMO[1]),
    rerank_questions(demo_shortlist, EXCERPT_CHARS),
    models=[TYPESAFE_MODEL],
)
display(
    Markdown(
        f"🔗 [Open the shortlist + questions in the TypeSafe playground]({playground_link})"
    )
)

Открыть шортлист + вопросы в песочнице TypeSafe →

Что дальше

Та же архитектурная схема применяется и в других руководствах:
Маршрутизация намерений (Intent Routing) для направления к
обработчику вместо навыка, Уверенность (Confidence) для
подбора пороговых значений, и
Speculative Fan-Out для объединения всех
вопросов в одном запросе.