Эта страница переведена с английского. Читать страницу на английском
Статус: реализовано за Cargo feature assistant крейта zeyn.
Приложение Zeyn может нести собственного ИИ-ассистента: чат на каждой странице, который отвечает данными приложения и работает с экранами и API приложения от имени вошедшего пользователя. Приложения, построенные на Zeyn, могут включить его для своих клиентов, подключить модель, которую те хотят, и хранить историю чата там, где нужно.
zeyn = { version = "0.0.2", features = ["assistant"] }Без feature ничего из этого не компилируется: ни HTTP-клиента, ни дополнительных маршрутов, ни дополнительной памяти.
1. Настройка
use zeyn::assistant::{Assistant, OpenAiCompatible};
let model = OpenAiCompatible::new(config.require("assistant.base_url")?, config.require("assistant.model")?)
.api_key(config.secret("assistant.api_key")?);
App::new("Lifts")
.assistant(
Assistant::new(model)
.permission("lifts.assistant.use")
.instructions("You help service engineers plan elevator maintenance.")
.suggest(work_orders, "Which lifts are overdue for service?")
.suggest_anywhere("What can you do?"),
)Это всё. Каждая страница получает плавающую кнопку, которая открывает чат боковой панелью. Экран может и сам показывать чат:
#[screen(route = "/assistant", permission = "lifts.assistant.use", ai = false)]
pub fn assistant_page() -> Screen {
Screen::new("Assistant").body(AssistantChat::page())
}AssistantChat::panel() помещается рядом с другим содержимым. Assistant::launcher(false) убирает плавающую кнопку, так что чат появляется только там, где его разместил экран.
| Параметр | По умолчанию |
|---|---|
permission("...") | каждый вошедший пользователь |
instructions("...") | нет; пишутся разработчиками приложения, никогда пользователями |
suggest(screen, prompt), suggest_anywhere(prompt) | нет стартовых подсказок |
speech_to_text(p), text_to_speech(p) | Web Speech API браузера |
no_voice() | микрофон и озвучивание включены |
store(s) | MemoryChatStore::default() |
read_only() | ассистент может также сохранять, после подтверждения |
rate_limit(RateLimit) | 20 запросов в минуту на пользователя |
token_limit(Some(RateLimit)) | 200 000 токенов в час на пользователя |
limits(Limits { .. }) | см. раздел 6 |
2. Модели
Работает любой тип, реализующий LlmProvider: один метод, который запускает потоковый раунд и возвращает ограниченный канал ChatEvent (фрагменты текста, вызовы инструментов, использование). Встроенные:
| Провайдер | Охватывает |
|---|---|
OpenAiCompatible::new(base_url, model) | OpenAI (https://api.openai.com/v1), Azure OpenAI v1 API (https://NAME.openai.azure.com/openai/v1, развёртывание как модель, .azure() для заголовка api-key), Ollama (http://localhost:11434/v1), vLLM, LiteLLM и другие совместимые серверы. .legacy_max_tokens() для серверов, не знающих max_completion_tokens |
Anthropic::new(model) | Anthropic Messages API (.base_url(...) для шлюза). Блоки рассуждений отправляются обратно без изменений в пределах хода |
ScriptedProvider | Тесты, демо и офлайн-разработка: правила вместо сетевого вызова, потоком слово за словом, настоящие вызовы инструментов |
Ключи берутся из значений Secret (Config::secret), остаются на сервере и никогда не попадают в сообщения об ошибках. Ошибки провайдера логируются на сервере; пользователь видит общее сообщение.
Чтобы направить showcase на настоящую модель, достаточно конфигурации:
NORTHWIND_ASSISTANT__PROVIDER=openai
NORTHWIND_ASSISTANT__BASE_URL=https://api.openai.com/v1
NORTHWIND_ASSISTANT__MODEL=gpt-4.1-mini
NORTHWIND_ASSISTANT__API_KEY=...или NORTHWIND_ASSISTANT__PROVIDER=anthropic с NORTHWIND_ASSISTANT__MODEL=claude-opus-5-5, или URL Ollama без ключа. NORTHWIND_ASSISTANT__PROVIDER=off выключает ассистента.
3. Речь
По умолчанию микрофон использует распознавание речи браузера, а «озвучивание» синтез речи браузера: никаких затрат сервера, и никакой звук не уходит из браузера к приложению. Для серверной речи:
Assistant::new(model)
.speech_to_text(OpenAiTranscription::new(base, "whisper-1").api_key(key.clone()))
.text_to_speech(OpenAiSpeech::new(base, "tts-1", "alloy").api_key(key))Оба работают с OpenAI-совместимыми конечными точками /audio/transcriptions и /audio/speech, которые предлагают и локальные серверы Whisper и речи. Записи ограничены (4 МБ, 60 секунд в браузере); звук речи передаётся потоком в браузер по мере поступления.
4. История
ChatStore хранит разговоры по ключу арендатора и пользователя (ChatKey), поэтому арендаторы никогда не делят один разговор:
pub trait ChatStore: Send + Sync + 'static {
fn load(&self, key: &ChatKey, limit: usize) -> BoxFuture<Result<Vec<ChatMessage>>>;
fn append(&self, key: &ChatKey, messages: Vec<ChatMessage>) -> BoxFuture<Result<()>>;
fn clear(&self, key: &ChatKey) -> BoxFuture<Result<()>>;
}MemoryChatStore::new(max_users, max_messages) используется по умолчанию (1000 разговоров по 60 сообщений, сначала вытесняются наименее недавно использованные, теряются при перезапуске). Для базы данных реализуйте три метода поверх таблицы с арендатором, пользователем, позицией и сообщением в виде JSON. Сообщения только добавляются; срок хранения определяет хранилище. Ожидающие подтверждения тоже лежат в истории, поэтому сервер не хранит никакого другого состояния на пользователя.
5. Инструменты и действия
Модель видит те же инструменты, что MCP и WebMCP (см. docs/runtime-protocol.md, раздел 9), построенные каждый раз для вошедшего пользователя: поиски по сеткам, сохранения форм и действия API, которые пользователю разрешены, за вычетом всего, что помечено ai = false, с описаниями из doc-комментариев /// и указаниями ai = "...". Добавляются два встроенных инструмента:
navigate: открывает маршрутизируемый экран, который пользователю можно открыть, без загрузки страницы (zeyn:navigate), и может отфильтровать его сетку (zeyn:filter). Затем сетка загружается черезPOST /_zeyn/grid, где сервер применяет свои обычные проверки.suggest: следующие подсказки, показываемые как чипы.
Каждый вызов проходит по обычному пути действия с контекстом, помеченным как агент: доступ экрана, guards feature, permission действия, interceptors, ограничения частоты, лимит времени, аудит (каждый вызов ассистента записывается с via_agent: true) и маскирование.
Подтверждение. Чтения выполняются сразу. Всё, что записывает (сохранения форм, действия API, вход которых не DataQuery), и каждое действие, помеченное #[action(confirm)], останавливает раунд и показывает карточку подтверждения с тем, что будет выполнено, и его аргументами. Выполняет его, и только один раз, лишь щелчок пользователя (POST /_zeyn/assistant/confirm, с CSRF, для вызова, ожидающего в собственной истории этого пользователя). Сообщение вроде «Подтверждаю» это просто текст: новое сообщение закрывает ожидающий вызов как невыполненный.
6. Модель безопасности
| Риск | Что делает фреймворк |
|---|---|
| Утечка ключей API | Ключи это Secret на сервере; браузер общается только с приложением |
| Модель действует за пределами прав пользователя | Инструменты строятся из прав пользователя; каждый вызов заново проверяется на сервере |
| Записи, которых пользователь не хотел | Записи и действия confirm требуют щелчка пользователя; модель подтвердить не может |
| Замаскированные данные попадают к модели | Замаскированные поля всегда замаскированы для вызовов агента, даже для пользователей, которым разрешено их раскрывать |
| Prompt injection через данные | Результаты инструментов это простой текст внутри разделителей <tool_result> с экранированными < и >; системный промпт сообщает, что это данные. Системный промпт пересобирается сервером каждый раунд из фиксированных правил, инструкций приложения и названия экрана, никогда не из данных пользователя, страницы или инструмента |
| Утечка данных через ссылки | Ответы отображаются как небольшое подмножество Markdown, построенное через textContent: без HTML, без ссылок, без изображений |
| Неконтролируемые расходы | Ограничение частоты запросов на пользователя, лимит токенов на пользователя, ограниченные выходные токены, не более 6 раундов и 4 вызовов инструментов на раунд |
| Большие входные данные | Сообщения до 2000 символов, 30 сообщений истории на раунд, результаты инструментов обрезаются до 6000 символов, записи до 4 МБ |
| Межсайтовые запросы | Каждый POST требует заголовок CSRF и учитывается в лимите записи приложения |
| Действия без ответственности | Каждый вызов инструмента ассистента аудируется с via_agent |
Чего он не делает: не решает, верен ли ответ модели. Сообщайте об этом пользователям (панель это делает) и держите важные действия за confirm.
7. Встраивание для клиентов
Приложению, которое продаёт ассистента своим клиентам, обычно нужно:
permission("app.assistant.use")и проверка модуля лицензии в guard, чтобы его видели только платящие арендаторы;- модель на каждого арендатора: реализуйте
LlmProviderкак небольшой маршрутизатор, выбирающий провайдера клиента из конфигурации по арендатору изctx, или запустите одну модель для всех; ChatStoreна базе данных приложения, с правилами хранения клиента;instructions(...), описывающие предметную область (лифтовая компания: агрегаты, объекты, интервалы обслуживания), и указанияai = "..."у действий, которым они нужны;token_limit, выставленный по тарифу, который платит клиент.
8. Конечные точки
| Конечная точка | Что делает |
|---|---|
GET /_zeyn/assistant/start?screen= | { messages, pending, suggestions, stt, tts, max } |
POST /_zeyn/assistant/chat { message, screen } | Передаёт потоком события application/x-ndjson |
POST /_zeyn/assistant/confirm { id, approve } | Выполняет или отклоняет ожидающий вызов, затем передаёт поток как чат |
POST /_zeyn/assistant/clear | Удаляет разговор; 204 |
POST /_zeyn/assistant/stt (тело с аудио) | { text }; 404 без серверного преобразования речи в текст |
POST /_zeyn/assistant/tts { text } | Аудио потоком; 404 без серверного преобразования текста в речь |
События, по одному объекту JSON на строку: {t:"text",v}, {t:"tool",name,id}, {t:"navigate",url,screen,filters}, {t:"suggest",v}, {t:"confirm",id,tool,title,question,details}, {t:"resolved",id,approved}, {t:"error",message}, {t:"done"}.
assistant.js и assistant.css загружаются только для пользователей, которым разрешено пользоваться ассистентом. Журнал чата это live region role="log" (aria-busy, пока идёт потоковый ответ), Enter отправляет, Shift+Enter начинает новую строку, Escape закрывает боковую панель и возвращает фокус на кнопку, а карточка подтверждения сначала ставит фокус на «Отмена».
9. Стоимость
Ассистент не хранит в памяти состояния на пользователя, кроме ограниченного хранилища истории и потоков в работе. Ответы передаются потоком от начала до конца через каналы по 32 элемента. Измерено на showcase (отладочная сборка, Windows, простой после пяти загрузок страниц): 14.4 МБ working set и 3.13 МБ private при выключенном ассистенте, 14.5 МБ и 3.19 МБ при включённом (примерно на 100 КБ больше).