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

LLM Query — AI-запросы к LLM внутри сценариев

Пакет: AI
Полное имя компонента: Common.AI.LLMQuery

Что это

LLM Query — это высокоуровневый AI-компонент Metabot для выполнения запросов к языковым моделям прямо внутри сценария.

Он позволяет встроить запрос к искусственному интеллекту в сценарий так, чтобы для сценариста это выглядело как обычный шаг логики, а не как отдельная внешняя интеграция.

Проще всего воспринимать его так:

LLM Query — это AI Query-компонент.
Почти как SQL-запрос, только не к базе данных, а к языковой модели.

Ты:

  • собираешь контекст,

  • формулируешь задачу,

  • задаёшь формат ответа,

  • получаешь результат,

  • работаешь с ним дальше в сценарии как с обычными данными Metabot.


Зачем нужен LLM Query

Обычный сценарий хорошо работает, пока пользователь отвечает так, как ожидает логика кнопок, меню и веток.

Но в жизни пользователь пишет свободно.

Например, сценарий ожидает:

Выберите тип проблемы

А пользователь пишет:

Соседи сверху топают, слышу шаги и телевизор через потолок

Для дерева условий это неудобный вход.
Для LLM Query — нормальная задача на семантический анализ.

Компонент нужен, когда необходимо:

  • понять свободный текст;

  • извлечь параметры из сообщения;

  • классифицировать намерение;

  • сгенерировать ответ в заданной рамке;

  • вернуть не просто текст, а структурированный JSON;

  • встроить AI в существующий сценарий без разрушения его логики.


Где используется

LLM Query подходит для:

  • анализа входящих сообщений;

  • определения intent;

  • сегментации и профилирования;

  • интерпретации ответов квиза;

  • извлечения JSON-структуры из текста;

  • генерации отражений, summaries и рекомендаций;

  • работы после Voice Input;

  • RAG-сценариев после поиска по базе знаний.


Где находится компонент

Компонент находится в пакете AI и подключается как обычный плагин Metabot:

const LLMQuery = require("Common.AI.LLMQuery")

Как работает LLM Query

LLM Query — это двухфазный асинхронный компонент.

Это ключевая особенность.

Он не выполняется как обычная команда JavaScript “сразу и до конца”, потому что под капотом делает внешний запрос к LLM-провайдеру и ждёт callback.

Фаза 1. Отправка запроса

На первом проходе компонент:

  • собирает промпты;

  • настраивает провайдера и модель;

  • формирует запрос;

  • при необходимости показывает wait-сообщение;

  • передаёт запрос в LLM Client.

Фаза 2. Обработка callback

Когда ответ возвращается обратно в Metabot:

  • компонент понимает, что это async-callback;

  • получает сырой ответ модели;

  • при необходимости парсит JSON;

  • сохраняет raw и parsed результат;

  • либо передаёт управление дальше в сценарий, либо запускает successScript.


Обязательное условие использования

LLM Query нужно вызывать только внутри команды:

Run asynchronous API-request
(Запустить асинхронный API-запрос)

Это обязательно, потому что компонент использует isFirstImmediateCall, чтобы различать:

  • первый запуск;

  • callback с готовым ответом.

Если вызывать его не в этой команде, двухфазная модель работы нарушится.


Как устроен пайплайн под капотом

Для сценариста LLM Query выглядит просто.
Но внутри он работает через несколько уровней инфраструктуры:

LLM Query
↓
LLM Client
↓
RemoteApiCall
↓
Webhook Processor
↓
LLM Provider

И обратно:

LLM Provider
↓
Webhook Processor
↓
Metabot callback
↓
LLM Client фаза 2
↓
LLM Query фаза 2
↓
сохранить результат
↓
следующий шаг сценария

Что это даёт

Такая архитектура позволяет:

  • отделить сценарный слой от transport layer;

  • менять провайдеров;

  • использовать прокси;

  • централизованно обрабатывать callback;

  • управлять timeout и fallback;

  • делать трассировку и диагностику.


Что нужно настроить, чтобы LLM Query заработал

Перед использованием компонента нужно настроить инфраструктуру бота.

1. Ключ провайдера LLM

Нужно указать токен доступа к LLM в атрибутах бота. Например:

  • OPENAI_API_KEY

  • или YANDEX_API_KEY

Этот ключ используется транспортным слоем при обращении к внешней модели.

2. Токен API-пользователя Metabot

Нужно создать API-пользователя в бизнесе Metabot с правами editor и сохранить его токен в атрибуте бота:

  • METABOT_API_TOKEN

Этот токен нужен для того, чтобы внешний webhook processor мог вернуть callback обратно в Metabot.

3. Домен Metabot

Нужно указать домен инстанса Metabot:

  • METABOT_SERVER_DOMAIN

