Вы построили MCP-сервер. Инструменты работают. Подключаете LLM-агента. Наблюдая за его работой, замечаете: он игнорирует ваши инструменты, вызывает их с непонятными параметрами, путается в том, какой инструмент использовать, склеивает их в странные последовательности.
Это разрыв между «инструменты существуют» и «LLM их использует правильно». Именно здесь сливаются усилия большинства MCP-серверов. Компании строят мощные возможности, выставляют их в виде инструментов и смотрят, как LLM не справляются с ними.
Переформулировка, которая помогает: относитесь к проектированию инструментов как к UX-дизайну, где пользователь — это LLM. Описание инструмента — это UI. Схема — это форма. Сообщения об ошибках — обратная связь. Сделайте это правильно — и LLM работают эффективно. Сделайте плохо — ваш изощрённый бэкенд для агента невидим.
Эта статья — о принципах, с конкретными примерами того, что работает, а что нет.
Принцип 1: имена инструментов передают намерение
Имя инструмента — первое, что видит LLM. Оно должно описывать, что делает инструмент, в терминах действия.
Плохо:
customers(существительное, нет действия)process_customer(расплывчато)do_x(бессмысленно)
Лучше:
search_customers(понятное действие)get_customer_by_id(конкретная операция)update_customer_email(конкретное изменение)
Почему это важно: LLM сканируют список инструментов в поисках релевантных. Описательное имя позволяет быстро найти нужный. Расплывчатое заставляет внимательно читать описание (что они делают не всегда).
Полезный паттерн: стандартные глагольные префиксы.
list_*,search_*,get_*для чтения.create_*,update_*,delete_*для записи.analyze_*,summarize_*для вычислений.
Согласованность в рамках сервера помогает LLM выстраивать ментальные модели.
Принцип 2: описания — это промпты
Описание инструмента — самый важный текст на вашем сервере. Это то, по чему LLM решает, использовать ли инструмент и как.
Плохое описание:
search_customers: Поиск по базе клиентов.
Лучшее описание:
search_customers: Найдите клиентов по имени, email или компании. Возвращает до 10 совпадений с базовой информацией. Используйте, когда нужно определить клиента, о котором говорит пользователь. Для точного поиска по ID используйте get_customer_by_id.
Обратите внимание, что делает лучшая версия:
- Описывает входы («by name, email, or company»).
- Описывает выходы («up to 10 matching customers with their basic info»).
- Говорит, когда использовать («when you need to identify a customer the user is referring to»).
- Говорит, когда не использовать («For exact lookups by ID, use get_customer_by_id instead»).
Часть «когда не использовать» — критична. Без неё LLM может вызвать search_customers, когда get_customer_by_id подошёл бы лучше.
Принцип 3: описания параметров имеют значение
Каждому параметру нужно описание. Не полагайтесь только на имя.
Плохо:
{
customer_id: string,
fields: string[]
}
Лучше:
{
customer_id: string, // "The customer's unique identifier. Get this from search_customers or from explicit user input."
fields: string[] // "Specific fields to return. Available: name, email, phone, tier, created_at, last_active. If not specified, returns name and email."
}
Описания:
- Объясняют LLM, как получить значение.
- Указывают допустимые значения там, где применимо.
- Указывают значения по умолчанию.
Принцип 4: ошибки ведут к восстановлению
Когда инструмент возвращает ошибку, её сообщение определяет следующее действие LLM. Расплывчатые ошибки приводят к запутавшимся агентам.
Плохая ошибка:
{ "error": "Неверный ввод" }
Лучшая ошибка:
{
"error": "validation_error",
"message": "Email «...» имеет неверный формат. Он должен быть вида «name@example.com».",
"field": "email",
"suggestion": "Спросите у пользователя корректный email."
}
LLM теперь знает:
- Что пошло не так (ошибка валидации поля email).
- Как это починить (использовать валидный формат email).
- Что делать дальше (спросить пользователя).
Сравните поведение агента с двумя этими ошибками. Первая может повторить тот же вызов (зря), сдаться (плохой UX) или выдумать валидный ввод. Вторая ведёт к чистому взаимодействию с пользователем.
Принцип 5: выход определяет следующее действие
Выход инструмента определяет, что LLM сделает дальше. Проектирование выхода влияет на поведение агента.
Плохой выход поиска:
[
{"id": "c1", "n": "John", "e": "john@..."},
{"id": "c2", "n": "Jane", "e": "jane@..."}
]
Лучший выход:
{
"customers": [
{"id": "c1", "name": "John Smith", "email": "john@example.com", "tier": "pro"},
{"id": "c2", "name": "Jane Doe", "email": "jane@example.com", "tier": "free"}
],
"total_found": 2,
"summary": "Found 2 customers matching 'john'. Note that one is named 'Jane Doe' but has 'john' in their email."
}
Лучший выход:
- Использует читаемые имена полей.
- Включает мета-контекст (
total_found). - Включает
summaryна естественном языке, помогающий LLM сформулировать, что сказать дальше.
Поле summary мощное — это как дать LLM подсказку «к слову», как интерпретировать результат.
Принцип 6: один инструмент, одна вещь
Инструменты, делающие несколько вещей, путают LLM. Модели приходится решать одновременно: использовать ли инструмент И в каком режиме.
Запутывает:
manage_customer:
- mode: "search" | "get" | "update" | "delete"
- params: зависит от mode
LLM должна выбрать режим. Часто выбирают неверно. Хуже того, схема параметров сложна, потому что зависит от режима.
Лучше: раздельные инструменты.
search_customers: поиск по имени/email/компании
get_customer: получить детали по ID
update_customer: обновить конкретные поля
delete_customer: архивировать клиента
Каждый инструмент однозначен. LLM выбирает один на основе намерения. Схемы простые.
Это означает больше инструментов, но каждый понятнее. LLM лучше справляется с 10 чёткими инструментами, чем с 3 мульти-режимными.
Принцип 7: ограничивайте входы
Где возможно, ограничивайте опции на входе. Enum-ы и валидация предотвращают галлюцинации.
Свободно:
{
status: string // could be anything
}
Ограничено:
{
status: "active" | "trial" | "churned" | "suspended"
}
Ограничение действует на уровне схемы (ограниченная генерация не даёт LLM выдавать недопустимые значения).
То же применимо к перечислениям (enum) операций, уровней серьёзности, типов — чему угодно с известным набором допустимых значений.
Для дат используйте формат ISO 8601 и указывайте это в описании («Date in ISO 8601 format, e.g., 2026-05-15»). Без этого LLM производят даты в случайных форматах.
Принцип 8: значения по умолчанию снижают галлюцинации
Когда у параметров есть разумные значения по умолчанию, делайте их опциональными, а значение по умолчанию применяйте на стороне сервера.
Плохо:
{
query: string,
limit: number, // LLM has to provide some value
include_archived: boolean,
sort_by: string
}
LLM должна подобрать значения для всех. Они могут быть неверными.
Лучше:
{
query: string,
limit: number = 10, // sensible default
include_archived: boolean = false, // safe default
sort_by: "relevance" | "name" | "created_at" = "relevance" // most common
}
LLM указывает только параметры, важные для конкретного запроса. Меньше параметров — меньше места для путаницы.
Документируйте значения по умолчанию в описании: «Limit: number of results to return. Default 10, max 50.»
Принцип 9: сочетаемость имеет значение
Инструменты должны складываться в рабочие процессы, которые LLM может построить. Правильная гранулярность делает сложные задачи простыми.
Рассмотрим задачу: «Расскажи мне обо всех открытых тикетах для наших топ-3 клиентов».
Плохой набор инструментов:
get_customer_summary(customer_id): возвращает клиента + тикеты + активность одним вызовом
LLM не может легко сделать фильтр «топ-3» — этот инструмент возвращает всё по одному клиенту за раз. Чтобы решить задачу, LLM сначала надо узнать, кто топ-клиенты, потом вызвать этот инструмент 3 раза.
Лучший набор инструментов:
list_customers(sort_by="value", limit=N): возвращает сводки клиентов с информацией о приоритете
list_tickets(customer_id, status): возвращает тикеты клиента
LLM может скомпоновать: получить топ-клиентов, потом для каждого получить открытые тикеты. Композиция естественная.
Принцип: думайте о рабочих процессах из нескольких инструментов. Инструменты, которые хорошо сочетаются, используются; те, что нет, — часто нет.
Принцип 10: идемпотентность проговаривается
Для инструментов записи упоминайте требования к идемпотентности в описании:
create_invoice: Создайте новый счёт для клиента.
ВАЖНО: Передайте idempotency_key (UUID, который вы сгенерируете). Если повторяете операцию, используйте тот же UUID, чтобы избежать дублирования счетов.
Parameters:
- amount: ...
- customer_id: ...
- idempotency_key: UUID для предотвращения дублирования при повторе. Генерируйте один раз на логическую операцию.
Теперь LLM знает, что нужно сгенерировать UUID и использовать тот же при повторной попытке.
Без этой подсказки LLM может либо пропустить ключ (никакой идемпотентности), либо генерировать новый UUID при каждой повторной попытке (что сводит идею на нет).
Принцип 11: проговаривайте пред- и постусловия
Для инструментов с предусловиями или важными побочными эффектами — пишите об этом:
delete_customer: Архивируйте запись клиента. Это обратимо в течение 30 дней; после 30 дней данные удаляются безвозвратно.
PRECONDITIONS:
- У клиента не должно быть активных подписок.
- У клиента не должно быть открытых тикетов.
Если предусловия не выполнены, инструмент возвращает ошибку с указанием, что нужно решить в первую очередь.
SIDE EFFECTS:
- Все контакты клиента тоже архивируются.
- Клиент удаляется из активных отчётов.
- Создаётся запись в журнале аудита.
LLM теперь знает, что проверить перед вызовом и чего ожидать после. Может корректно планировать многошаговые процессы («сначала закрыть тикеты, потом удалить»).
Принцип 12: если сомневаетесь — примеры
Для сложных инструментов пример в описании помогает:
analyze_funnel: Проанализируйте воронку конверсии по данным событий.
Parameters:
- start_date: дата в формате ISO 8601
- end_date: дата в формате ISO 8601
- steps: массив определений шагов, каждый {event_name: string, filters?: object}
Example:
{
"start_date": "2026-01-01",
"end_date": "2026-01-31",
"steps": [
{"event_name": "signup"},
{"event_name": "first_login"},
{"event_name": "first_action", "filters": {"action_type": "create_project"}},
{"event_name": "subscription_started"}
]
}
Примеры учат LLM структуре лучше, чем одни только схемы.
Принцип 13: не выставляйте внутренности
LLM не нужно знать структуру вашей базы данных или внутренние ID. Выставляйте чистую концептуальную модель.
Плохо:
get_user_by_pk(pk: number)
LLM приходится знать про «primary key» — концепцию из БД.
Лучше:
get_user(user_id: string)
Спрячьте концепцию БД. LLM использует user_id — это осмысленная концепция.
Аналогично: не выставляйте устаревшие поля, внутренние флаги, отладочные параметры или всё остальное, что относится к вашей реализации, а не к понятию, с которым работает пользователь.
Принцип 14: избегайте «магических» строк
Некоторые инструменты требуют строк, похожих на команды или коды. Они подвержены ошибкам.
Плохо:
modify_record(record_id: string, change_string: string)
// где change_string вида "field1=value1;field2=value2"
LLM должна закодировать изменения в специфичный строковый формат. Будут ошибки.
Лучше:
update_record(record_id: string, updates: { field1?: any; field2?: any; ... })
Структурированные обновления как объект. LLM может использовать любое поле напрямую.
Принцип 15: тестируйте с настоящими LLM
Описания инструментов хорошо читаются человеком, но могут запутать LLM. Единственный способ узнать — протестировать.
Полезный порядок работы:
- Соберите инструмент.
- Дайте LLM-агенту попробовать несколько реалистичных задач, используя только ваши инструменты.
- Понаблюдайте за провалами.
- Подкорректируйте описания на основе провалов.
- Повторите.
Паттерны, которые встретите:
- LLM использует не тот инструмент → имя или описание не ясны.
- LLM передаёт неверные значения параметров → описание параметра или схема требуют доработки.
- LLM сдаётся после ошибок → сообщения об ошибках надо улучшить.
- LLM не пробует инструмент, который помог бы → инструмент плохо подан или плохо назван.
Каждая проблема подсказывает конкретное исправление.
Диагностика: признаки того, что инструменты спроектированы плохо
Несколько паттернов, указывающих на проблемы дизайна:
LLM часто использует не тот инструмент. Вы видите, что она зовёт search_customers, когда должна была позвать get_customer_by_id. Как исправить: уточнить, какой инструмент для какой ситуации.
LLM вызывает много инструментов для одной задачи. Склеивает 5 вызовов, чтобы сделать то, что должно быть 1. Как исправить: возможно, нужен составной инструмент более высокого уровня, или гранулярность слишком мелкая.
LLM сдаётся после ошибок. Пробует один раз, получает ошибку и говорит пользователю, что не может помочь. Как исправить: лучшие сообщения об ошибках с подсказками следующих шагов.
LLM галлюцинирует значения параметров. Изобретает user_id, даты, ID. Как исправить: уточнить, как получать валидные значения; добавить ограничения; добавить обработку ошибок, которая ловит и объясняет проблему.
LLM повторяет один и тот же неудачный вызов. Та же ошибка, повторно. Как исправить: сообщение об ошибке не говорит LLM конкретно, что не так.
LLM не пользуется мощным инструментом. Вы построили отличный инструмент; LLM его не вызывает. Как исправить: улучшить обнаружение (более понятное имя, лучшее описание, подсказка «use this when…»).
Таксономия инструментов
Полезное упражнение: организовать инструменты в таксономию.
Read tools (безопасные, идемпотентные):
- search_customers
- get_customer_by_id
- list_tickets
- list_orders
Compute tools (без изменения состояния):
- summarize_account_activity
- analyze_funnel
- calculate_lifetime_value
Write tools (изменяют состояние, нужна идемпотентность):
- create_customer
- update_customer_email
- create_ticket
- send_email
Destructive tools (требуют осторожной авторизации):
- delete_customer
- cancel_subscription
- archive_record
Таксономия помогает:
- Применить соответствующие ограничения (идемпотентность, подтверждение для деструктивных).
- Задокументировать категории в системных промптах для LLM.
- Найти недостающие инструменты (если категория пуста, нужен ли вам такой?).
Полезное дополнение в системном промпте:
Доступные категории инструментов:
- READ tools (безопасно вызывать): search_customers, get_customer_by_id, ...
- COMPUTE tools (без побочных эффектов): summarize_account_activity, ...
- WRITE tools (побочные эффекты, включайте idempotency_key): create_customer, ...
- DESTRUCTIVE tools (требуют подтверждения человеком): delete_customer, ...
Перед вызовом WRITE или DESTRUCTIVE tool подтвердите с пользователем.
Это формирует поведение LLM на уровне рабочего процесса, а не только отдельных вызовов.
Примеры типичных улучшений
Чтобы сделать принципы конкретнее, примеры «до» и «после»:
Пример 1: инструмент поиска
До:
// search documents
{
name: "documents",
description: "Search documents",
inputSchema: { query: "string" }
}
После:
{
name: "search_documents",
description: `Search internal documents (knowledge base, wiki pages, policies).
Returns matching documents with title, excerpt, and link. Use when the user asks about company policies, procedures, or internal documentation. Returns up to 10 most relevant matches by semantic similarity.`,
inputSchema: {
query: {
type: "string",
description: "Search query. Be specific. Good: 'remote work policy 2026'. Bad: 'documents about work'."
},
document_type: {
type: "string",
enum: ["policy", "procedure", "guide", "faq", "any"],
default: "any",
description: "Filter to a specific type of document."
},
limit: {
type: "number",
default: 5,
maximum: 10,
description: "Number of results."
}
}
}
Пример 2: инструмент действия
До:
{
name: "send_email",
description: "Send an email",
inputSchema: {
to: "string",
subject: "string",
body: "string"
}
}
После:
{
name: "draft_email_to_customer",
description: `Draft an email to a customer based on a recent interaction. The email is saved as a draft for human review before sending — it is NOT sent automatically. The user must approve drafts in their inbox.
Use when:
- You've identified an action requiring follow-up with the customer.
- You have a specific reason and content for the email.
Do NOT use:
- To send marketing or promotional content.
- Without explicit user request.
- To respond to refund or cancellation requests (escalate to human instead).`,
inputSchema: {
customer_id: {
type: "string",
description: "Customer ID from search_customers or get_customer."
},
subject: {
type: "string",
description: "Email subject, 4-8 words, specific. Avoid generic subjects like 'Following up'."
},
body: {
type: "string",
description: "Email body, plain text. 3-5 sentences. Personal, specific, not template-y."
},
tone: {
type: "string",
enum: ["professional", "friendly", "apologetic", "urgent"],
default: "professional",
description: "Tone of the email."
},
idempotency_key: {
type: "string",
description: "UUID for this draft. Use the same UUID if retrying to avoid duplicates."
}
}
}
Версии «После» направляют LLM намного эффективнее. Они кажутся многословными; они того стоят.
Главный вывод
Проектирование инструментов для LLM — отдельная дисциплина. Принципы не интуитивны; они требуют относиться к LLM как к пользователю и проектировать интерфейс соответственно.
Паттерны, которые имеют значение:
- Имена-глаголы действия.
- Богатые описания, объясняющие что, когда и когда-не.
- Описание каждого параметра с примерами и ограничениями.
- Структурированные, действенные сообщения об ошибках.
- Формы выхода, направляющие следующее действие.
- Один инструмент — одна концепция.
- Разумные значения по умолчанию.
- Сочетаемая гранулярность.
- Явная идемпотентность.
- Документированные пред-/постусловия.
- Примеры для сложных инструментов.
- Скрытые внутренности.
- Тестирование с настоящими LLM.
Большинство MCP-серверов проваливаются не потому, что протокол сложен, а потому, что инструменты не были спроектированы с расчётом на LLM. Сделайте дизайн инструментов правильно — и сервер становится эффективным; сделайте плохо — и ваш изощрённый бэкенд бесполезен.
Относитесь к LLM как к пользователю. Проектируйте соответственно. Вложение многократно окупается в том, насколько хорошо ваши инструменты реально используются.



