ragline — бекенд AI/RAG Telegram-бота підтримки
Огляд
Незалежний demo-проєкт: REST API на Go для AI-ботів підтримки — база знань, повнотекстовий пошук Postgres та явний поріг довіри, що передає питання людині замість вигадування відповіді. Моделює патерн AI-ботів підтримки: власна документація клієнта проглядається пошуком, і бот має відрізняти “я знайшов відповідь” від “я не знайшов нічого релевантного” — випадок, який наївний бот приховує правдоподібною вигадкою.
Результат: бот, що ніколи не повторює здогадку з низькою довірою навіть із власного кешу, і ніколи не віддає закешовану відповідь після редагування документа, на якому вона ґрунтувалась — доведено на 4 реальних сценаріях, не заявлено.
Справжня складна задача
Справжній режим відмови RAG-бота — не “неправильна відповідь”, а “впевнена неправильна відповідь”. Пошук завжди повертає щось, якщо не обмежений; питання в тому, чи знає бот, коли це щось недостатньо добре, щоб відповідати. Окремо: закешована для швидкості відповідь має інвалідуватись при редагуванні вихідного документа.
Архітектура

Кожне питання проходить одну оркестрацію: повнотекстовий пошук бази знань → пошук кеш-запису, відфільтрований за поточною версією документа найкращого збігу → чиста функція Decide → ескалація, видача кешу, або генерація-й-кешування свіжої відповіді — з логуванням питання в обох випадках.
Ключові інженерні рішення
| Рішення | Чому |
|---|---|
Decide(hasMatch, topScore, threshold, cacheHit) винесена як чиста функція |
Жодного I/O, повністю юніт-тестована — низький бал веде до ескалації навіть якщо для цього запиту є кеш-запис, тож бот не може нескінченно повторювати одну погану здогадку |
Кеш відповідей з ключем (query_text_hash, document_id, document_version) |
Редагування підвищує version; наступне ідентичне питання не знаходить застарілий рядок кешу й регенерує проти нового вмісту, замість мовчки видавати видалену інформацію |
OR-об’єднаний tsquery з конфігурацією 'english' замість AND за замовчуванням у websearch_to_tsquery |
Знайдено наживо, не спроєктовано заздалегідь: AND-запит провалювався на будь-якому реальному питанні зі словами, відсутніми в цільовому документі (майже всі); ранній OR-фікс над 'simple' дозволяв нерелевантним питанням обганяти релевантні через спільні стоп-слова — вбудоване прибирання стоп-слів + стемінг 'english' виправили обидва одразу |
Генерація за pluggable інтерфейсом Generator |
TemplateGenerator (за замовчуванням, нуль I/O, екстрактивний) не потребує зовнішнього сервісу; опційний OllamaGenerator (реальний локальний LLM) підключається через OLLAMA_URL, не торкаючись рішення ескалації/кешування взагалі — той самий поділ, що й pluggable Generator у tumanomir |
Результати
- Повний REST API: JWT-авторизація, CRUD бази знань (
PUTпідвищує version),POST /api/askзі спільним секретом (machine-to-machine, не дія персоналу), черга ескалацій для персоналу - Рішення ескалація/генерація/кеш чисте й повністю юніт-тестоване — включно з випадком “кеш є, але довіра низька — все одно ескалація” — без потреби в БД для
go test ./... - Перевірено наскрізно через
docker compose upна одному засіяному документі “Billing FAQ”:- Релевантне питання знайшло збіг і згенерувало відповідь (бал
0.2) - Те саме питання іншим регістром влучило в кеш замість регенерації
- Нерелевантне питання (“швидкість польоту незавантаженої ластівки”) не знайшло нічого й ескалювалось — саме той випадок, для якого знадобився фікс пошуку вище; до нього це питання отримувало бал вищий, ніж справжнє питання про білінг, лише через спільні стоп-слова
- Після редагування документа (
version1 → 2) те саме питання регенерувалось замість видачі застарілого кешу, а третє ідентичне питання коректно влучило у свіжий кеш
- Релевантне питання знайшло збіг і згенерувало відповідь (бал
Код: github.com/valpere/ragline
Стек
Go · chi (router) · pgx/v5 (без ORM) · golang-jwt · bcrypt · PostgreSQL (повнотекстовий пошук: tsvector, ts_rank_cd, конфігурація english) · Docker Compose · testify · опційно: Ollama (локальний LLM, pluggable)