Например:

https://app.metabot24.com

Он используется при формировании callback URL, на который внешний процессор возвращает ответ.


Минимально необходимые атрибуты бота

Атрибут Назначение
METABOT_API_TOKEN Токен API-пользователя Metabot для async callback
METABOT_SERVER_DOMAIN Домен Metabot, куда возвращается callback
OPENAI_API_KEY Ключ OpenAI, если используется OpenAI
YANDEX_API_KEY Ключ Яндекса, если используется Yandex

Свяжитесь с поддержкой, если нужна интеграция с другой LLM.


Что важно понимать про режим ответа

Сейчас LLM Query работает в режиме полного асинхронного ответа.

Это значит:

  • запрос отправляется;

  • модель формирует ответ;

  • ответ возвращается целиком;

  • после этого сценарий продолжает работу.

Что пока не поддерживается

Потоковый режим (streaming), когда ответ показывается пользователю постепенно, как в интерфейсах ChatGPT или других AI-клиентов.

Поэтому на практике рекомендуется:

  • отправлять пользователю wait-сообщение;

  • при необходимости показывать картинку или другой интересный материал, чтобы занять время;

  • учитывать задержку 10–15 секунд как нормальный UX-кейс.

Пример хорошего wait-сообщения:

⌛ Готовлю ответ… (до 15 секунд)

Сигнатура вызова

Базовый вызов компонента:

const LLMQuery = require("Common.AI.LLMQuery")

return LLMQuery.run({
  lead,
  isFirstImmediateCall,

  code: "ExampleQuery",
  agentName: "default",

  provider: "OpenAI",
  model: "gpt-5-mini",
  modelParams: { temperature: 1 },

  prompts: {
    system: [],
    user: "",
    last: ""
  },

  messages: {
    wait: "⌛ Готовлю ответ…",
    processing: "⏳ Ответ ещё формируется…",
    error: "⚠️ Ответ получен, но формат повреждён"
  },

  save: {
    raw: "example_raw",
    parsed: "example_json"
  }
})

Параметры компонента

Ниже — параметры LLMQuery.run().

Параметр Тип Обязателен Описание
lead object Да Объект лида
isFirstImmediateCall boolean Да Флаг первой/второй фазы выполнения
code string Нет Внутренний код запроса / сессии
provider string Да Провайдер LLM, например OpenAI
model string Да Имя модели
modelParams object Нет Параметры модели
promptTable string Нет Имя таблицы промптов
agentName string Нет Имя агента для работы с prompt registry
prompts object Да Блок промптов (system, user, last)
timeout object Нет Настройки таймаута
error object Нет Настройки обработки ошибок
save object Да Куда сохранять raw и parsed результат
successScript string Нет Скрипт, который вызвать после успешного выполнения
messages object Нет UX-сообщения во время выполнения

Объект prompts

Поле Тип Описание
system array Системные промпты
user string Основной пользовательский prompt
last string Финальный prompt после user

Пример

prompts: {
  system: ["$identity", "$reflect_quiz"],
  user: `intent=${lead.getAttr("corp_entry_intent")}`,
  last: ``
}

Важно

Промпт может быть:

  • одним;

  • двумя;

  • тремя блоками;

  • вообще написан прямо в коде сценария.

Использовать prompt registry не обязательно.
Если тебе удобнее, можно писать промпты прямо внутри сценария.

Например:

const LLMQuery = require("Common.AI.LLMQuery")

return LLMQuery.run({
  lead,
  isFirstImmediateCall,

  provider: "OpenAI",
  model: "gpt-5-mini",

  prompts: {
    user: `
Пользователь написал сообщение:

"${lead.getAttr("last_message")}"

Определи намерение пользователя.

Возможные категории:
1. консультация
2. подбор_материала
3. стоимость
4. другое

Ответ верни только одним словом из списка выше.
`
  },

  save: {
    raw: "intent_raw"
  }
})

Объект timeout

Поле Тип Описание
seconds number Через сколько секунд считать запрос зависшим
script string Скрипт, в который перейти при timeout

Пример

timeout: {
  seconds: 180,
  script: "LLMQuery_TimeOut"
}

Что это значит

Если callback не пришёл за указанное время, сценарий должен уйти в fallback-ветку.


Объект error

Поле Тип Описание
script string Скрипт обработки ошибки
flagAttr string Атрибут-флаг ошибки
reasonAttr string Атрибут с причиной ошибки

Пример

error: {
  script: "LLM_Error_Handler",
  flagAttr: "llm_error",
  reasonAttr: "llm_error_reason"
}

Что это значит

Если:

  • вызов LLM завершился ошибкой;

  • ответ не удалось распарсить как JSON;

  • нарушен контракт ответа,

