Инструменты и протоколы: вызов функций, MCP, свои интеграции
В предыдущей главе мы разбирали окно контекста как ограниченный ресурс. Эта глава — про то, что этот ресурс тратит и что взамен даёт: инструменты. Начнём с утверждения, которое стоит запомнить дословно.
Модель не вызывает инструменты. Она печатает структурированный текст, в котором есть имя инструмента и аргументы. Вызов делает программа вокруг модели — харнесс. Модель не знает, состоялся ли вызов, пока харнесс не положит результат обратно в контекст.
Отсюда всё содержание главы. Инструмент — это не «способность модели», а контракт между харнессом и моделью: он занимает место в окне, требует прав и может отработать не так, как модель считает.
Из чего состоит инструмент
С точки зрения модели инструмент — это три вещи в её контексте: имя, описание на естественном языке и схема аргументов (обычно JSON Schema). Ни кода, ни реализации, ни гарантий модель не видит: она выбирает инструмент так же, как дописывает текст — по описанию, по именам полей, по тому, что было раньше в диалоге.
Практический вывод: описание инструмента — это промпт, а не документация. Оно должно говорить не только «что делает», но и «когда НЕ применять» и «что вернётся при отказе». Формат вызова стандартизован у всех крупных провайдеров (function calling у OpenAI, tool use у Anthropic и так далее): детали сериализации отличаются, суть одна.
# Определение инструмента — это данные, а не код. Ровно так его видит модель.
RUN_TESTS = {
"name": "run_tests",
"description": (
"Запускает pytest для ОДНОГО пакета репозитория и возвращает код выхода, "
"число прошедших и упавших тестов и последние 80 строк вывода. "
"НЕ применять для линтеров и сборки — для них есть отдельные инструменты. "
"Ничего не коммитит и не меняет файлы. "
"Код выхода 5 (не найдено ни одного теста) — это НЕ успех."
),
"input_schema": {
"type": "object",
"properties": {
"package": {"type": "string", "description": "Путь от корня, например src/billing"},
},
"required": ["package"],
"additionalProperties": False,
},
}
Две строки в описании — «НЕ применять для линтеров» и «код выхода 5 — не успех» — появились после
того, как агент один раз запустил run_tests вместо линтера и один раз отрапортовал «тесты зелёные»
на пустом наборе. Описания пишутся по следам отказов; это нормальный цикл, а не брак проектирования.
Цикл вызова: кто что делает
возможно запрос апрува у человека H->>T: фактический запуск процесса T-->>H: stdout, stderr, код выхода Note over H: обрезка вывода до бюджета,
нормализация формата H->>M: результат как сообщение с ролью tool M-->>H: интерпретация результата или следующий вызов H-->>U: отчёт
Три места, где всё ломается, видны прямо на диаграмме:
- Шаг 3 — модель предлагает несуществующий инструмент или аргумент не той формы; схема ловит это по форме, но не по смыслу.
- Шаг 7 — вывод обрезается: модель видит хвост, а причина ошибки была в начале.
- Шаг 9 — интерпретация: модель прочитала
0 passed, 0 failedи написала «тесты проходят». Формально она не соврала про вывод — она соврала про смысл.
Встроенные инструменты кодового агента
У любого практичного кодового агента набор примерно один и тот же:
| Инструмент | Что делает | Чем опасен |
|---|---|---|
| чтение файла | отдаёт содержимое, часто с лимитом строк | молча обрезает длинный файл — модель судит по куску |
| поиск по содержимому | grep/ripgrep по репозиторию | регэксп ловит не то; «не нашлось» трактуется как «такого нет» |
| поиск по именам | glob | зависит от .gitignore и скрытых каталогов |
| правка файла | точечная замена строки | «уникальная строка» оказалась не уникальной |
| запись файла | перезапись целиком | затирает то, чего модель не читала |
| терминал | произвольная команда | всё сразу: права, сеть, необратимость |
Терминал — универсальный инструмент, и это ключевой факт главы. Если в системе есть CLI, агенту
не нужна отдельная интеграция: gh, psql, kubectl, aws, ваш make test — это уже инструменты.
Большая часть желаний «напишем MCP-сервер для X» закрывается командой плюс двумя строчками в
контракте проекта: как её звать и чего с ней не делать.
Обратная сторона: терминал — дыра размером с вашу машину, схема тут не валидирует ничего (аргумент один, и это строка). Отсюда allowlist команд и запрет разрушительных операций без апрува: подробнее в главе https://courses.digitable.life/post/ai-agents/13-security/.
Сколько стоят инструменты
Определения инструментов сериализуются в запрос и оплачиваются как входные токены на каждом шаге цикла, а не раз за сессию: запрос не хранит состояние, харнесс каждый раз шлёт весь контекст заново.
Три следствия, заметных на практике:
- Постоянная плата. Двадцать MCP-серверов по десять инструментов — это двести описаний в каждом запросе. Кэширование префикса промпта (есть у основных провайдеров) снижает стоимость повторной отправки, но место в окне занимают те же токены: кэш экономит деньги и время, а не контекст.
- Деградация выбора. Чем больше похожих инструментов, тем чаще берётся не тот: нужное в середине длинного перечня находится хуже (Lost in the Middle, Liu et al., 2023 — работа про документы, но механика выбора из перечня та же).
- Выводы дороже определений. Один
kubectl get pods -Aили неотфильтрованный лог перевешивает все схемы вместе взятые: бюджет вывода — параметр вашей интеграции, а не мелочь.
Измерьте у себя, а не верьте мне на слово. Точное число даёт токенайзер провайдера; прикидка «символы делить на четыре» годится для английского и заметно врёт на русском и на JSON.
# 1. Базовая линия: сессия без единого MCP-сервера, тривиальный вопрос,
# счётчик входных токенов первого запроса в логах харнесса.
# 2. Подключаем серверы по одному, повторяем вопрос: разница — цена подключения.
# 3. Что за неделю реально вызывалось (путь и формат транскрипта зависят от версии):
grep -ho '"name":"[a-z_]*"' ~/.claude/projects/*/*.jsonl | sort | uniq -c | sort -rn
Смысл важнее команды: инструмент, который за месяц не вызвали ни разу, вы всё это время оплачивали.
Когда инструмент вообще не нужен
Самый частый способ потратить время — обернуть в интеграцию то, что дешевле сделать иначе.
во внешней системе"] --> B{"Задача разовая?"} B -->|да| C["Сделайте руками
или одной командой в терминале"] B -->|нет| D{"Есть готовый CLI
или HTTP API с curl?"} D -->|да| E{"Нужен контроль
аргументов и прав?"} E -->|нет| F["Терминал + строка в контракте проекта:
как звать и чего не делать"] E -->|да| G["Скрипт в репозитории:
узкий интерфейс, свой код выхода,
тестируется без агента"] D -->|нет| H{"Нужен нескольким людям
или нескольким хостам агентов?"} H -->|нет| G H -->|да| I{"Нужны состояние, авторизация,
подписки на изменения?"} I -->|нет| G I -->|да| J["MCP-сервер"] G -.->|"если позже понадобится
другим хостам"| J
Заметьте направление стрелок: скрипт в репозитории — не черновик MCP-сервера, а обычно финальная форма. У него есть свойства, которых у сервера нет: он работает без агента, его видит код-ревью, он запускается в CI, покрывается тестами и не тратит контекст, пока не вызван. Оговорка в другую сторону: как только он нужен трём людям с разными IDE, двум CI-раннерам и агенту в пайплайне, вы начинаете переписывать обёртки — и тогда протокол окупается.
MCP: что он на самом деле решает
Model Context Protocol опубликован Anthropic в ноябре 2024 года и открыт; сегодня его поддерживают разные хосты, не только Claude Code.
Задача комбинаторная. Есть M хостов (Claude Code, Cursor, IDE-плагин, ваш собственный раннер) и N систем (Jira, Postgres, Sentry, внутренний сервис). Без общего протокола вы пишете M×N интеграций, с протоколом — M клиентов и N серверов, то есть M+N.
Технически это JSON-RPC 2.0 (спецификация) поверх транспорта: локального stdio для серверов на вашей машине и сетевого HTTP для удалённых. Имена и статусы сетевых транспортов менялись между ревизиями спецификации — не берите на веру ни этот абзац, ни статью годичной давности, открывайте текущую ревизию.
Сервер объявляет три разных вещи, и их регулярно путают:
| Примитив | Кто инициирует | Аналогия | Типичная ошибка |
|---|---|---|---|
| tools | модель | POST-запрос: действие с эффектом | делать инструментом то, что должно быть данными |
| resources | клиент или пользователь | GET-запрос: чтение по URI | ждать, что модель «сама» их подтянет |
| prompts | пользователь | шаблон-команда | прятать в них бизнес-логику |
Ключевое отличие: ресурсы не попадают в контекст сами по себе — их подставляет хост по явному действию пользователя или по своей логике. Ждать, что модель «увидит» объявленный ресурс, бессмысленно.
Чего MCP не даёт. Он не делает модель умнее, не добавляет ей памяти, не гарантирует правильный выбор инструмента и не решает вопрос прав: авторизация остаётся задачей вашей и хоста. И отдельно: подключая удалённый сервер, вы впускаете его описания инструментов прямо в свой системный контекст, а его ответы — в рассуждение модели.
Свой сервер: минимальный пример и его цена
Официальные SDK лежат в репозиториях проекта; API питоновского менялся между версиями — сверяйтесь с README той, которую ставите. Идея неизменна: функция, схема аргументов из аннотаций типов, описание из докстринга.
# pip install mcp — имя пакета и путь импорта проверьте в README вашей версии SDK
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("billing-tools")
@mcp.tool()
def refund_status(order_id: str) -> str:
"""Возвращает статус возврата по заказу.
Только чтение: ничего не меняет. order_id — строка вида ORD-000123.
Если заказ не найден, возвращает 'not_found', а не пустую строку —
чтобы модель не приняла отсутствие данных за отсутствие возврата.
"""
if not ORDER_RE.fullmatch(order_id):
# Ошибка — это тоже промпт: она объясняет модели, как исправиться.
return "invalid_order_id: ожидается формат ORD-000123, получено " + order_id[:32]
row = db.fetch_one("SELECT state FROM refunds WHERE order_id = %s", order_id)
return row["state"] if row else "not_found"
if __name__ == "__main__":
mcp.run()
Двадцать строк. Теперь честный счёт дальше: сервер нужно установить на всех машинах и зафиксировать
версию; обновлять синхронно со схемой БД; он не покрыт тестами репозитория, если живёт отдельно; он
тратит контекст у всех, кто его подключил, включая тех, кому он не нужен. Тот же результат скриптом
bin/refund-status ORD-000123 стоит одну строку в контракте проекта, покрывается обычным тестом и не
требует ничего устанавливать. Выбирайте сервер тогда, когда клиентов больше одного — это
единственный критерий, который выдерживает проверку временем.
Как проектировать инструмент, который агент не сломает
Правила выведены из отказов, а не из вкуса. Каждое стоило кому-то часа разбирательств.
1. Один инструмент — одно намерение. manage_database(action=...) с семью значениями action
проигрывает трём отдельным инструментам: схема не может выразить «при action=drop обязателен
confirm», и модель регулярно комбинирует поля не так.
2. Ошибка — это подсказка, а не код. Сравните:
плохо: Error: 400
хорошо: invalid_argument: поле 'since' ожидает ISO-8601 в UTC, например
2026-07-01T00:00:00Z; получено '01.07.2026'. Повторите вызов с исправленным значением.
Второй вариант модель почти всегда чинит с первой попытки, первый порождает три случайных вызова подряд. Ошибка попадает в контекст и работает как инструкция.
3. Бюджет вывода задан явно, ответ детерминирован. Инструмент, способный вернуть мегабайт, должен
возвращать голову и хвост с отметкой ... обрезано 4120 строк ... — без отметки модель сделает вывод
по фрагменту и не будет знать, что видела фрагмент. И одинаковые аргументы должны давать одинаковый
ответ: иначе ломается главное свойство отчёта, возможность перепроверить.
4. Разделяйте чтение и запись, помечайте разрушительное. Хост может спрашивать апрув только на изменяющие вызовы — но лишь если вы честно проставили признак. Аннотация «только чтение» — заявление сервера, а не проверяемая гарантия: хост верит вам на слово, ограничивать должна система прав.
5. Идемпотентность и dry-run. Агент повторяет вызовы — после таймаута, после компакции, просто
«на всякий случай». Ключ идемпотентности превращает повтор из инцидента в ничто, а режим
dry_run=true, возвращающий план, стоит дёшево и спасает регулярно.
6. Никаких скрытых состояний и угадываемых единиц. timeout: 30 — это секунды или миллисекунды?
Модель угадает, и иногда неправильно; пишите timeout_seconds. Сценарий «сначала вызови connect,
потом query» переживёт не каждую компакцию контекста: состояние держите на стороне сервера, а
инструменты делайте самодостаточными.
Где агент врёт про инструменты
Это центральный раздел главы. Отказы вокруг инструментов образуют устойчивую типологию.
«Ответ из головы вместо вызова». Агент уверенно описывает содержимое файла, который не читал, или
поведение функции, которую не запускал. Это ложь не про инструмент, а про источник. Ловится одним
требованием: каждое утверждение о внешней системе несёт адрес — файл и строку, команду и её
вывод, URL и дату. Это правило RSN-02 из нашего пакета products/workbench/templates/memory/ и
самое дешёвое в применении: нужна строка в контракте проекта и привычка не принимать отчёт без адресов.
«Отсутствие совпадений равно отсутствию кода». grep не нашёл handleRefund — значит, обработки
возвратов в проекте нет. На самом деле она называется processReturn, лежит в другом регистре или в
сгенерированном файле, исключённом из поиска. Отрицательный результат поиска — слабое
свидетельство, а агенты систематически трактуют его как сильное.
«Код выхода 0 при пустом результате». pytest вернул 5 (тесты не собраны), обёртка проглотила
код, модель увидела «no failures» и отрапортовала зелёный прогон. Лечится на стороне инструмента:
пустой результат — отдельное состояние, а не успех.
«План подан как выполненная работа». Неприятен формой: отчёт выглядит как список сделанного,
потому что писался в том же стиле, что и план. Ловится сверкой с эффектами: git diff --stat,
время модификации файлов, записи в логах.
Полная типология — в главе https://courses.digitable.life/post/ai-agents/09-failure-modes/. Здесь важно запомнить: заметная часть отказов происходит не в модели, а на границе между харнессом и инструментом.
Жизненный цикл вызова и точки контроля
Два перехода требуют внимания. Таймаут → Предложен: побочный эффект мог произойти, и повтор без
ключа идемпотентности — это второй платёж, второе письмо, второй деплой. Отклонён человеком →
Интерпретация: модель нередко продолжает так, будто вызов состоялся, особенно если отказ пришёл
сухим denied, — возвращайте причину отказа текстом.
Проверяемость: как убедиться, что вызов был
Утверждение агента о поведении кода не стоит ничего без прогона. В инструментах это принимает конкретную форму — четыре приёма по возрастанию надёжности:
- Требовать команду и её вывод в отчёте. Дёшево, ловит выдуманные вызовы, но не выдуманный вывод.
- Смотреть транскрипт сессии. Харнесс пишет лог вызовов: видно, что вызвано и с чем.
- Проверять побочные эффекты независимо.
git diff, свежесть артефактов, записи в БД, строки в логах сервиса. Ловит и выдуманный вывод тоже. - Прогнать самому. Единственный способ, который ловит всё, включая недетерминированность.
git diff --stat # что реально изменилось
git status --porcelain # что появилось незакоммиченного
find . -newermt '-30 minutes' -name '*.py' -not -path './.git/*'
pytest src/billing -q # перепрогон заявленного, своими руками
Отдельно о формулировке требования: работает не «проверь, что всё хорошо», а «приведи имя теста и
строку вывода на каждое утверждение о поведении» — правило CODE-10 из того же пакета
templates/memory/; парное CODE-11 гласит, что рассуждение о том, что тест должен пройти, не
заменяет прогона. Требование к форме отчёта меняет то, что агент делает до отчёта: цитаты надо
откуда-то взять. Полный разбор — в главе https://courses.digitable.life/post/ai-agents/10-verification/.
Права, апрув и инъекции через вывод инструмента
Вывод инструмента — это недоверенный вход. Текст из тикета, комментарий в pull request, содержимое веб-страницы, поле в чужой БД попадают в контекст и читаются моделью одинаково с вашими инструкциями: модель не различает «данные» и «команды», и то и другое — токены в одном окне.
Отсюда правило «смертельной тройки»: опасно сочетание трёх свойств в одной сессии — доступ к приватным данным, обработка недоверенного контента и возможность отправить что-то наружу. Формулировка и разбор — у Саймона Уиллисона, который ведёт хронику атак этого класса. Убрав одно свойство из трёх, вы ломаете сценарий утечки.
Минимум, который окупается сразу:
- инструменты с сетевым доступом и инструменты с доступом к секретам — не в одной сессии;
- allowlist команд терминала вместо чёрного списка: чёрный список обходится конвейером и
eval; - удалённые MCP-серверы — только с фиксированной версией и понятным владельцем; секреты — в переменные окружения сервера: аргументы вызова попадают в транскрипт и в контекст.
Развёрнуто — в главе https://courses.digitable.life/post/ai-agents/13-security/; общий разбор инъекций есть в треке ИИ-инженерии: https://courses.digitable.life/post/ai-engineering/14-safety-and-injection/.
Что стоит оборачивать в интеграцию
Логика квадрантов: часто и дорого вручную — окупается любая обёртка; редко и дорого — лучше скрипт, который человек запускает сам; часто и дёшево — терминал справится, а интеграция только съест контекст; редко и дёшево — не трогайте, это шум.
Версии, воспроизводимость и команда
Версии меняются быстро. Спецификация MCP ревизионируется, SDK ломают API между минорными версиями, наборы встроенных инструментов у Claude Code, Cursor и Codex различаются и переименовываются. Любое конкретное утверждение про транспорт, имя пакета или флаг проверяйте в текущей документации, а не в статье — включая эту.
Конфигурация инструментов — часть репозитория. Если у половины команды сервер подключён, а у другой нет, сессии невоспроизводимы: агент у коллеги «умеет» то, чего не умеет у вас, и разбор чужого отчёта превращается в гадание. Файл конфигурации в git, версии зафиксированы — как lockfile.
Инструмент — это поверхность ответственности. Всё, что агент сделал через ваш сервер, сделали вы: подключая интеграцию с правом записи, вы расширяете не возможности агента, а свою зону ответственности за ревью. Как это уживается с командными договорённостями — https://courses.digitable.life/post/ai-agents/14-team-workflow/.
Типичные ошибки
- Подключить всё «на всякий случай». Двадцать серверов ухудшают выбор и постоянно едят окно. Начинайте с нуля и добавляйте по одному, спрашивая, что сломается без него.
- Считать MCP синонимом инструментов. Function calling работает без всякого MCP; MCP — способ переиспользовать серверы между хостами. Путаница ведёт к серверам ради одного клиента.
- Писать описание для человека. Модель читает его буквально и целиком; отсутствие раздела «когда не применять» — самая частая причина неверного выбора.
- Возвращать голые коды ошибок и сырой вывод целиком. Экономия десяти байт оплачивается лишними вызовами, а неотфильтрованный лог вытесняет код, ради которого всё затевалось.
- Верить признаку «только чтение» и принимать отчёт без адресов. Первое — заявление сервера, а не гарантия системы; второе — гипотеза, как бы уверенно она ни звучала.
Мини-итог
- Модель не вызывает инструменты: она печатает предложение вызова, исполняет харнесс; всё, что между ними, — ваша инженерная ответственность.
- Инструмент для модели — это имя, описание и схема: описание работает как промпт, ошибка работает как инструкция, признак «только чтение» не работает как гарантия.
- Схемы оплачиваются на каждом шаге и занимают окно постоянно, выводы растут и первыми упираются в компакцию — измерьте у себя, прежде чем подключать очередной сервер.
- Терминал закрывает большинство интеграций, скрипт в репозитории лучше сервера, пока клиент один, а MCP решает комбинаторную задачу M×N и ничего не решает в области прав и корректности выбора.
- Заметная часть отказов живёт на границе харнесса и инструмента; проверяемость — четыре приёма: команда в отчёте, транскрипт, независимая проверка эффектов, собственный прогон.
Источники
- modelcontextprotocol.io — спецификация MCP: примитивы, транспорты, текущая ревизия. Репозитории проекта — SDK и референсные серверы. Анонс Anthropic, ноябрь 2024, — постановка задачи M×N.
- JSON-RPC 2.0 и JSON Schema — транспортный слой и язык описания аргументов.
- OpenAI, Function calling — тот же механизм у другого провайдера, полезно для сравнения формата.
- Anthropic, Building effective agents — когда агентный цикл оправдан, а когда достаточно детерминированного процесса.
- Liu et al., Lost in the Middle — деградация поиска в длинном перечне. Yao et al., ReAct — исходный цикл наблюдение-действие.
- Simon Willison, тег prompt injection — хроника атак через недоверенный вход, включая «смертельную тройку».
products/workbench/templates/memory/— наш пакет правил:REASONING-DISCIPLINE.md(RSN-01,RSN-02— маркеры и адреса для утверждений),VERIFIED-CODE.md(CODE-04— никаких угаданных API,CODE-10иCODE-11— утверждение о тесте требует прогона).
Что дальше
Инструменты дают агенту руки, но не дают ему помнить, чем всё кончилось в прошлый раз. Следующая глава — про то, что стоит записывать между сессиями, а что записывать вредно, и почему «память» у агента чаще всего оказывается обычным файлом, который кто-то должен поддерживать: Память агента: что стоит хранить, а что вредно.