Сэмплирование
MCP 2026-07-28 убрал sampling/createMessage как серверный запрос к клиенту,
управляемый возможностями, и перенёс эту способность на
MRTR
в виде вида input-запроса — сразу устаревшего, в соответствии с
12-месячным жизненным циклом самой спецификации. API на этой странице
помечены #[deprecated] и существуют для миграции; местам вызова нужен
#[allow(deprecated)].
Для нового кода предпочтительнее инструмент, предоставленный хостом, а не заимствование модели клиента.
Model Context Protocol (MCP) предоставляет стандартизированный способ для серверов запрашивать у клиентов сэмплирование LLM («завершения» или «генерации»). Такая схема позволяет клиентам сохранять контроль над доступом к моделям, их выбором и разрешениями, одновременно давая серверам возможность использовать возможности ИИ — без необходимости хранить ключи API на стороне сервера. Серверы могут запрашивать текстовые, аудио и графические взаимодействия, а также опционально включать контекст из MCP-серверов в свои запросы.
Важная концептуальная модель
- Сервер запрашивает сэмплирование
- Клиент принимает решение:
- какую модель использовать
- разрешено ли сэмплирование
- как выполнять инструменты
- Сервер никогда не владеет ключами API и никогда не обращается к LLM напрямую
neva::types реэкспортирует типы сэмплирования только в сборках с client
или legacy-spec, поэтому в обычной сборке server-full их нет в
prelude. Импортируйте их из собственного модуля — как в примерах ниже и в
апстримном examples/sampling:
use neva::types::sampling::{CreateMessageRequestParams, SamplingMessage};
Базовое использование
Для использования сэмплирования внедрите Context в обработчик инструмента и вызовите метод sample() с запросом и стабильным replay-ключом.
use neva::prelude::*;
use neva::types::sampling::CreateMessageRequestParams;
#[tool]
async fn generate_weather_report(mut ctx: Context, city: String) -> Result<String, Error> {
let params = CreateMessageRequestParams::new()
.with_message(format!("What's the weather in {city}?"))
.with_sys_prompt("You are a helpful assistant.");
// Первый раунд разворачивает обработчик с `input_required` и конвертом
// `sampling/createMessage`; второй — воспроизводит готовый ответ.
#[allow(deprecated)]
let result = ctx.sample("weather", params).await?;
Ok(format!("{:?}", result.content))
}
Поскольку сэмплирование идёт по подложке MRTR, обработчик выполняется с
самого начала на каждом раунде. Всё дорогое или заметное снаружи выше точки
сэмплирования оборачивайте в ctx.memo, ctx.once или ctx.on_commit — те
же примитивы, что и для
получения данных.
Сервер может запросить только тот вид, который объявил клиент: именно
регистрация обработчика сэмплирования заставляет клиента выставить
clientCapabilities.sampling. Запрос к клиенту, который его не объявил,
приводит к ошибке, а не к подвисанию раунда.
Если в вашем MCP-сервере уже объявлен подходящий шаблон промпта, можно использовать метод prompt() объекта Context вместо передачи форматированной строки.
Настройка запроса на создание сообщения
Структура CreateMessageRequestParams предоставляет методы для настройки:
- Температуры
- Системного запроса
- Лимитов токенов
- Предпочтений модели
use neva::prelude::*;
use neva::types::sampling::{CreateMessageRequestParams, ModelPreferences};
let model_pref = ModelPreferences::new()
.with_hints(["claude-4.5-sonnet", "gpt-5"])
.with_cost_priority(0.3)
.with_speed_priority(0.8)
.with_intel_priority(0.5);
let params = CreateMessageRequestParams::new()
.with_message(format!("What's the weather in {city}?"))
.with_sys_prompt("You are a helpful assistant.")
.with_max_tokens(1000)
.with_temp(0.2)
.with_pref(model_pref);
Предпочтения модели являются подсказками, а не гарантиями.
Клиент может:
- Проигнорировать их
- Сопоставить с другой моделью
- Применить дополнительные политики
Использование инструментов
Если клиент поддерживает возможность sampling.tools, сервер может передать список инструментов для использования LLM в процессе сэмплирования. Для этого служит метод with_tools():
Инструменты всегда выполняются сервером, а не клиентом или моделью.
use neva::prelude::*;
use neva::types::sampling::CreateMessageRequestParams;
let Some(tool) = ctx.find_tool("get_weather").await else {
return Err(ErrorCode::MethodNotFound.into());
};
let params = CreateMessageRequestParams::new()
.with_message(format!("What's the weather in {city}?"))
.with_sys_prompt("You are a helpful assistant.")
.with_tools([tool]);
У Context также есть методы tools(), find_tool() и find_tools(), которые могут быть полезны для получения метаданных инструментов для клиента.
Настройка выбора инструмента
По умолчанию метод with_tools() устанавливает toolChoice для LLM как auto. Это значение можно изменить с помощью метода with_tool_choice().
use neva::prelude::*;
use neva::types::sampling::{CreateMessageRequestParams, ToolChoiceMode};
let Some(tool) = ctx.find_tool("get_weather").await else {
return Err(ErrorCode::MethodNotFound.into());
};
let params = CreateMessageRequestParams::new()
.with_message(format!("What's the weather in {city}?"))
.with_sys_prompt("You are a helpful assistant.")
.with_tools([tool])
.with_tool_choice(ToolChoiceMode::Required);
Значения ToolChoiceMode:
Auto— модель сама решает, вызывать ли инструменты (по умолчанию).Required— модель обязана вызвать хотя бы один инструмент.None— модель не должна вызывать инструменты.
Цикл сэмплирования
Ниже приведена эталонная реализация цикла сэмплирования с выполнением инструментов. Большинство реальных MCP-серверов следуют этому паттерну.
Метод sample() возвращает CreateMessageResult. Необходимо проверять поле stop_reason и продолжать сэмплирование до достижения конечного состояния.
use neva::prelude::*;
use neva::types::sampling::{CreateMessageRequestParams, SamplingMessage, StopReason, ToolChoiceMode};
#[tool]
async fn generate_weather_report(mut ctx: Context, city: String) -> Result<String, Error> {
let Some(tool) = ctx.find_tool("get_weather").await else {
return Err(ErrorCode::MethodNotFound.into());
};
let mut params = CreateMessageRequestParams::new()
.with_message(format!("What's the weather in {city}?"))
.with_sys_prompt("You are a helpful assistant.")
.with_tools([tool]);
let mut step = 0;
loop {
// Каждой итерации нужен **свой** replay-ключ — общий ключ вечно
// воспроизводил бы первый ответ вместо продвижения цикла.
#[allow(deprecated)]
let result = ctx.sample(format!("sample-{step}"), params.clone()).await?;
if result.stop_reason == Some(StopReason::ToolUse) {
// Получаем запросы на вызов инструментов из ответа сэмплирования
let tools: Vec<ToolUse> = result.tools()
.cloned()
.collect();
// Записываем их как сообщения ассистента в «контекст»
let assistant_msg = tools
.iter()
.fold(SamplingMessage::assistant(), |msg, tool| msg.with(tool.clone()));
// Вызываем инструменты, запрошенные LLM, — через `memo`, чтобы
// они выполнились один раз. Без этого инструменты каждого
// предыдущего шага выполнялись бы заново на каждом следующем
// раунде, ведь обработчик стартует с начала.
let tool_results = ctx
.memo(format!("tools-{step}"), async { Ok(ctx.use_tools(tools).await) })
.await?;
// Записываем результаты инструментов как сообщения пользователя
let user_msg = tool_results
.into_iter()
.fold(SamplingMessage::user(), |msg, result| msg.with(result));
// Формируем параметры для следующего шага с предыдущим контекстом и результатами инструментов
params = params
.with_message(assistant_msg)
.with_message(user_msg)
.with_tool_choice(ToolChoiceMode::None);
step += 1;
} else {
// Останавливаемся при получении причины, отличной от вызова инструмента
return Ok(format!("{:?}", result.content));
};
}
}
Каждый шаг сэмплирования должен включать:
- Сообщения ассистента с вызовами инструментов
- Сообщения пользователя с результатами инструментов
Это соответствует обучающим данным LLM и позволяет клиенту восстановить полный контекст.
В продакшен-коде всегда следует:
- Ограничивать количество итераций сэмплирования
- Обрабатывать неожиданные причины остановки
Каждая итерация — это полноценный раунд MRTR, то есть ещё один повтор
запроса со стороны клиента. Клиенты ограничивают их числом
McpOptions::with_max_mrtr_rounds.
И ключ на каждую итерацию нужен не только вызову sample, но и любому
побочному эффекту в теле цикла: обработчик стартует с начала на каждом
раунде, поэтому незащищённый ctx.use_tools заново выполнял бы инструменты
всех предыдущих шагов.
Когда не использовать сэмплирование
Избегайте сэмплирования, если:
- Задача детерминирована
- Рассуждение на естественном языке не требуется
- Достаточно обычного вызова инструмента или функции
Обучение на примерах
Полный рабочий пример доступен здесь.