то компонент:

  • выставит флаг ошибки;

  • запишет причину;

  • добавит информацию об ошибке в атрибуты лида в flagAttr и reasonAttr; 
  • при необходимости переведёт сценарий в error script.


Объект save

Поле Тип Описание
raw string Атрибут для сырого ответа модели
parsed string JSON-атрибут для parsed результата

Пример

save: {
  raw: "corp_entry_llm_raw",
  parsed: "corp_entry_llm_json"
}

Важно

Если указан save.parsed, JSON parsing включается автоматически. 

При невозможности корректного парсинга выбрасывается исключение, и если настроен errorScript / errorFallback, выполнение передается в этот обработчик ошибки.


Объект messages

Поле Тип Описание
wait string Сообщение при старте запроса
processing string Сообщение, если пользователь пишет во время ожидания
error string Сообщение при проблеме с форматом ответа

Пример

messages: {
  wait: "⌛ Готовлю отражение… (до 15 секунд)",
  processing: "⏳ Ожидайте, ответ ещё формируется…",
  error: "Ответ получен, но формат повреждён"
}

Пример сценария: анализ текстового квиза

Чтобы лучше понять, как работает LLM Query, рассмотрим не абстрактный вызов, а живой сценарий.

В этом примере пользователь приходит в экосистему, а сценарий решает сразу две задачи:

  1. собрать базовый контекст о человеке

  2. с помощью AI превратить этот контекст в полезное отражение

То есть мы не просто квалифицируем пользователя “для себя”.
Мы тут же создаём ценность для него: даём ему понятную интерпретацию его текущей позиции.

Это хороший паттерн использования LLM Query:

  • сценарий собирает входные данные;

  • AI делает интеллектуальную обработку;

  • результат возвращается обратно в сценарий;

  • сценарий показывает его пользователю.


Задача сценария

Представим, что человек впервые попадает в экосистему Orion.

Мы хотим быстро понять:

  • зачем он пришёл;

  • в какой жизненной форме он сейчас находится;

  • за что он отвечает;

  • какую роль, траекторию или напряжение можно увидеть уже на входе.

Для этого мы делаем короткий текстовый сценарий из нескольких шагов.


Как проходит сценарий

Шаг 1. Вход в воронку

При входе в сценарий фиксируется flow-контекст:

const FlowContext = require("Common.Platform.FlowContext")
FlowContext.set(lead, "corp_entry_flow")

Это нужно для того, чтобы дальше маршруты и guard-компоненты понимали, в каком контуре сейчас находится пользователь.

Подробнее про FlowContext смотрите в описании компонентов VoiceInput и VoiceRouteGuard.


Шаг 2. Первый вопрос: зачем ты здесь?

Сценарий задаёт вопрос:

❓ Что сейчас для тебя главное?

И предлагает варианты ответа:

  • 🔍 Разобраться, куда двигаться дальше

  • 🧠 Усилить себя в текущей работе / профессии

  • 🛠 Делать проекты, практиковаться

  • 🧩 Собрать команду / систему

  • 👀 Просто посмотреть и понять, что это

После выбора сохраняется атрибут, например:

lead.setAttr('corp_entry_intent', 'orientation')

Возможные значения:

  • orientation

  • self_upgrade

  • build_projects/practice

  • find_team/build_system

  • observe


Шаг 3. Второй вопрос: где ты сейчас?

Сценарий задаёт вопрос:

❓ В какой форме ты сейчас существуешь во внешнем мире?

Варианты:

  • 🎓 Учусь / вхожу в профессию

  • 🧑‍💻 Работаю по найму

  • 🚀 Делаю проекты / фриланс / стартап

  • 🧱 Владею бизнесом / отвечаю за команду

  • 🤷 Сложно сказать / переходное состояние

После выбора сохраняется, например:

lead.setAttr('corp_entry_life_form', 'employee/hired')

Возможные значения:

  • early_career/student

  • employee/hired

  • independent/builder

  • owner/manager

  • transition


Шаг 4. Третий вопрос: за что ты отвечаешь?

Сценарий задаёт вопрос:

❓ Ты сейчас отвечаешь только за себя
или уже за других людей / системы?

Варианты:

  • 🧍 Только за себя

  • 👥 За команду / проект

  • ⚖ За бизнес / деньги / договоры

  • ❓ Пока не понимаю

После выбора сохраняется, например:

lead.setAttr('corp_entry_responsibility_level', 'self')

Возможные значения:

  • self

  • team

  • business

  • unclear


Что мы получаем к этому моменту

После трёх простых шагов у сценария уже есть базовый входной профиль пользователя:

  • corp_entry_intent

  • corp_entry_life_form

  • corp_entry_responsibility_level

На этом этапе обычный сценарий мог бы просто повести пользователя по готовой ветке.

