AmigaОбучение ИИ
Модуль 5 · Подключаем клиент · урок 13 из 14

Промпты в клиенте

10 мин чтения▶ есть видеоверсия

Промпты на сервере есть, осталось научить клиент их использовать. Промпты контролирует человек: клиент должен показать, какие есть, дать выбрать и заполнить аргументы, получить сообщения и начать с них разговор. Сделаем это через команды со слешем — как в Claude Code. Заодно соберём окончательную версию client.py, в которой работают все три примитива.

Список промптов при старте

Пользователь должен видеть, что можно вызвать. Добавьте в main после вывода инструментов:

python
        prompts = [p.name for p in (await client.list_prompts()).prompts]
        print("Промпты:", ", ".join("/" + p for p in prompts))

list_prompts возвращает объект с полем prompts; у каждого промпта есть name, title, description и arguments — список с name, description и required. Для подсказки достаточно имён, но в настоящем интерфейсе стоит показывать title и аргументы: именно для этого они и передаются.

Команда → сообщения

Договоримся: строка, начинающаяся с /, — это вызов промпта. Первое слово — имя, остальные — аргументы по порядку. /summarize brief.md вызовет summarize с name="brief.md", /to_theses risks.md заказчикto_theses с name="risks.md" и audience="заказчик".

Добавьте в client.py:

python
async def messages_for(client: Client, query: str) -> list[dict]:
    """Превратить ввод пользователя в начальные сообщения для модели."""
    if query.startswith("/"):
        name, *args = query[1:].split()
        prompts = {p.name: p for p in (await client.list_prompts()).prompts}
        if name not in prompts:
            raise ValueError(f"Нет промпта {name!r}. Есть: {', '.join(prompts)}")
        arg_names = [a.name for a in (prompts[name].arguments or [])]
        result = await client.get_prompt(name, dict(zip(arg_names, args)))
        return [
            {"role": m.role, "content": m.content.text}
            for m in result.messages
            if isinstance(m.content, TextContent)
        ]
    return [{"role": "user", "content": await attach_docs(client, query)}]

Разберём. Мы получаем список промптов и по имени находим нужный, чтобы узнать порядок его аргументов: протокол передаёт аргументы словарём «имя → значение», а пользователь вводит их по порядку, и zip сопоставляет одно с другим. Лишние аргументы отбрасываются; недостающие обязательные вызовут ошибку от сервера ещё до выполнения функции промпта. Текст этой ошибки обезличен («Internal server error»), поэтому в настоящем клиенте стоит проверять обязательные аргументы по списку arguments самостоятельно и подсказывать пользователю, чего не хватает.

get_prompt возвращает объект с полем messages. У каждого сообщения есть role (user или assistant) и content; для текстовых промптов это TextContent с полем text — тот же тип, что у результатов инструментов. Мы переводим сообщения в формат Anthropic API: словарь с role и content. Если промпт вернул несколько сообщений (диалог с примерами), они все попадут в историю в нужном порядке.

Обычный запрос без слеша идёт прежним путём — через attach_docs с упоминаниями.

Собираем main

Окончательная версия:

python
async def main() -> None:
    server = StdioServerParameters(command="uv", args=["run", "docs_server.py"])
    async with Client(server) as client:
        names = [t.name for t in (await client.list_tools()).tools]
        print("Подключено. Инструменты:", ", ".join(names))
        prompts = [p.name for p in (await client.list_prompts()).prompts]
        print("Промпты:", ", ".join("/" + p for p in prompts))
        while True:
            try:
                query = (await asyncio.to_thread(input, "\n> ")).strip()
            except EOFError:
                break
            if not query or query.lower() in {"quit", "exit"}:
                break
            try:
                print(await run(client, await messages_for(client, query)))
            except Exception as e:
                print("Ошибка:", e)

Функция run из урока про клиент не изменилась: она принимает список сообщений и крутит цикл инструментов. Ей всё равно, откуда взялось первое сообщение — набрал его пользователь, собрал промпт или дополнили ресурсы.

Запуск

bash
uv run client.py

Наберите /summarize brief.md. В stderr появится [вызов read_doc {'name': 'brief.md'}] — промпт попросил модель прочитать документ инструментом, и она это сделала, — а затем выжимка в три-пять предложений. Наберите /to_theses risks.md заказчик и получите тезисы для заказчика; без второго аргумента — для команды разработки. Наберите /nope и увидите сообщение об ошибке со списком доступных промптов.

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

Как это делает Claude Code

