Проектируем MCP-инструменты, которыми LLM реально пользуются правильно
Большинство MCP-инструментов, которые мы видим, технически корректны и практически бесполезны. LLM их игнорируют, неправильно применяют или вызывают так, что толку нет. Принципы проектирования инструментов, которые LLM подхватывают естественно, с примерами типичных провалов и их фиксов.
Вы построили 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 the customer database.Лучшее описание:
search_customers: Find customers by name, email, or company. Returns up to 10 matching customers with their basic info. Use this when you need to identify a customer the user is referring to. For exact lookups by ID, use get_customer_by_id instead.Обратите внимание, что делает лучшая версия:
- Описывает входы («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": "Invalid input" }Лучшая ошибка:
{
"error": "validation_error",
"message": "The email '...' is not in valid format. It must be like 'name@example.com'.",
"field": "email",
"suggestion": "Ask the user for a valid email address."
}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). - Включает natural-language
summary, помогающий LLM сформулировать, что сказать дальше.
Поле summary мощное — это как дать LLM подсказку «к слову», как интерпретировать результат.
Принцип 6: один инструмент, одна вещь
Инструменты, делающие несколько вещей, путают LLM. Модели приходится решать одновременно: использовать ли инструмент И в каком режиме.
Запутывает:
manage_customer:
- mode: "search" | "get" | "update" | "delete"
- params: depends on modeLLM должна выбрать режим. Часто выбирают неверно. Хуже того, схема параметров сложна, потому что зависит от режима.
Лучше: раздельные инструменты.
search_customers: search by name/email/company
get_customer: get details by ID
update_customer: update specific fields
delete_customer: archive a customerКаждый инструмент однозначен. LLM выбирает один на основе намерения. Схемы простые.
Это означает больше инструментов, но каждый понятнее. LLM лучше справляется с 10 чёткими инструментами, чем с 3 мульти-режимными.
Принцип 7: ограничивайте входы
Где возможно, ограничивайте опции на входе. Enum-ы и валидация предотвращают галлюцинации.
Свободно:
{
status: string // could be anything
}Ограничено:
{
status: "active" | "trial" | "churned" | "suspended"
}Ограничение энфорсится на уровне схемы (constrained generation не даёт LLM производить невалидные значения).
То же применимо к enum-ам операций, severity, типов — чему угодно с известным набором допустимых значений.
Для дат используйте формат 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): returns customer + tickets + activity all in oneLLM не может легко сделать фильтр «топ-3» — этот инструмент возвращает всё по одному клиенту за раз. Чтобы решить задачу, LLM сначала надо узнать, кто топ-клиенты, потом вызвать этот инструмент 3 раза.
Лучший набор инструментов:
list_customers(sort_by="value", limit=N): returns customer summaries with priority info
list_tickets(customer_id, status): returns tickets for a customerLLM может скомпоновать: получить топ-клиентов, потом для каждого получить открытые тикеты. Композиция естественная.
Принцип: думайте о мульти-инструментных воркфлоу. Инструменты, которые хорошо композируются, используются; те, что нет, — часто нет.
Принцип 10: идемпотентность проговаривается
Для инструментов записи упоминайте требования к идемпотентности в описании:
create_invoice: Create a new invoice for a customer.
IMPORTANT: Pass an idempotency_key (a UUID you generate). If you retry this operation, use the same UUID to prevent duplicate invoices.
Parameters:
- amount: ...
- customer_id: ...
- idempotency_key: UUID to prevent duplicate creation on retry. Generate once per logical operation.Теперь LLM знает, что нужно сгенерировать UUID и использовать тот же при ретрае.
Без этой подсказки LLM может либо пропустить ключ (никакой идемпотентности), либо генерировать новый UUID на каждый ретрай (что сводит идею на нет).
Принцип 11: проговаривайте пред- и постусловия
Для инструментов с предусловиями или важными побочными эффектами — пишите об этом:
delete_customer: Archive a customer record. This is reversible within 30 days; after 30 days, the data is permanently deleted.
PRECONDITIONS:
- Customer must have no active subscriptions.
- Customer must have no open tickets.
If preconditions are not met, this tool returns an error indicating what to resolve first.
SIDE EFFECTS:
- All customer's contacts are also archived.
- Customer is removed from active reports.
- An audit log entry is created.LLM теперь знает, что проверить перед вызовом и чего ожидать после. Может корректно планировать многошаговые воркфлоу («сначала закрыть тикеты, потом удалить»).
Принцип 12: если сомневаетесь — примеры
Для сложных инструментов пример в описании помогает:
analyze_funnel: Analyze a conversion funnel from event data.
Parameters:
- start_date: ISO 8601 date
- end_date: ISO 8601 date
- steps: array of step definitions, each {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 — это осмысленная концепция.
Аналогично: не выставляйте устаревшие поля, внутренние флаги, отладочные параметры или всё остальное, что про вашу реализацию, а не про user-facing концепт.
Принцип 14: избегайте «магических» строк
Некоторые инструменты требуют строк, похожих на команды или коды. Они подвержены ошибкам.
Плохо:
modify_record(record_id: string, change_string: string)
// where change_string is like "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 (safe, idempotent):
- search_customers
- get_customer_by_id
- list_tickets
- list_orders
Compute tools (no state changes):
- summarize_account_activity
- analyze_funnel
- calculate_lifetime_value
Write tools (state changes, need idempotency):
- create_customer
- update_customer_email
- create_ticket
- send_email
Destructive tools (require careful authorization):
- delete_customer
- cancel_subscription
- archive_recordТаксономия помогает:
- Применить соответствующие ограничения (идемпотентность, подтверждение для деструктивных).
- Задокументировать категории в системных промптах для LLM.
- Найти недостающие инструменты (если категория пуста, нужен ли вам такой?).
Полезное дополнение в системном промпте:
Tool categories available:
- READ tools (safe to call): search_customers, get_customer_by_id, ...
- COMPUTE tools (no side effects): summarize_account_activity, ...
- WRITE tools (side effects, include idempotency_key): create_customer, ...
- DESTRUCTIVE tools (require human confirmation): delete_customer, ...
Before calling a WRITE or DESTRUCTIVE tool, confirm with the user.Это формирует поведение 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 как к пользователю. Проектируйте соответственно. Вложение многократно окупается в том, насколько хорошо ваши инструменты реально используются.