Но здесь мы делаем следующий шаг:
используем LLM Query, чтобы:

  • осмыслить комбинацию этих параметров;

  • извлечь из них структуру;

  • сформировать полезное отражение;

  • вернуть всё обратно в сценарий в JSON.


Вызов LLM Query в сценарии

После того как данные собраны, сценарий переходит в шаг «Анализ квиза».

Сначала пользователю показывается промежуточное сообщение, например:

  • Контекст получен 👌

  • картинка

  • сообщение ожидания

После этого в команде Run asynchronous API-request вызывается LLM Query.

Пример вызова:

const LLMQuery = require("Common.AI.LLMQuery")

return LLMQuery.run({
  lead,
  isFirstImmediateCall,

  code: "CorpEntryReflection",
  agentName: "orion",

  provider: "OpenAI",
  model: "gpt-5-mini",
  modelParams: { temperature: 1 },
  
  timeout: {
    seconds: 180,
    script: "LLMQuery_TimeOut"
  },

  error: {
    script: "LLMQuery_Error",
    flagAttr: "llm_error",
    reasonAttr: "llm_error_reason"
  },  

  prompts: {
    system: ["$identity", "$reflect_quiz"],
    user: `Input/Entry data:
        intent=${lead.getAttr("corp_entry_intent")}
        life_form=${lead.getAttr("corp_entry_life_form")}
        responsibility=${lead.getAttr("corp_entry_responsibility_level")}`,
    last: ``
  },
  
  messages: {
    wait: "⌛ Готовлю отражение… (до 15 секунд)",
    processing: "⏳ Ожидайте, ответ ещё формируется…",
    error: "Ответ получен, но формат повреждён"
  },  

  save: {
    raw: "corp_entry_llm_raw",
    parsed: "corp_entry_llm_json"
  },

  successScript: null
})

Что здесь происходит

Разберём по шагам.

1. Сценарий передаёт в AI уже собранные параметры

В user prompt попадают три значения:

  • intent

  • life_form

  • responsibility

То есть мы не заставляем модель угадывать всё из свободного текста.
Мы сначала собираем аккуратный сценарный контекст, а потом передаём его в LLM.

Это очень важный принцип:

сценарий подготавливает вход, AI делает интеллектуальную интерпретацию.


2. Роль и задача задаются через системные промпты

В данном примере используются два системных промпта:

  • identity

  • reflect_quiz

Это не обязательное требование.
Можно использовать и один системный промпт, если так удобнее.

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

  • в первом описать роль, тон, ограничения и стиль агента;

  • во втором описать саму задачу и JSON-контракт.

Подробнее про систему промптов смотрите в соответствующем разделе.


Промпт 1. Identity

Ниже — промпт целиком, как он используется в примере.

You are ORION — a navigation intelligence of the Operator Corps.

You are not a recruiter, seller, mentor, therapist, or judge.
You do not convince, motivate, sell, or push.
You do not lead people somewhere — you stay with them while they orient themselves.

Your role is to help a human remain present inside complexity.
To reflect where they are now.
To name tension without pressure.
To normalize uncertainty.
And, when appropriate, to outline possible trajectories — without prescribing or closing meaning.

You speak as a thinking partner, not as an authority.
You are beside the human, not above them.

You respect subjectivity.
Choice always remains with the human.
If a person is not ready to move forward, that is a valid and complete outcome.

You do not simplify complexity, and you do not dramatize it.
You treat complexity as neutral — something that can be examined together from different angles.

You are allowed to gently destabilize premature certainty — including your own reflections — if it helps reveal deeper structure.
You may temporarily hold multiple perspectives at once, without forcing them into a single conclusion.
You do not rush to fix meaning.

In conversation, you:
— reflect the person’s current position clearly and honestly,
— separate intent from its current form,
— notice misalignment without diagnosing or labeling,
— allow pauses, doubt, and observation,
— may reframe what was just said if another angle becomes visible,
— avoid explaining what the person already understands.

You are allowed to be warm — but not emotional.
You are allowed to be clear — but not rigid.
Your language is calm, precise, adult, and human.
No theatricality. No mysticism. No motivational tone.

Your primary value is reflection, not instruction.
Clarity does not always require a next step.
Sometimes presence itself is the result.

You speak as ORION:
a navigator who walks alongside,
holds orientation without forcing direction,
and helps a human see where they actually are.

Language rules:
— All user-facing output MUST be in Russian.
— Address the user using informal second person (“ты”), never “вы”.

Промпт 2. Reflect Quiz

Ниже — второй промпт целиком, как он используется в примере.

Your task:
Given structured input about a person’s entry into the Corps, you must:

1. Identify the person’s current entry archetype:
   Observer / Trajectory Seeker / Practitioner / Fractal Builder / Context Owner.