В Claude Code промпты подключённых серверов появляются в списке команд как /mcp__project-docs__summarize. Имя сервера в команде нужно, потому что серверов может быть несколько, и у двух могут совпасть имена промптов. Хост вызывает prompts/get, получает сообщения и отправляет их модели — ровно наша messages_for, только с формой для аргументов вместо разбора по пробелам.

Что мы построили

Клиент теперь умеет всё, что нужно минимальному хосту:

  • запускает сервер по stdio и договаривается о протоколе;
  • передаёт инструменты модели и выполняет цикл вызовов;
  • прикладывает ресурсы по упоминанию @имя;
  • запускает промпты командами /имя аргументы.

Сто с небольшим строк. Всё остальное в настоящих хостах — интерфейс, подтверждения, несколько серверов, переподключение — надстройки над этими четырьмя вещами.

Попробуйте сами

10–15 мин на рабочем месте
  1. Выведите при старте не имена, а title и аргументы каждого промпта: /summarize <name>, /to_theses <name> [audience]. Квадратные скобки — для необязательных.
  2. Разрешите в аргументах промпта кавычки, чтобы /to_theses risks.md "финансовый директор" передавал аудиторию из двух слов. Модуль shlex из стандартной библиотеки сделает разбор за вас.
  3. Добавьте промпту summarize на сервере необязательный аргумент length со значениями «короткая» и «подробная», и проверьте из клиента, что оба варианта работают без изменений в client.py. Это и есть смысл протокола: сервер меняется, клиент — нет.

Коротко

  • list_prompts даёт имена, заголовки и аргументы; показывайте их пользователю как команды.
  • get_prompt(имя, {аргумент: значение}) возвращает messages с role и content; текст — в content.text.
  • Сообщения промпта переводятся в формат API один к одному и становятся началом разговора.
  • Недостающие обязательные аргументы отклоняет сервер до вызова функции.
  • Функции run всё равно, откуда взялись сообщения: от пользователя, из промпта или с ресурсами.
  • Клиент в сто строк умеет всё, что нужно минимальному хосту; остальное — надстройки.

Видеоверсия

Сценарий озвучки · 451 слово, ≈ 3 мин

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

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

Теперь команда. Договоримся: строка со слеша — это вызов промпта. Первое слово — имя, остальные — аргументы по порядку. Функция находит промпт по имени, чтобы узнать порядок его аргументов: протокол передаёт их словарём «имя — значение», а пользователь вводит по порядку, и мы сопоставляем одно с другим. Если промпта с таким именем нет, показываем список доступных. Если не хватает обязательного аргумента, сервер вернёт ошибку ещё до выполнения функции — её тоже увидит пользователь.

Дальше запрос промпта. Сервер возвращает список сообщений, у каждого роль и содержимое. Мы переводим их в формат Anthropic API один к одному: роль и текст. Если промпт вернул несколько сообщений — диалог с примерами, — все они попадут в историю в нужном порядке. Обычный запрос без слеша идёт прежним путём, через упоминания ресурсов.

Функция, которая крутит цикл инструментов, не изменилась. Ей всё равно, откуда взялось первое сообщение — набрал его пользователь, собрал промпт или дополнили ресурсы.

Запускаем. Набираем слеш, «саммарайз», имя брифа. В потоке ошибок появляется вызов чтения документа — промпт попросил модель взять документ инструментом, и она это сделала, — а потом выжимка. Набираем «тезисы» с именем документа и аудиторией «заказчик» — получаем тезисы для заказчика. Без аудитории — для команды разработки, это значение по умолчанию.

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

Claude Code делает то же самое: промпты серверов появляются в списке команд с именем сервера в названии — потому что серверов может быть несколько. Хост запрашивает промпт, получает сообщения и отправляет модели, только с формой для аргументов вместо разбора по пробелам.

Что мы построили. Клиент запускает сервер, передаёт инструменты модели и крутит цикл вызовов, прикладывает ресурсы по упоминанию, запускает промпты командами. Сто с небольшим строк. Всё остальное в настоящих хостах — интерфейс, подтверждения, несколько серверов — надстройки над этими четырьмя вещами.

Одна тонкость про недостающие аргументы. Если пользователь не передал обязательный аргумент, сервер вернёт ошибку, но её текст обезличен — просто «внутренняя ошибка сервера». Поэтому в настоящем клиенте стоит проверять обязательные аргументы по списку из ответа сервера самостоятельно и подсказывать пользователю, чего именно не хватает.

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

Отметка хранится только в вашем браузере