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

Встроенный ассистент

Эта страница переведена с английского. Читать страницу на английском

Статус: реализовано за 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 КБ больше).