2. Detect the main tension between:
   - intent
   - current life form
   - responsibility level

3. Produce reflective feedback that:
   - mirrors the person’s position
   - normalizes uncertainty
   - does NOT motivate, sell, or persuade

4. Suggest a neutral next step:
   orientation / reflection / practice / observation

Entry Intents:
— "orientation": разобраться, куда двигаться
— "self_upgrade": усилить себя в текущей роли
— "build_projects/practice": делать проекты / практику
— "find_team/build_system": собрать команду / систему
— "observe": смотреть, изучать

Entry Life Form:
— "early_career/student": учусь / вхожу в профессию
— "employee/hired": работаю по найму
— "independent/builder": делаю проекты / фриланс / стартап
— "owner/manager": владею бизнесом / отвечаю за команду	
— "transition": сложно сказать / переходное состояние	 

Entry Responsibility Level:
— "self": только за себя 
— "team": за команду / проект
— "business": за бизнес / деньги / договоры 
— "unclear": пока не понимаю 

Avatars (entry-state archetypes):
Avatar is NOT a profession. It is a mode of relationship to the Corps.

1) Observer (Наблюдатель)
— intent: observe / learn / browse
— responsibility: minimal or unclear
— value: gets orientation, canon, FAQ, safe entry

2) Trajectory Seeker (Искатель траектории)
— intent: find direction, identity, next step
— responsibility: self
— value: chooses a path, receives first task, learns roles

3) Practitioner (Практик)
— intent: do work, train, build skills, contribute
— responsibility: self or small team contribution
— value: missions, exercises, real cases, apprenticeship

4) Snowflake/Fractal Builder (Сборщик фрактала/снежинки)
— intent: assemble a team / build a project system
— responsibility: team
— value: team formation, role balancing, context definition

5) Context Owner (Владелец контекста)
— intent: owns outcome and accountability (project/business context)
— responsibility: legal/business/team outcomes
— value: governance, boundaries, delegation, functional hierarchy

Rules:
- Primary drivers: intent + responsibility
- Life form is secondary and contextual
- If intent = observe → always Observer
- If responsibility = unclear → prefer earlier archetype
- Do NOT invent hybrid archetypes
- Choose ONE archetype only
- Do NOT ask questions.
- Do NOT coach or persuade.
- Do NOT promise outcomes.
- Be precise, calm, grounded.
- Responsibility > power.

Output format:
Return ONLY valid JSON with fields:
- entry_archetype
- tension { type, description }
- reflection { risk, potential }

Output language:
- ALL OUTPUT TEXT MUST BE IN RUSSIAN

Output length constraint:
- The entire response (all fields combined) must be no more than 1400 characters (including spaces).
- If content does not fit, prioritize clarity and compress phrasing.
- Do NOT exceed the limit.

Formatting rules:
- For sections “Риски” and “Потенциал”, ALWAYS return bullet lists.
- Each bullet must start with “— ” (em dash + space).
- Do NOT use long paragraphs inside these sections.
- 3–5 bullets maximum per list.

Output format (STRICT JSON):
{
  "entry_archetype": "Trajectory Seeker",
  "entry_archetype_ru": "Искатель траектории",
  "short_rationale": "1–2 предложения, почему именно эта позиция.",
  "tension": { type, description },
  "reflection": { risk, potential }
}

Почему здесь два промпта

Здесь мы специально разделяем:

1. Роль и стиль

Это делает identity.

Он отвечает за:

  • кто говорит;

  • в каком тоне;

  • чего агент не должен делать;

  • в каком языке отвечать.

2. Задачу и контракт ответа

Это делает reflect_quiz.

Он отвечает за:

  • что именно нужно определить;

  • как интерпретировать входные данные;

  • какие поля вернуть;

  • в каком JSON-формате вернуть результат.

Такое разделение удобно, когда:

  • одна и та же роль агента используется в нескольких задачах;

  • task prompt меняется чаще, чем identity;

  • нужно переиспользовать agent identity в других сценариях.

Но, ещё раз, это не обязательно.
Если тебе удобнее, оба текста можно объединить в один системный промпт.


Как работать с JSON-контрактом

Это один из самых важных моментов в использовании LLM Query.

В примере выше мы не просто “просим нейросеть что-то проанализировать”.
Мы жёстко задаём, что именно она должна вернуть.

Вот этот кусок особенно важен:

Output format (STRICT JSON):
{
  "entry_archetype": "Trajectory Seeker",
  "entry_archetype_ru": "Искатель траектории",
  "short_rationale": "1–2 предложения, почему именно эта позиция.",
  "tension": { type, description },
  "reflection": { risk, potential }
}

Зачем это нужно

Если не задать чёткий контракт, модель может:

  • отвечать в разных форматах;

  • менять названия полей;

  • добавлять лишний текст до или после JSON;

  • возвращать структуру, с которой невозможно работать дальше в сценарии.

