Перейти к основному содержимому

Сэмплирование

Устарело с момента появления

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 и позволяет клиенту восстановить полный контекст.

warning

В продакшен-коде всегда следует:

  • Ограничивать количество итераций сэмплирования
  • Обрабатывать неожиданные причины остановки

Каждая итерация — это полноценный раунд MRTR, то есть ещё один повтор запроса со стороны клиента. Клиенты ограничивают их числом McpOptions::with_max_mrtr_rounds.

И ключ на каждую итерацию нужен не только вызову sample, но и любому побочному эффекту в теле цикла: обработчик стартует с начала на каждом раунде, поэтому незащищённый ctx.use_tools заново выполнял бы инструменты всех предыдущих шагов.

Когда не использовать сэмплирование

Избегайте сэмплирования, если:

  • Задача детерминирована
  • Рассуждение на естественном языке не требуется
  • Достаточно обычного вызова инструмента или функции

Обучение на примерах

Полный рабочий пример доступен здесь.