Skip to content

AI Telegram bot

Что делает: пользователь пишет Telegram-боту → OPORA gets update → LLM отвечает на основе вашей knowledge-базы (через RAG) → бот отправляет ответ. Помнит контекст разговора per-чат.

Типовой use-case: FAQ-бот магазина, бот технической поддержки, «спроси нашу документацию» ассистент.

  • Telegram-бот + webhook setup (getting-started/telegram.md)
  • OPORA knowledge-база — это data-table с текстовыми чанками, которые ai.embed нода предварительно проиндексировала
  • LLM API key настроен
telegram-bot-token
telegram-webhook-secret

Подготовка knowledge-базы (один раз)

Section titled “Подготовка knowledge-базы (один раз)”

Data Tables+ New → name: kb-chunks → schema:

ColumnTypeDescription
chunk_texttextRaw текст чанка
sourcetextОткуда (URL / имя файла)
embeddingvector(1536)Вектор (заполняется при загрузке)

Через CSV-импорт или через ad-hoc workflow. Пример workflow’а для bulk-import’а:

cron.trigger (once, manual)
external.http
url: "https://your-cms.example/api/articles"
(или load'ите из файла через data.fetch)
data.array.foreach (on: articles)
ai.embed
input: "{{item.title}}\n\n{{item.body}}"
data_table.rows.insert
table: kb-chunks
data:
chunk_text: "{{item.title}}\n\n{{item.body}}"
source: "{{item.url}}"
embedding: "{{embed.output}}"

Это одноразовый workflow — запускаете его, когда обновляете knowledge-базу.

┌──────────────────┐
│ telegram.webhook │
│ .trigger │
└────────┬─────────┘
│ {message: {text, chat.id, from.id}}
┌──────────────────┐
│ ai.rag.answer │ semantic search + LLM answer
│ │ memory_key: "tg:{{chat.id}}"
└────────┬─────────┘
│ {answer: "...", sources: [...]}
┌──────────────────┐
│ telegram.message │ reply_to original message
│ .send │
└──────────────────┘
secretTokenRef: telegram-webhook-secret
filter:
# Игнорируем все updates кроме text-messages в приватном чате.
# edited_message / callback_query / group-chats пусть идут мимо.
expression: |
trigger.message && trigger.message.text && !trigger.edited_message
query: "{{trigger.message.text}}"
knowledgeBase:
table: kb-chunks
embeddingColumn: embedding
textColumn: chunk_text
topK: 5
minSimilarity: 0.7 # чтобы не отвечать на вопросы, где KB пуста
systemPrompt: |
Ты помощник магазина Acme. Отвечай только на основе приведённого
контекста. Если контекста недостаточно — скажи «Не нашёл в
документации, переключаю на менеджера» и ничего не выдумывай.
memory:
key: "tg:{{trigger.message.chat.id}}"
ttlSeconds: 3600 # история чата — 1 час
model: gpt-4o-mini
maxTokens: 500

ai.rag.answer объединяет три операции: embedding запроса (ai. embed внутри), vector-search по kb-chunks (ai.vector.search) + LLM-call с context’ом (ai.generate). Это shortcut-нода — если вам нужен custom-поведение, разбейте на три отдельные.

chat_id: "{{trigger.message.chat.id}}"
text: "{{rag.answer.answer}}"
reply_to_message_id: "{{trigger.message.message_id}}"
parse_mode: Markdown

Первое сообщение:

  • /runs — новый run
  • Timeline: webhook.trigger (5ms) → ai.rag.answer (800-1500ms, embedding + search + LLM) → telegram.message.send (200ms)
  • Ответ в Telegram приходит в течение 1-2 секунд

Второе сообщение в том же чате:

  • ai.rag.answer видит memory[tg:<chat_id>] содержащий prev-turn, feed’ит в prompt → ответ контекстный

Memory очищается через 1 час inactivity (TTL).

  • ai.embed per-query — ~0.001 копейка (1 запрос, ~20 токенов)
  • ai.vector.search — 0 (local Postgres vector search)
  • ai.generate с gpt-4o-mini — ~1-2 копейки per ответ (context + output)
  • Итого: ~2 коп / вопрос. 1000 вопросов / день — 20 руб / день.

Двухуровневый fallback: сначала KB, потом general chat

Section titled “Двухуровневый fallback: сначала KB, потом general chat”
ai.rag.answer
minSimilarity: 0.7
control.switch on rag.answer.sources.length
> 0 → telegram.message.send (text = rag.answer.answer)
== 0 → ai.chat (general, без RAG)
telegram.message.send (text = chat.output)

Escalate к менеджеру на «не знаю»

Section titled “Escalate к менеджеру на «не знаю»”

Если LLM отвечает «переключаю на менеджера» — отправьте параллельно уведомление в чат-команды:

ai.rag.answer
control.switch on rag.answer.answer.includes('переключаю на менеджера')
yes → telegram.message.send (chat_id = support_team, ...)
no → terminal

Inline-кнопки «Подробнее» на sources

Section titled “Inline-кнопки «Подробнее» на sources”
telegram.message.send
reply_markup:
inline_keyboard:
- - text: "📄 Источник 1"
url: "{{rag.answer.sources.0.url}}"

Добавьте ai.transcribe перед rag.answer:

telegram.webhook.trigger
control.switch on trigger.message.voice
yes → telegram.get_file (file_id = voice.file_id)
ai.transcribe (audio = get_file.output)
↓ trigger_text = transcribe.output
no → trigger_text = trigger.message.text
ai.rag.answer (query = trigger_text)
...

ai.transcribe + telegram.get_file — в backlog’е, ETA Q2-2026.

Бот отвечает «ничего не нашёл» на очевидные вопросы

Section titled “Бот отвечает «ничего не нашёл» на очевидные вопросы”

KB пустая или embedding-column пустой. Data Tables → kb-chunks → Rows проверьте что embedding’и заполнены (не null). Если пусто — ре-запустите bulk-import workflow.

ai.rag.answer не получает memory.key. Проверьте что template resolve’ится правильно (в /runs/<id>/traces видно resolved-value в step-input’е).

LLM выдумывает ответы не из KB

Section titled “LLM выдумывает ответы не из KB”
  • Прописанный system prompt не достаточно жёсткий. Добавьте явное «Если в контексте нет ответа — не отвечай, не выдумывай».
  • Model — gpt-4o-mini иногда халатничает. Попробуйте gpt-4o для critical-вопросов.

Webhook retry от Telegram → OPORA дедуп’ит по update_id, должно работать из коробки. Если всё равно дубли — проверьте что trigger- нода настроена с idempotency (default включена).