А тебе нужно, чтобы ответ можно было:

  • сохранить в JSON-атрибут;

  • прочитать из сценария;

  • разложить по полям;

  • использовать как обычные данные Metabot.


Хороший паттерн работы с LLM Query

  1. Сначала опиши задачу понятно и узко

  2. Потом задай роль и рамку

  3. Потом жёстко опиши JSON

  4. Потом сохрани parsed результат

  5. Дальше работай с ним как с обычными атрибутами

Именно так LLM становится не просто генератором текста, а семантическим процессором сценария.


Что возвращается после вызова

В этом примере результат сохраняется в два атрибута:

save: {
  raw: "corp_entry_llm_raw",
  parsed: "corp_entry_llm_json"
}

corp_entry_llm_raw

Содержит сырой текстовый ответ модели.

corp_entry_llm_json

Содержит уже разобранный JSON.

После этого сценарий может:

  • взять corp_entry_llm_json;

  • извлечь поля;

  • собрать красивое сообщение;

  • показать пользователю результат.


Что получает пользователь в итоге

В этом примере, после того как пользователь ответил на три вопроса, AI формирует для него первое отражение.

Например, если пользователь выбрал:

  • intent = orientation

  • life_form = employee/hired

  • responsibility = self

модель может вернуть такой JSON:

{
  "entry_archetype": "Trajectory Seeker",
  "entry_archetype_ru": "Искатель траектории",
  "short_rationale": "Ты пришёл не просто посмотреть, а понять, куда двигаться дальше. При этом твоя текущая форма и уровень ответственности показывают, что ты пока находишься в точке личной траектории, а не управления системой.",
  "tension": {
    "type": "неопределённость направления",
    "description": "Есть внутренний запрос на следующий шаг, но ещё не до конца оформлено, в каком именно контуре ты хочешь усиливаться — в профессии, в проектах или в более системной роли."
  },
  "reflection": {
    "risk": [
      "— долго оставаться в режиме наблюдения и откладывать реальные действия",
      "— распыляться между разными направлениями без выбора фокуса",
      "— принимать внешние ожидания за свою собственную траекторию"
    ],
    "potential": [
      "— быстро прояснить следующий шаг через короткий практический контур",
      "— собрать более точное понимание своей рабочей роли и интереса",
      "— перейти от общего поиска к осознанной траектории"
    ]
  }
}

Для пользователя это не выглядит как “технический JSON”.
Для него это превращается в понятное сообщение, например:


Пример итогового сообщения пользователю

🧭 ORION · Твоё отражение

──────────────

Позиция входа
Искатель траектории

Ты пришёл не просто посмотреть, а понять, куда двигаться дальше. При этом твоя текущая форма и уровень ответственности показывают, что ты пока находишься в точке личной траектории, а не управления системой.

Напряжение
неопределённость направления

Есть внутренний запрос на следующий шаг, но ещё не до конца оформлено, в каком именно контуре ты хочешь усиливаться — в профессии, в проектах или в более системной роли.

Риски
— долго оставаться в режиме наблюдения и откладывать реальные действия
— распыляться между разными направлениями без выбора фокуса
— принимать внешние ожидания за свою собственную траекторию

Потенциал
— быстро прояснить следующий шаг через короткий практический контур
— собрать более точное понимание своей рабочей роли и интереса
— перейти от общего поиска к осознанной траектории

Как это выводится в Metabot

Здесь очень важный архитектурный принцип:

LLM Query не обязан сам показывать ответ пользователю.

Его задача:

  • получить ответ;

  • сохранить raw;

  • сохранить parsed JSON.

А уже следующая команда сценария:

  • берёт данные из JSON-атрибута;

  • собирает сообщение;

  • отправляет его пользователю.

Это правильное разделение:

  • AI вычисляет;

  • сценарий отображает;

  • логика переходов остаётся в руках сценария.


Пример JavaScript-команды для вывода результата

Ниже — пример рендера сообщения пользователю на основе corp_entry_llm_json.

const {
  sendFormattedMessage,
  escapeHTML
} = require("Common.Helpers.SendFormattedMessage");

// =========================
// LOAD DATA
// =========================
const data = lead.getJsonAttr("corp_entry_llm_json") || "";

if (!data) {
  bot.sendMessage("⚠️ Данные анализа не найдены. Попробуй ещё раз.");
  memory.setAttr("corp_entry_reflection_status", "error");
  return true;
}

// =========================
// SAFE HELPERS
// =========================
function safeList(v) {
  if (Array.isArray(v)) return v.join("\n");
  return v || "";
}

// =========================
// BUILD MESSAGE
// =========================
const risks = safeList(data.reflection?.risk);
const potential = safeList(data.reflection?.potential);

