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

Промпты

Model Context Protocol (MCP) предоставляет стандартизированный способ для серверов предоставлять клиентам шаблоны промптов. Промпты позволяют серверам передавать структурированные сообщения и инструкции для взаимодействия с языковыми моделями. Клиенты могут получать список доступных промптов, извлекать их содержимое и передавать аргументы для их настройки.

В главе Основы мы научились объявлять простой промпт:

#[prompt(descr = "Generates a user message requesting a hello world code generation.")]
async fn hello_world_code(lang: String) -> PromptMessage {
PromptMessage::user()
.with(format!("Write a hello-world function on {lang}"))
}

Того же результата можно добиться без использования процедурного макроса:

use neva::prelude::*;

async fn hello_world_code(lang: String) -> PromptMessage {
PromptMessage::user()
.with(format!("Write a hello-world function on {lang}"))
}

#[tokio::main]
async fn main() {
let mut mcp_server = App::new()
.with_options(|opt| opt
.with_stdio()
.with_name("Sample MCP Server")
.with_version("1.0.0"));

mcp_server
.map_prompt("hello_world_code", hello_world_code)
.with_description("Generates a user message requesting a hello world code generation.");

mcp_server.run().await;
}

В примере выше имя промпта должно быть задано явно. При использовании атрибутного макроса #[prompt] имя промпта автоматически выводится из имени функции.

Все остальные параметры промпта, доступные в атрибутном макросе, можно настроить с помощью методов with_* (например, with_description()).

Метод map_prompt() регистрирует обработчик промпта под указанным именем и возвращает изменяемую ссылку на зарегистрированный промпт.

Входные аргументы

Для промпта можно явно задать входные аргументы. Если аргументы не указаны, Neva автоматически генерирует их на основе сигнатуры функции-обработчика.

Для переопределения сгенерированной схемы укажите её в виде JSON-строки:

#[prompt(
descr = "Generates a user message requesting a hello world code generation.",
args = r#"[
{
"name": "lang",
"description": "A language to use",
"required": true
}
]"#
)]
async fn hello_world_code(lang: String) -> PromptMessage {
PromptMessage::user()
.with(format!("Write a hello-world function on {lang}"))
}

Необязательные аргументы

Аргумент, объявленный как Option<T>, публикуется с "required": false, а prompts/get, который его не передал, отдаёт обработчику None:

#[prompt(descr = "Generates a user message requesting a hello world code generation.")]
async fn hello_world_code(lang: String, tone: Option<String>) -> PromptMessage {
let tone = tone.unwrap_or_else(|| "neutral".into());
PromptMessage::user()
.with(format!("Write a hello-world function on {lang}, tone: {tone}"))
}

Если вы собираете список аргументов вручную, PromptArgument::named(name, required) — это форма без описания; PromptArgument::required и PromptArgument::optional — то же самое с описанием.

Имена аргументов

Аргументы промпта читаются из prompts/get по имени, поэтому имена, по которым читает обработчик, обязаны совпадать с теми, что публикует prompts/list.

#[prompt] берёт имена параметров самой функции. У «голого» замыкания их нет — Rust их не сохраняет, — и оно откатывается к позиционным arg0, arg1, … Макрос map_prompt! считывает их с замыкания:

use neva::{App, map_prompt, types::Role};

#[tokio::main]
async fn main() {
let mut app = App::new();

map_prompt!(app, "analyze", |lang: String, code: String| async move {
(format!("Analyze this {lang} code: {code}"), Role::User)
})
.with_description("Analyzes a code snippet");

app.run().await;
}

Prompt::with_args() — явная форма: она задаёт публикуемые аргументы и имена для извлечения одним вызовом, так что разойтись они не могут.

Промпт, публикующий аргументы, которые его обработчик не читает, роняет App::run при старте — см. Инструменты → Проверка при старте: к промптам применяется то же правило.

MCP-контекст

В более сложных сценариях — например, когда промпту нужен доступ к ресурсам, объявленным на том же MCP-сервере, — можно внедрить Context в обработчик промпта:

#[prompt(descr = "Generates a user message requesting a translate a text using the glossary.")]
async fn translate_with_glossary(ctx: Context, text: String) -> PromptMessage {
let glossary = ctx.resource("res://glossary").await?;
let resource = result.contents
.into_iter()
.next()
.expect("No resource contents");

PromptMessage::user()
.with(format!("Translate using this glossary:\n{glossary}\n\nText: {text}"))
}

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

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