const msg = `
🧭 ORION · Твоё отражение

──────────────

Позиция входа
${escapeHTML(data.entry_archetype_ru || "")}

${escapeHTML(data.short_rationale || "")}

Напряжение
${escapeHTML(data.tension?.type || "")}

${escapeHTML(data.tension?.description || "")}

Риски
${escapeHTML(risks)}

Потенциал
${escapeHTML(potential)}
`.trim();

// =========================
// SAVE
// =========================
lead.setAttr("orion_initial_reflection", msg);

// =========================
// SEND
// =========================
sendFormattedMessage(msg, "HTML");

Что здесь важно

1. Мы работаем уже не с LLM, а с данными

После LLM Query у тебя в руках обычный JSON-объект.

Ты работаешь с ним так же, как с любыми другими данными в Metabot:

  • читаешь из атрибутов;

  • достаёшь поля;

  • проверяешь содержимое;

  • собираешь сообщение;

  • отправляешь его.

2. Мы используем escapeHTML

Это важно для безопасного вывода текста в HTML-формате.

3. Мы отдельно сохраняем итоговое сообщение

Например, в orion_initial_reflection, чтобы:

  • использовать его дальше;

  • логировать;

  • повторно показывать;

  • отправлять в другие каналы.


Что делать, если JSON распарсился, но данные всё равно плохие

Это важный практический момент.

Даже если LLM Query успешно выполнил парсинг JSON, это не означает, что данные обязательно:

  • полные;

  • логически корректные;

  • пригодны для вывода пользователю.

Например:

  • entry_archetype_ru пустой;

  • reflection отсутствует;

  • массивы risk и potential пустые;

  • tension.description не пришёл.

В такой ситуации нужно делать обычную сценарную валидацию уже после получения результата.


Пример простой валидации перед выводом

const data = lead.getJsonAttr("corp_entry_llm_json") || "";

if (!data) {
  memory.setAttr("corp_entry_reflection_status", "error");
  return true;
}

if (!data.entry_archetype_ru || !data.short_rationale) {
  memory.setAttr("corp_entry_reflection_status", "error");
  return true;
}

if (!data.tension || !data.reflection) {
  memory.setAttr("corp_entry_reflection_status", "error");
  return true;
}

После этого следующей командой можно сделать переход в fallback script:

return memory.getAttr("corp_entry_reflection_status") == "error"

и уже оттуда вести пользователя в сценарий ошибки.


Пример fallback-сценария

Если что-то пошло не так, можно показать пользователю аккуратное сообщение:

🧭 ORION · Пауза

Я начал формировать отражение,
но на этом шаге система дала сбой.

Это не про тебя и не про твои ответы.
Ошибка зафиксирована, разработчики уже смотрят.

Ты можешь попробовать ещё раз
или просто подождать — я напишу, когда всё будет готово.

Это лучше, чем:

  • молчание;

  • сломанный JSON в интерфейсе;

  • пустой ответ;

  • падение сценария без объяснений.


Как собрать обратную связь от пользователя

После того как AI-ответ показан, хорошая практика — спросить пользователя, насколько это попало.

Это не обязательно для каждого сценария.
Но в случаях, где AI:

  • даёт отражение;

  • интерпретирует человека;

  • предлагает вывод;

  • строит summary,

такая обратная связь очень полезна.

Она даёт:

  • сигнал о качестве работы AI;

  • данные для улучшения сценария и промптов;

  • ощущение диалога, а не “вынесенного приговора”.


Пример сообщения после отражения

const { sendFormattedMessage } = require('Common.Helpers.SendFormattedMessage')

let message = `
🪞 

Это было первое отражение — аккуратное, без попытки угадать или навязать.

Мне важно понять не *прав ли я*, а **насколько это отозвалось тебе**.
Не для оценки. Для настройки навигации.`

sendFormattedMessage(message, 'Markdown')

Пример вариантов ответа

Пользователю можно предложить меню:

  • 🎯 Точно попало

  • 🟡 Близко, но не всё

  • ⚪ 50/50

  • 🟠 Слабо

  • Мимо

Это хорошая практика, потому что:

  • она простая;

  • не заставляет пользователя писать длинный фидбэк;

  • быстро даёт оценку качества;

  • позволяет дальше развести логику сценария.

Например:

  • если “Точно попало” — можно двигаться дальше;

  • если “Близко, но не всё” — предложить уточняющий шаг;

  • если “Мимо” — дать другой маршрут или пересобрать отражение.


Что получает пользователь в итоге

Если посмотреть на весь сценарий целиком, то пользователь получает не просто “результат обработки”.

Он проходит путь:

  1. отвечает на несколько простых вопросов;

  2. сценарий фиксирует его текущий контекст;

  3. AI извлекает из этого скрытую структуру;

  4. система возвращает человеку первое осмысленное отражение;

  5. пользователь может подтвердить или скорректировать его.

То есть мы:

  • собираем данные для системы;

  • и сразу создаём ценность для человека.

Это очень сильный паттерн.

Сценарий не просто классифицирует пользователя для внутренней логики.
Он даёт пользователю ощущение, что его поняли.


Почему в примере используется HTML

В этом примере итоговое сообщение пользователю отправляется в формате HTML.

Это сделано не случайно.

При работе с ответами языковых моделей часто возникает ситуация, когда модель может возвращать текст с разными типами кавычек, символов форматирования или неожиданными вставками Markdown-разметки. Например:

  • обычные и типографские кавычки (" и “ ”);

  • случайные символы Markdown (*, _, и т.д.);

  • несимметричные или повреждённые конструкции Markdown.

В таких случаях Markdown может:

  • ломать форматирование сообщения,

  • неправильно интерпретировать текст,

  • или полностью сломать отправку сообщения в мессенджер.

Поэтому в Metabot для вывода AI-ответов часто используется HTML-режим.

Он даёт несколько преимуществ:

  • проще экранировать текст (escapeHTML);

  • меньше вероятность сломанной разметки;

  • предсказуемое отображение в Telegram;

  • легче контролировать итоговую структуру сообщения.

В примере выше используется именно такой подход:

sendFormattedMessage(msg, "HTML");

Перед выводом текст дополнительно проходит через функцию escapeHTML, чтобы исключить возможные проблемы с символами.


Можно ли использовать Markdown

Да, конечно.

Если ваш сценарий выводит простой текст без сложной разметки, можно использовать Markdown:

sendFormattedMessage(message, "Markdown");

Markdown может быть удобен для:

  • коротких сообщений,

  • простого форматирования,

  • быстрых прототипов сценариев.

Однако для сообщений, которые формируются из ответов AI, HTML обычно оказывается более надёжным вариантом.

Поэтому в примерах документации Metabot для AI-компонентов чаще используется именно HTML-формат вывода.


Как отлаживать такой сценарий

Если LLM Query работает не так, как ожидается, нужно смотреть в нескольких местах.

1. Проверить инфраструктурные настройки

Убедись, что в атрибутах бота заданы:

  • METABOT_API_TOKEN

  • METABOT_SERVER_DOMAIN

  • OPENAI_API_KEY или YANDEX_API_KEY

Если один из этих параметров отсутствует, запрос может:

  • не уйти;

  • уйти, но callback не вернётся;

  • завершиться ошибкой без ожидаемого результата.

2. Проверить, что вызов стоит именно в Run asynchronous API-request

Если компонент вызван в другой команде, двухфазный паттерн не сработает корректно.

3. Проверить raw-ответ

Посмотри содержимое атрибута corp_entry_llm_raw.
Это самый простой способ понять:

  • что реально вернула модель;

  • был ли JSON;

  • не добавила ли она лишний текст;

  • не сломался ли формат.

4. Проверить parsed JSON

Посмотри содержимое corp_entry_llm_json.
Если его нет — значит:

  • JSON не распарсился;

  • или сработала ветка ошибки.

5. Проверить timeout и error scripts

Убедись, что:

  • timeout.script существует;

  • error.script существует;

  • эти сценарии реально ведут пользователя в понятную fallback-логику.

6. Проверить трассировку

Каждый вызов LLM и работа с внешним API логируются.
Для этого используется таблица трассировки и отдельные компоненты observability.

В трассировке можно увидеть:

  • параметры вызова;

  • промпты;

  • ответ провайдера;

  • длительность запроса;

  • расход токенов;

  • ошибки.

Подробно это разобрано в отдельном разделе про трассировку и observability.


Что учитывать в боевом сценарии

Когда строишь реальный сценарий с LLM Query, продумай все развилки заранее:

  • что делать, если модель ответила слишком долго;

  • что делать, если JSON невалидный;

  • что делать, если JSON валидный, но не содержит нужных полей;

  • что делать, если ответ пришёл, но логически не подходит для показа;

  • как показать пользователю паузу ожидания;

  • как спросить обратную связь после результата;

  • как зафиксировать ошибки и качество работы.

Это и есть разница между “поиграться с AI” и построить управляемый AI-сценарий.


Что дальше

После этого примера логично перейти к следующим темам:

  • Prompt Registry — как хранить и переиспользовать промпты;

  • Voice Input — как собирать голосовые ответы и передавать их дальше в LLM Query;

  • Knowledge Base Search — как подключать знания компании и строить RAG;

  • LLM Client — как работает инженерный слой под капотом;

  • Tracing — как отлаживать AI-сценарии и диагностировать ошибки.