aichat-go

Мультитенантная платформа AI-бота для продаж

Агент с tool-calling
Гибридный поиск: векторный + BM25
Мультитенантная изоляция
Автоматические follow-up
Go Qdrant PostgreSQL OpenRouter NixOS

Агент с tool-calling

Модель сама ведёт диалог через четыре инструмента: hybrid_search, set_state, get_state, send_lead. Никаких порогов и переходов между этапами.

🔍

Гибридный поиск

Плотные эмбеддинги + BM25-разреженные векторы с DBSF-слиянием в Qdrant. Реранкер — сама агентская модель: сырые результаты сразу попадают в ответ инструмента.

Multi-backend ToolCaller

Anthropic Messages API или OpenAI tools spec (через OpenRouter — GLM, DeepSeek, Kimi и пр.). Пул ключей с round-robin и prompt-кэшированием там, где оно есть.

🕑

Умные follow-up

4-шаговая последовательность таймеров (5 мин / 15 мин / 40 мин / 24 ч) с LLM-классификацией клиента. Автоматически определяет холодные, горячие и завершённые диалоги.

🏠

Мультитенантность

Полная изоляция проектов через составные ключи. Каждый арендатор получает собственного бота, коллекции, промпты и группу для лидов при общей инфраструктуре.

🚀

Инкрементальный краулер

4-фазный конвейер (скачивание / чанкинг / эмбеддинг / загрузка) с возобновлением по URL. Поддержка HTML, PDF, DOCX, XLSX, TXT. Семантический чанкинг с LLM-метками тем.

Основные понятия

Проект
Арендатор (tenant) — один бизнес со своим ботом, базой знаний, промптами и CRM-целью. Все пути данных используют составные ключи (chat_id, project_id).
Этап
Либо StageActive (активный диалог), либо StageFinal (лид передан, диалог зафиксирован). Вычисляется из поля Finished — отдельного поля этапа нет, что исключает рассинхронизацию.
База знаний
Коллекции Qdrant для каждого проекта с чанкированным контентом сайта, содержащим как плотные (embedding), так и разреженные (BM25) векторы.
Determined URL
URL страницы услуги, на который агент фиксируется через set_state(determined_url=...). Используется как фильтр для последующих вызовов hybrid_search.
Лид
Результат работы: контактная информация + резюме диалога + история чата + файлы, отправляемые в CRM-группу для лидов.
Таймер
Планировщик автоматических follow-up. Когда пользователь замолкает, таймер запускает агентский tool-calling цикл с синтетическим ходом [TIMER PING]; агент сам решает, отправить ли напоминание или передать лид.
ToolCaller
LLM-клиент, который использует агент. Многоходовые tool-calling диалоги. Два бэкенда: нативный Anthropic Messages API и OpenAI tools spec (покрывает маршрутизацию OpenRouter к GLM, DeepSeek, Kimi и т.д.).

Обзор архитектуры

Сквозной поток данных от webhook Telegram до отправки лида в CRM. Агент работает как tool-calling цикл — модель сама решает, когда искать, на чём фиксироваться и когда передать лид.

TelegramWebhook POST
нормализация
GatewayTelegramUpdate → Message
АгентTool-calling цикл
tool: hybrid_search
Гибридный поискQdrant + BM25
QdrantDense + Sparse
PostgreSQLBM25 Index
рассуждение / ответ
ToolCallerAnthropic / OpenAI tools
OpenRouterGLM / DeepSeek / Kimi / ...
tool: send_lead
CRM-выводГруппа лидов
TelegramСообщения в группу
Ключевые проектные решения: Состояние вычисляется (а не хранится) из поля Finished. Агент управляет ходом диалога через четыре инструмента (hybrid_search, set_state, get_state, send_lead) — никаких подкрученных порогов. Метрики применяются через декорирование (ноль импортов Prometheus в ядре). Все пути данных используют составные ключи (chat_id, project_id) для изоляции арендаторов.
Детали компонентов
КомпонентРасположениеНазначение
Gatewayinternal/gateway/Нормализация TelegramUpdate в domain.Message (текст, подпись, файлы, отправитель)
Агентinternal/agent/Tool-calling цикл: восстанавливает поток сообщений из chat_history.blocks, диспатчит вызовы инструментов, возвращает финальный текстовый ответ
ToolCallerinternal/llm/toolcaller*.goМногобэкендный tool-calling клиент: Anthropic Messages API + OpenAI tools spec (включая маршрутизацию через OpenRouter). Пул ключей с rate-limit fallback.
Provider (extract / embedding)internal/llm/openai.go, claude.goОдношотовые completion-вызовы для краулерных тем чанков, классификации статуса в таймере, эмбеддингов. Маршрутизация через слоты extract / embedding.
Гибридный поискinternal/search/Плотные эмбеддинги + BM25-разреженные векторы с DBSF-слиянием в Qdrant. Реранкер — сама агентская модель: сырые результаты сразу попадают в ответ инструмента hybrid_search.
CRMinternal/crm/Отправка лидов в Telegram-группу (резюме + история + медиа). Двухфазная запись гарантирует идемпотентность.
Таймерinternal/timer/Автоматические follow-up. Запускает агентский tool-calling цикл с синтетическим ходом [TIMER PING]; агент сам решает, что делать.
Краулерinternal/crawler/4-фазный конвейер: скачивание, чанкинг, эмбеддинг, загрузка в Qdrant
Метрикиinternal/metrics/Обёртки Prometheus (без связи с основными пакетами)
Администрированиеinternal/admin/Обработчик команд админ-группы для кросс-проектного управления
i18ninternal/i18n/Локализация (ru/en) с встроенными файлами локалей на этапе компиляции
Жизненный цикл запроса (от webhook до ответа)

Каждое входящее сообщение Telegram проходит следующий путь:

  1. Приём webhookPOST /webhook/{tg_api_key} поступает на публичный HTTP-сервер (:8080). API-ключ в URL маршрутизирует к нужному бандлу проекта.
  2. Нормализация в GatewayTelegramUpdate преобразуется в domain.Message с извлечением текста, подписи, файлов и информации об отправителе.
  3. Загрузка состояния — Состояние чата загружается из PostgreSQL по составному ключу (chat_id, project_id).
  4. Проверка команд/start сбрасывает состояние и возвращает приветствие. /debug переключает режим отладки.
  5. Сбор файлов — Вложения накапливаются в состоянии. Файлы без текста получают немедленное подтверждение без LLM-вызовов.
  6. Диспетчеризация по этапу — На основе вычисленного состояния (CurrentStage()) сообщение направляется соответствующему обработчику этапа.
  7. Сохранение состояния — Обновлённое состояние записывается обратно в PostgreSQL.
  8. Сброс таймера — Таймер follow-up сбрасывается на шаг 0 для данного чата.
Структура проекта
cmd/
  bot/         # Основной бинарник бота
    main.go     # Связка: БД, LLM по уровням, бандлы проектов, HTTP-сервер
    config.go   # Загрузка JSON-конфига с fallback на env-переменные
  crawler/     # CLI-индексатор базы знаний
    main.go     # Подкоманды: init-project, seed, add-urls, register
  cli/         # Локальный readline-интерфейс для тестирования
    main.go     # Тот же конвейер агента, файловый CRM-вывод

internal/
  domain/      types.go        # Stage, Message, ChatState, Lead, Block
  agent/       agent.go        # Точка входа HandleMessage
                toolagent.go    # Tool-calling цикл, диспатч инструментов, системный промпт
                commands.go     # /start, /debug
                lead_commands.go # Группа лидов: /prompt, /stats
                trace.go        # Записи трейса по ходу для отладки
  llm/         toolcaller.go               # Интерфейс ToolCaller, типы Block/Message
                toolcaller_anthropic.go     # Бэкенд Anthropic Messages API
                toolcaller_openai.go        # Бэкенд OpenAI tools spec (включая OpenRouter)
                toolcaller_chain.go         # Цепочка нескольких бэкендов с fallback
                tiered.go       # TieredClient (слоты extract / embedding)
                openai.go       # OpenAI-совместимый провайдер (extract, embeddings)
                claude.go       # Claude провайдер (extract)
                embedding_adapter.go # Форматирование текста для конкретных моделей
  search/      hybrid.go       # DBSF-слияние Qdrant (dense + BM25)
                bm25.go         # BM25Encoder, Tokenize
  db/          postgres.go     # Реализация PostgreSQL
  gateway/     telegram.go     # Нормализация TelegramUpdate
  telegram/    client.go       # HTTP-клиент, retry при 429
  crm/         telegram.go     # TelegramCRM: резюме + история + медиа
  timer/       timer.go        # Планировщик, последовательности, LLM-классификация
  metrics/     metrics.go      # Определения метрик
                llm.go          # Обёртка InstrumentedLLMClient
  crawler/     pipeline.go     # Возобновляемый 4-фазный конвейер
                chunker.go      # Семантический + простой чанкинг
                indexer.go      # Upsert в Qdrant
  i18n/        i18n.go         # T(), Tf(), встроенные локали
                locales/    ru.json, en.json

migrations/  001-013 (последние: 012_chat_history_blocks.sql, 013_drop_3stage_remnants.sql)
nix/         module.nix, example-configuration.nix
flake.nix    Makefile
Точки расширения
Новый LLM-провайдер

Реализуйте интерфейс Provider

internal/llm/
Новый CRM-бэкенд

Реализуйте интерфейс CRM (паттерн мульти-CRM диспетчер)

internal/crm/
Новый Gateway

Нормализация в domain.Message + webhook-маршрут

internal/gateway/
Свой поисковый бэкенд

Реализуйте интерфейс KnowledgeBase

internal/search/

Диалоговый поток

Агент работает как один tool-calling цикл на ход пользователя. Модель сама решает, когда искать, на чём фиксироваться через set_state, и когда передавать лид через send_lead.

HandleMessage
  ├─ если state.Finished → стандартное подтверждение (диалог зафиксирован)
  └─ иначе runToolLoop(state, history, userMsg)
        msgs = rebuildMessages(history)         // воспроизводит прошлые tool_use / tool_result блоки
        msgs += user(userMsg)
        цикл (макс. 8 итераций):
          response = toolcaller.Call(systemPrefix, systemSuffix, msgs, tools)
          msgs += assistant(response.blocks)
          если нет tool_use блоков:
              вернуть последний текстовый блок как ответ пользователю
          для каждого tool_use:
              result = dispatch(tool_use)        // hybrid_search / set_state / get_state / send_lead
              msgs += user(tool_result(id, result))
        // переполнение → стандартный safety-ответ, громкий лог

Инструменты

ИнструментНазначениеАргументы
hybrid_search Поиск по базе знаний проекта. Возвращает топ-k DBSF-объединённых (dense + BM25) чанков. Реранкер — сама модель. query, top_k (с серверной стороны ограничен 3), опционально url_filter
set_state Сохранить любые данные, которые модель хочет помнить между ходами. Три плоских строковых поля (без вложенных объектов, поэтому слабые toolcaller-модели не могут сломать JSON). notes (свободный блокнот — перезаписывается полностью при каждом вызове), determined_url, client_status (hot/cold)
get_state Перечитать текущий ChatState прямо посередине хода. Подстраховка — те же данные есть в суффиксе системного промпта. нет
send_lead Передать диалог как лид в CRM. Идемпотентна — если LeadSent=true уже стоит, возвращает ok без повторной отправки. summary

Структура системного промпта

Системный промпт разделён на две кэшируемые части:

  • Префикс (стабильный, кэшируемый): преамбула движка (agent.toolcaller_preamble — объясняет инструменты и как мапить инструкции проекта в естественном языке на вызовы инструментов), затем редактируемый prompt1 проекта, затем схема инструментов.
  • Суффикс (изменчивый, ломает кэш): снимок состояния на текущий ход — DeterminedURL, контакт, extras, файлы, опциональная подсказка PriceURL, ClientStatus.

Бэкенды Anthropic помечают префикс cache_control: ephemeral (90% скидка на input при попадании в кэш). OpenAI / DeepSeek / GLM (через OpenRouter) автоматически кэшируют длинные стабильные префиксы.

Детали двухфазного сохранения лида

Finished=true записывается в базу данных до отправки в CRM, а LeadSent=true записывается после. Это предотвращает дублирование лидов при параллельных webhook-запросах или срабатываниях таймера, читающих устаревшее состояние. Если бот падает между двумя записями, система таймеров повторяет отправку в CRM при следующем срабатывании или при перезапуске (через chatStore.ListFinishedUnsent).

Finished=trueЗапись в БД
Отправка в CRM
LeadSent=trueЗапись в БД

Сбой между этими состояниями → таймер повторяет отправку в CRM при следующем срабатывании или перезапуске

Сохранение блоков хода (chat_history.blocks)

Полная последовательность assistant / tool_result блоков для каждого хода хранится в chat_history.blocks (JSONB). На следующем ходе rebuildMessages воспроизводит эти блоки обратно в модель, чтобы она видела свои собственные предыдущие tool_use / tool_result обмены. Для legacy-строк, где blocks пуст, fallback — обычные поля question / reply.

Обработка загрузки файлов

Когда пользователь отправляет файл без текста, бот немедленно подтверждает получение («Принято! Дайте знать, когда закончите.») без обращения к LLM. Файлы накапливаются в ChatState.Files и прикрепляются к лиду при срабатывании send_lead.

Медиафайлы в лиде группируются по типу для наглядного представления:

  • Фото + видео — отправляются как медиа-альбом (Telegram группирует их визуально)
  • Документы — отправляются как альбом документов
  • Голосовые / видео-заметки — отправляются поштучно (Telegram не поддерживает альбомы для них)
Кнопка запроса контакта

На первом ходе, где упоминается сбор контакта, бот отправляет кнопку Telegram request_contact вместе с ответом. Это позволяет пользователям поделиться номером телефона одним нажатием.

  • Отправляется только один раз за диалог
  • Полученный контакт попадает в ChatState.SharedContact (не в историю) для конфиденциальности
  • Включается в передаваемый лид
Передача лида по таймеру

Планировщик таймеров запускает тот же tool-calling цикл с синтетическим ходом [TIMER PING]. Агент сам решает, отправить ли follow-up сообщение или вызвать send_lead. На последнем интервале (24 ч) таймер передаёт лид безусловно для любого незавершённого чата — независимо от client_status — чтобы ни один лид не потерялся. Горячих клиентов с уже полученным контактом агент должен передавать раньше сам (промт подталкивает к send_lead после 1–2 пингов, если данных достаточно).

Когда срабатывает страховочная отправка, а агент сам так и не вызвал send_lead, state.LeadSummary пустой. Тогда таймер просит агента сделать резюме диалога через Agent.SummarizeForLead — одиночный Complete-вызов (без инструментов) на тире summary (дешёвый mid-тир, например gemma-4-26b-a4b-it). Он рендерит диалог как транскрипт Клиент/Бот плюс блокнот агента и выдаёт резюме на 1–2 абзаца для оператора, чтобы лид никогда не приходил пустым.

Блокнот агента (set_state)

set_state(notes="...") — свободный текстовый блокнот агента. Заменил прежнюю структурированную схему contact{method,value} + extras{...}. Модель пишет обычный текст, при каждом вызове перезаписывается полностью — что не перенёс, то потерял. Соглашения живут в описании инструмента, не в схеме:

name: Виктор
contact: phone +79130001234
service: оценка квартиры
city: Барнаул
date: на следующей неделе

Блокнот рендерится в системный суффикс на каждом ходу, так что модель видит своё состояние, и он же служит входом для Agent.SummarizeForLead на 24 ч. Плоская строковая форма выбрана намеренно: слабые toolcaller-модели (например, glm-5.1) роняют закрывающие фигурные скобки на set_state с вложенными объектами — это раньше тихо теряло номера телефонов и упирало цикл в overflow-предохранитель.

Лид как живой документ (обновление после отправки)

После того как сработает send_lead, чат не замолкает. Клиент может продолжать писать ещё до пяти сообщений; каждое добавляется в chat_history, а существующая запись лида в CRM обновляется на месте — для Telegram это editMessageText по основному сообщению с резюме плюс editMessageMedia по прикреплённому файлу истории чата .txt. Текст резюме остаётся неизменным, но появляется подпись history updated: <ts>, чтобы оператор заметил.

Ссылки на артефакты CRM живут в отдельной таблице lead_dispatches (chat_id, project_id, provider) — одна строка на CRM. Multi-CRM поддерживается по построению: чат, отправленный одновременно в Telegram и Bitrix, получит две строки, а пост-отправочное обновление вызовет UpdateLead по каждой.

Очередь сообщений per-chat (батчинг)

Входящие сообщения не вызывают HandleMessage напрямую. Каждый шлюз (Telegram webhook, CLI-цикл) вызывает agent.Enqueue, который сохраняет сообщение пользователя как orphan-строку в chat_history (Question заполнен, Reply пустой) и кладёт в per-chat очередь cheggaaa/mb. Одна горутина-воркер на (chat_id, project_id) разгребает очередь последовательно.

Что это даёт:

  • Нет гонки двойной отправки: один воркер на чат — структурное решение проблемы конкурентных горутин, которые раньше затирали состояние и слали в CRM по два поста на одного клиента.
  • Объединение всплесков: сообщения, прилетевшие пока воркер думал над предыдущим ходом, копятся в очереди и обрабатываются следующей пачкой. Модель видит один объединённый ход пользователя, а не три последовательных.
  • Восстановление после краша: orphan-строки, пережившие перезапуск процесса, подбираются следующей пачкой — ни одно сообщение не теряется.

Ответы возвращаются через per-project колбэк agent.Replier (cmd/bot подключает его к tgClient.SendMessage; cmd/cli печатает в stdout). Воркеры завершаются после пяти минут простоя и заново стартуют при следующем сообщении.

Поисковый конвейер

Агент вызывает hybrid_search как инструмент. Инструмент возвращает DBSF-объединённые результаты Qdrant (dense + BM25) сырыми — реранкер это сама агентская модель.

1

Вызов инструмента: hybrid_search

Агент сам формулирует поисковую строку (отдельного LLM-вызова для перефразирования нет). Опционально ограничивает результаты одним URL через url_filter — обычно когда уже установлен determined_url, или с price_url при вопросах о ценах.

2

Гибридный поиск

Qdrant prefetch с плотными эмбеддингами (косинусное сходство, 1024 измерения через Matryoshka-усечение) и BM25-разреженными векторами. Встроенное в Qdrant DBSF (Distribution-Based Score Fusion) объединяет результаты, сохраняя абсолютный сигнал релевантности в отличие от ранговой RRF. Каждый prefetch извлекает limit * 2 кандидатов для алгоритма слияния.

3

Серверное ограничение

Модели возвращаются топ-3 результата независимо от запрошенного top_k. Модель сама судит, какие из них (если вообще) релевантны для текущего хода. Никаких отдельных вызовов реранкера, никакого расширения соседями — цикл маленький и быстрый.

Визуальная схема поиска

Агент вызывает hybrid_search(query, url_filter?)
Плотные эмбеддинги
cosine, 1024 dims
+
BM25 Sparse
ключевые слова
DBSF-слияние
Distribution-Based Score Fusion (сохраняет абсолютную релевантность)
Топ-3 результата возвращаются агенту
Агент анализирует результаты, решает, что делать дальше
DBSF vs RRF: DBSF нормализует выход каждого скорера на основе его реального распределения, поэтому результат с высоким косинусным сходством сохраняет свой абсолютный сигнал релевантности. RRF (Reciprocal Rank Fusion) основан исключительно на рангах и теряет эту информацию.
3
Top-k cap
1024
Размерность эмбеддинга
DBSF
Слияние
0.5
Мин. скор
Коллекции Qdrant для каждого проекта
КоллекцияВекторыНазначение
{project}_content_hybridDense + SparseЧанкированный контент с эмбеддингами, обогащёнными темой. Инструмент hybrid_search читает эту коллекцию.
{project}_sourceТолько DenseПолное содержимое страниц. Хранится для нужд краулера; рантайм-агент к этой коллекции не обращается.
Детали реализации BM25
  • Стандартная формула BM25: IDF * (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * dl/avgDL))
  • Параметры: k1=1.5, b=0.75
  • Токенизатор: Unicode-совместимый (кириллица + латиница), приведение к нижнему регистру, разбивка по не-буквенным/не-цифровым символам
  • Хранение: словарь, IDF, avgDL, numDocs хранятся как JSONB в PostgreSQL (таблица bm25_indexes)
  • Кеш в памяти: загружается из БД при первом использовании для каждого проекта, инвалидируется при обновлениях краулера

Карта LLM-вызовов

В системе два LLM-клиента: агентский ToolCaller (многоходовый tool-calling для клиентского цикла) и legacy Provider (одношотовые completion-вызовы для тем краулера, классификации в таймере, эмбеддингов).

Клиенты

КлиентКонфигНазначениеБэкенды
ToolCaller models.toolcaller Агентский tool-calling цикл. Многоходовый диалог с вызовами инструментов и их результатами. Модель сама ведёт диалог. Anthropic Messages API (нативно); OpenAI tools spec (включая маршрутизацию через OpenRouter к GLM, DeepSeek, Kimi и пр.)
Provider (extract) models.extract Одношотовый completion для тем чанков краулера, классификации статуса в таймере, текста follow-up. OpenAI-совместимый; Claude
Provider (embedding) models.embedding Векторные эмбеддинги для гибридного поиска. OpenAI-совместимый (OpenRouter, native)

Где срабатывает каждый клиент

ГдеКлиентНазначение
internal/agent/toolagent.go · runToolLoopToolCallerГлавный цикл агента — один вызов на итерацию цикла; модель возвращает либо финальный ответ, либо один или несколько tool_use блоков.
internal/agent/toolagent.go · GeneratePingToolCallerТот же цикл, запускается таймером с синтетическим ходом [TIMER PING].
internal/timer/timer.go · classifyStatusProvider (extract)Классификация молчащего клиента как COLD / HOT.
internal/timer/timer.go · generateFollowUpProvider (extract)Генерация текста follow-up сообщения.
internal/crawler/chunker.go · chunkSemanticProvider (extract)Определение границ внутри страницы при индексации.
internal/llm/embedding_adapter.goProvider (embedding)Векторные эмбеддинги для чанков (индексация) и запросов (поиск).
Структура системного промпта (с кэшированием)
  • Префикс (стабильный, кэшируемый): преамбула движка (agent.toolcaller_preamble) + prompt1 проекта + схема инструментов. Бэкенды Anthropic помечают это cache_control: ephemeral — 90% скидка на input при попадании в кэш. OpenAI / DeepSeek / GLM автоматически кэшируют длинные стабильные префиксы (~50% скидка при cached_tokens > 0).
  • Суффикс (изменчивый, ломает кэш): снимок состояния на текущий ход — DeterminedURL, контакт, extras, файлы, опциональная подсказка PriceURL, ClientStatus.
Маршрутизация провайдеров и retry

Каждая цепочка ToolCaller / Provider имеет независимый retry и fallback:

Попытка 1 → rate-limit/transient? → задержка (1s + jitter) → повтор
Попытка 2 → rate-limit/transient? → задержка (2s + jitter) → повтор
Попытка 3 → rate-limit/transient? → падение далее
Не-transient ошибка? → падение далее немедленно
Все исчерпаны → AllProvidersExhaustedError

Формула задержки: 2^attempt + 10% jitter, максимум 60 с (или Retry-After провайдера, если меньше). Пул ключей ротирует API-ключи round-robin.

Классификация transient-ошибок: rate-limits (429) считаются transient. Для OpenAI-tools бэкендов мы дополнительно считаем transient встроенные ошибки и пустые массивы choices (возвращаем RateLimitError) — наблюдается, когда апстрим DeepSeek/GLM возвращает некорректный success.

Адаптеры эмбеддингов

AdaptedProvider оборачивает провайдер эмбеддингов и форматирует текст в зависимости от режима (документ или запрос). Адаптер выбирается автоматически по имени модели. Модели E5-instruct / Qwen3-Embedding получают префикс Instruct: ...\nQuery: ... для запросов. Авторазбиение по лимиту токенов: если чанк превышает лимит модели, текст разбивается по предложениям, каждая половина эмбеддится рекурсивно, и векторы усредняются + L2-нормализуются.

Конвейер краулера

4-фазный инкрементальный конвейер, который скачивает, чанкирует, эмбеддит и загружает контент в Qdrant. Промежуточные результаты сохраняются для каждого URL для возобновления.

1

Скачивание страниц

HTTP GET с параллельными воркерами (по умолчанию: 3). Поддержка HTML, PDF, DOCX, XLSX, TXT через автоопределение. Каждая страница сохраняется в 1_pages/{urlhash}.json. Логика пропуска: страницы с существующими файлами пропускаются при повторном запуске.

2

Семантический чанкинг

Определение границ через LLM: выводит якорные фразы TOPIC: X | STARTS: Y для каждой страницы (уровень Извлечение, temp 0.1). Исходный текст разрезается по обнаруженным границам. Каждый чанк получает метку темы. Сохраняется в 2_chunks/{urlhash}.json. Fallback на разбивку по абзацам с --no-llm-chunk.

3

Пакетный эмбеддинг

32 чанка за API-вызов. Тема добавляется перед эмбеддингом: "Topic: X\n\n{text}". BM25-разреженные векторы перестраиваются из всех чанков (IDF требует полного корпуса). Сохраняется в 3_embeds/{urlhash}.json + bm25.json. Авторазбиение по лимиту токенов обрабатывает слишком большие чанки.

4

Загрузка в Qdrant

Upsert в Qdrant пакетами по 50 точек. Страницы источников загружаются с реальными эмбеддингами в {name}_source. Чанкированный контент — в {name}_content_hybrid. BM25-индекс сохраняется в PostgreSQL. Данные конвейера архивируются в project-archives/{name}.tar.gz.

Инкрементальное поведение: Каждый шаг проверяет наличие существующих файлов для URL перед обработкой. Повторный запуск конвейера обрабатывает только отсутствующий контент. Удалите папку шага для принудительной переобработки с этого момента. Ключ файла: sha256(url)[:16].
Структура хранения
new-projects/{project-name}/
  1_pages/{urlhash}.json    # одна Page на URL
  2_chunks/{urlhash}.json   # []Chunk на URL
  3_embeds/{urlhash}.json   # []EmbeddedChunk на URL
  3_embeds/bm25.json        # снимок BM25-энкодера
Поддержка типов документов
ФорматБиблиотекаПримечания
HTMLgo-readability v2Алгоритм Firefox Reader View. Fallback: обход DOM (main → article → body, удаление nav/header/footer/script)
PDFledongthuc/pdfИзвлечение текста. Отсканированные PDF без текстового слоя пропускаются.
DOCXfumiama/go-docxИзвлечение текста абзацев и таблиц
XLSXxuri/excelizeВсе листы как текст с табуляцией
TXTСодержимое как есть, без HTML-парсинга
Семантический чанкинг vs простой чанкинг

Семантический чанкинг (по умолчанию)

Один LLM-вызов на страницу (уровень Извлечение, temp 0.1) определяет естественные тематические границы. LLM выводит якорные фразы в формате TOPIC: X | STARTS: Y. Исходный текст разрезается по обнаруженным границам, сохраняя точный текст источника без перефразирования LLM. Каждый чанк получает метку темы для эмбеддинга.

Простой чанкинг (--no-llm-chunk)

Разбивка с учётом абзацев и заголовков. Текст разбивается по двойным переносам строк и markdown-заголовкам. Короткие абзацы объединяются до maxChars (по умолчанию 1500). Чанки получают нумерованные метки тем («Part 1», «Part 2» и т.д.).

Стратегия добавления темы

При индексации текст каждого чанка дополняется меткой темы перед эмбеддингом:

До: "Our basic plan starts at $99/month with unlimited support."
После:  "Topic: Pricing plans\n\nOur basic plan starts at $99/month with unlimited support."

Тема выступает как семантический якорь — эмбеддинг теперь захватывает тему чанка, а не только его поверхностное содержание. Запрос о «стоимости» будет лучше совпадать с чанком, привязанным к теме «Pricing plans», даже если текст чанка не содержит слова «стоимость».

Поисковые запросы не нуждаются в префиксе темы — пространство эмбеддингов выравнивается естественным образом.

Примеры использования CLI
# Создать новый проект и сразу заполнить базу знаний
bin/aichat-crawler init-project \
  --name myproject \
  --tg-api-key "123456:ABC-DEF" \
  --tg-lead-group -1001234567890 \
  --language en \
  --prompt1 "You are a sales consultant..." \
  --start-reply "Welcome! How can I help?" \
  --sitemap-url "https://example.com/sitemap.xml"

# Заполнить проект позднее (отдельно от создания)
bin/aichat-crawler seed \
  --project-id "uuid-from-init-output" \
  --project-name myproject \
  --sitemap-url "https://example.com/sitemap.xml"

# Добавить URL в существующий проект
bin/aichat-crawler add-urls \
  --project-name myproject \
  --urls "https://example.com/new-page1,https://example.com/new-page2"

# Установить промпты из файлов
bin/aichat-crawler set \
  --project-name myproject prompt1 @prompts/prompt1.txt

Система таймеров

Автоматические follow-up последовательности, срабатывающие при молчании пользователя. LLM-классификация определяет действие на каждом шаге.

Временная шкала follow-up

1
5 мин
Первая проверка — агент сам решает, ответить или промолчать
2
15 мин
Вторая проверка
3
40 мин
Третья проверка
4
24 часа
Последний интервал — лид передаётся для любого незавершённого чата

Логика каждого срабатывания

Срабатывание таймера
Запуск агентского tool-calling циклас синтетическим ходом [TIMER PING]
Агент вызвал send_lead
Лид передан, диалог зафиксирован. Закрывающий текст не отправляется — диалог окончен.
Агент ответил текстом
Follow-up сообщение отправлено клиенту, таймер переходит к следующему интервалу.
Агент промолчал
Сообщение не отправляется. Таймер переходит дальше. На последнем интервале (24 ч) лид передаётся всё равно, независимо от client_status.
Детали жизненного цикла
  1. Сброс — Каждое входящее сообщение сбрасывает таймер для данного чата, начиная последовательность с шага 0.
  2. Срабатывание — Загружается свежее состояние чата и история. Пропускается, если уже Finished + LeadSent. Иначе запускается агентский tool-calling цикл с синтетическим ходом [TIMER PING] (Agent.GeneratePing). Агент сам решает, что делать.
  3. Подстраховка на последнем интервале — На последнем интервале (24 ч) планировщик передаёт лид безусловно для любого незавершённого чата, независимо от client_status. Это страховка от потери лида — горячих клиентов агент должен передавать раньше сам (промт подталкивает к send_lead после 1–2 пингов, если контакт получен), но если не вызвал, лид всё равно уходит в 24 ч с тем что собрано.
  4. Сохранение — Состояние таймера записывается в PostgreSQL со временем следующего срабатывания. При перезапуске Reload() восстанавливает все активные таймеры, вычисляя оставшуюся задержку и немедленно запуская просроченные.
  5. Повтор отправки лида — Если таймер обнаруживает Finished=true, LeadSent=false, он повторяет отправку в CRM вместо запуска агента.
  6. Отмена — Когда пользователь отправляет новое сообщение, старый таймер отменяется (горутина завершается + запись в БД обновляется).

Безопасность горутин: Каждая пара чат/проект получает одну горутину. Проверка идентичности current == self предотвращает ситуацию, когда вытесненная горутина очищает состояние более нового таймера.

Мультитенантность

Все пути данных используют составные ключи (chat_id, project_id) для изоляции арендаторов. Каждый проект получает собственные ресурсы при общей инфраструктуре.

По проектам (изолировано)

Telegram bot token
Коллекции Qdrant
Системные промпты (JSONB)
Группа лидов (CRM-цель)
BM25 sparse encoder
Экземпляр агента
Языковая локаль

Общее (инфраструктура)

Экземпляр PostgreSQL
Экземпляр Qdrant
Пул LLM-провайдеров
Планировщик таймеров
HTTP-серверы
Эндпоинт метрик
Детали изоляции данных
ДанныеТаблица/КоллекцияКлюч
Состояние чатаchat_statesPRIMARY KEY (chat_id, project_id)
История чатаchat_historyWHERE chat_id AND project_id
ТаймерыtimersPRIMARY KEY (chat_id, project_id)
BM25-индексbm25_indexesPRIMARY KEY (project_id)
Поисковые векторыQdrant{project_name}_content_hybrid, {project_name}_source
Маршрутизация webhookURL-путьPOST /webhook/{tg_api_key}
Регистрация в рантайме

Бандлы проектов (Telegram-клиент, агент, CRM, база знаний) хранятся в карте в памяти. Новые проекты могут быть зарегистрированы в рантайме двумя путями:

  • Админ-группа — команда /project_init создаёт проект, заполняет базу знаний и регистрирует бандл мгновенно. Перезапуск не требуется.
  • Внутренний APIPOST /api/register-project/{id} для CLI или внешних инструментов, которые заполняют БД самостоятельно.
Процесс подключения проекта (Telegram)

Шаг 1: Создайте бота в BotFather, скопируйте токен.

Шаг 2: Отправьте одну команду в группе лидов админ-проекта:

/project_init myproject sitemap https://example.com/sitemap.xml 123456:AAH...token

Система проверяет токен, создаёт проект, заполняет базу знаний и отвечает с прогрессом:

> Project "myproject" created (bot: @myproject_bot). Seeding started...
> Scraping completed: 42 pages
> Chunking completed: 156 chunks
> Embeddings completed: 156 vectors
> Upload completed: 42 pages, 156 chunks indexed
> Project "myproject" init finished!
> Test the bot: https://t.me/myproject_bot
> To create the CRM group, click:
> https://t.me/myproject_bot?startgroup=connect_myproject

Шаг 3: Нажмите на deep link. Telegram откроет интерфейс создания группы с уже добавленным ботом. Бот автоматически обнаружит группу и подключит её как группу лидов. ID группы не нужен — обнаруживается автоматически через команду /start connect_{name}.

Справочник команд администратора
КомандаОписание
/project_init <name> sitemap <url> <token>Создать проект, просканировать sitemap, заполнить базу знаний
/project_init <name> urls <url1,url2> <token>Создать проект из отдельных URL
/project_prompt1 <name> <text>Обновить системный промпт проекта
/project_price_url <name> <url>Установить URL-подсказку о ценах (используется агентом через hybrid_search)
/project_stats <name> [period]Показать статистику чатов
/project_info <name>Показать информацию о проекте
/project_add_urls <name> <urls>Добавить URL в базу знаний
/project_delete_urls <name> <urls>Удалить чанки URL из базы знаний
/project_delete <name>Мягкое удаление проекта
/project_restore <name>Восстановить мягко удалённый проект

Команды группы лидов (по проекту)

КомандаОписание
/helpПоказать все доступные команды
/promptПоказать текущий системный промпт
/prompt1 [text]Показать или обновить системный промпт проекта
/price_url [url]Показать или установить URL-подсказку о ценах
/stats [period]Показать статистику чатов (напр. 1 week, 3 days)
/infoПоказать информацию о проекте

Команды пользовательского чата

КомандаОписание
/startСбросить состояние диалога и показать приветствие проекта
/debugПереключить режим отладки (добавляет трейс-информацию к ответам бота)
Схема базы данных

Три миграции в migrations/:

001_initial.sql

CREATE TABLE projects (
  id          UUID PRIMARY KEY,
  name        TEXT UNIQUE,
  tg_lead_group BIGINT,
  tg_api_key  TEXT UNIQUE,
  prompts     JSONB,
  language    TEXT DEFAULT 'ru'
);

CREATE TABLE chat_history (
  id          UUID PRIMARY KEY,
  chat_id     BIGINT,
  project_id  UUID REFERENCES projects(id),
  question    TEXT,
  reply       TEXT,
  dbinfo      JSONB,
  created_at  TIMESTAMPTZ
);

CREATE TABLE chat_states (
  chat_id     BIGINT,
  project_id  UUID REFERENCES projects(id),
  state       JSONB,
  PRIMARY KEY (chat_id, project_id)
);

002_bm25_index.sql

CREATE TABLE bm25_indexes (
  project_id  UUID PRIMARY KEY REFERENCES projects(id),
  vocab       JSONB,
  idf         JSONB,
  avg_dl      FLOAT,
  num_docs    INT,
  updated_at  TIMESTAMPTZ
);

003_timers.sql

CREATE TABLE timers (
  chat_id     BIGINT,
  project_id  UUID REFERENCES projects(id),
  step        INT,
  trigger_at  TIMESTAMPTZ,
  PRIMARY KEY (chat_id, project_id)
);

Ключевые индексы

  • idx_chat_history_chat_id на (chat_id, created_at) — поиск истории
  • idx_chat_states_project на (project_id) — запросы на уровне проекта
  • idx_chat_states_state_gin GIN на (state) — JSONB-запросы по состоянию

Отправка лидов в CRM

При отправке лида в Telegram-группу проекта передаётся следующее.

1
Пересылка последнего сообщения — последнее сообщение пользователя пересылается, чтобы менеджеры могли кликнуть на профиль и начать прямой диалог.
2
Сообщение-резюме — сгенерированное LLM резюме, содержащее: ID лида, имя, город, услугу, способ связи, переданный контакт (если есть), статус клиента (finished/hot/cold), количество файлов.
3
Документ истории чата — полная транскрипция Q/A с временными метками, загруженная как lead{chatID}.txt в ответ на сообщение-резюме.
4
Медиафайлы — сгруппированные по типу: фото+видео как альбом, документы как альбом, голосовые/видео-заметки поштучно. Всё в ответ на резюме.
Хранение контактов: Контакты извлекаются один раз и переиспользуются между ходами. Контакты, переданные через кнопку Telegram, хранятся в состоянии чата (не в истории) и включаются в резюме лида при отправке.
Дизайн интерфейса CRM
type CRM interface {
    SendLead(ctx context.Context, lead domain.Lead) (string, error)
    Name() string
}

Текущая реализация: TelegramCRM. Интерфейс спроектирован для будущих CRM-бэкендов (Bitrix, AmoCRM) через паттерн диспетчера MultiCRM.

Развёртывание

Нативное развёртывание на NixOS с усилением безопасности systemd, секретами sops-nix и Cloudflare Tunnel.

Цепочка зависимостей сервисов

postgresql
aichat-migrate
aichat-bot
qdrant
(также зависит)
aichat-bot

Усиление безопасности

ПараметрЗначениеНазначение
NoNewPrivilegestrueПредотвращение повышения привилегий
ProtectSystemstrictФайловая система только для чтения, кроме разрешённых путей
ProtectHometrueНет доступа к /home
PrivateTmptrueИзолированный /tmp
MemoryMax512MЛимит памяти (защита от OOM)
Конфигурация модуля NixOS
services.aichat = {
  enable = true;
  configFile = "/run/secrets/aichat-config.json";
  listenAddr = ":8080";

  # База данных
  enablePostgres = true;
  dbName = "aichat";
  postgresPort = 5432;

  # Qdrant
  enableQdrant = true;
  qdrantPort = 6333;
  qdrantDataDir = "/var/lib/qdrant";

  # Резервное копирование
  enableBackup = true;
  backupDir = "/var/backup/aichat";
  backupRetentionDays = 14;
};
Резервное копирование и секреты

Резервное копирование

  • Ежедневный systemd-таймер с RandomizedDelaySec=1h
  • pg_dump + снимки Qdrant
  • 14-дневное хранение с автоматической очисткой

Секреты (sops-nix)

  • Зашифрованы в secrets/production.yaml с использованием age-ключей
  • Расшифровываются при загрузке на целевой машине с помощью SSH-ключа хоста
  • Ключи: aichat_config (полный JSON-конфиг), cloudflared_creds (учётные данные туннеля)
Команды быстрого развёртывания
# Развернуть изменения кода в продакшн
make prod-deploy

# SSH-туннели для доступа краулера к БД + Qdrant продакшна
make prod-tunnel
# PostgreSQL на localhost:15432, Qdrant на localhost:16333

# Заполнить проект на продакшне (при открытом туннеле)
./bin/aichat-crawler --config /tmp/config.prod-tunnel.json init-project \
  --name myproject --tg-api-key "TOKEN" --language ru \
  --no-llm-chunk --workers 1 --urls "URL1,URL2,..."

# Получить файлы трейсов с продакшна
ssh root@server "cat /var/lib/aichat/traces/{chatID}/{dialogID}.log"
Настройка webhook

После запуска бота и его доступности по HTTPS зарегистрируйте webhook в Telegram:

curl -X POST "https://api.telegram.org/bot{TG_API_KEY}/setWebhook" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-domain.com/webhook/{TG_API_KEY}"}'

URL webhook должен точно соответствовать https://{host}/webhook/{tg_api_key}, где tg_api_key — токен бота из таблицы projects. API-ключ в URL маршрутизирует входящие обновления к нужному бандлу проекта.

Ручное развёртывание / Docker

Требования: Go 1.22+, PostgreSQL 16, Qdrant

# Собрать все бинарники
make build
# создаёт bin/aichat-bot, bin/aichat-crawler, bin/aichat-cli

# Запустить миграции
make migrate DATABASE_URL="postgres://user:pass@localhost/aichat"

# Запустить бота с JSON-конфигом
bin/aichat-bot --config config.prod.json

# Или с переменными окружения
DATABASE_URL="postgres://user:pass@localhost/aichat" \
OPENAI_API_KEYS="sk-xxx" \
bin/aichat-bot
Настройка Cloudflare Tunnel

В продакшне используется Cloudflare Tunnel для маршрутизации HTTPS-трафика к боту без открытия портов. Туннель направляет aichat.example.com на localhost:8080 на сервере.

  • Учётные данные туннеля управляются через sops-nix (зашифрованы в покое)
  • Настроен в nix/production.nix
  • Нет необходимости в TLS-сертификатах или настройке обратного прокси
  • Webhook Telegram указывают на URL туннеля

Локализация

Пользовательские строки и шаблоны промптов LLM локализованы через JSON-файлы локалей, встроенные при компиляции.

АспектДетали
Поддерживаемые языкиru (по умолчанию), en
Файлы локалейinternal/i18n/locales/ru.json, internal/i18n/locales/en.json
APIi18n.T(lang, key) для строк, i18n.Tf(lang, key, args...) для форматированных строк
Цепочка fallbackЗапрошенный язык → "ru" → сам ключ
Покрытие36+ ключей: ответы агента, промпты LLM, форматирование истории, сообщения таймера, форматирование лидов CRM, промпты чанкера
По проектамУстанавливается в столбце projects.language, передаётся агенту, CRM и таймеру
Добавление локалиСоздайте internal/i18n/locales/{code}.json с теми же ключами — автозагрузка через //go:embed

Мониторинг

Метрики Prometheus доступны по GET /metrics на внутреннем сервере (localhost:9090 по умолчанию). Метрики применяются через декорирование — основные пакеты не импортируют Prometheus.

Метрики LLM

МетрикаТипМеткиОписание
aichat_llm_requests_totalcounterprovider, method, statusВсего вызовов LLM API
aichat_llm_request_duration_secondshistogramprovider, methodЗадержка вызовов LLM (бакеты 0.1-51 с)

Метрики Telegram

МетрикаТипМеткиОписание
aichat_telegram_requests_totalcountermethod, statusВызовы Telegram API
aichat_telegram_request_duration_secondshistogrammethodЗадержка Telegram API

Метрики Webhook и агента

МетрикаТипМеткиОписание
aichat_webhook_requests_totalcounterproject, statusВходящие webhook-запросы
aichat_webhook_request_duration_secondshistogramprojectВремя обработки webhook
aichat_agent_messages_totalcounterproject, stageОбработано сообщений по этапам

Метрики таймеров и CRM

МетрикаТипМеткиОписание
aichat_timer_active_sequencesgaugeТекущие активные последовательности таймеров
aichat_timer_fires_totalcounteractionРезультаты срабатываний таймера (follow_up, lead, skip)
aichat_crm_leads_totalcounterproject, statusЛиды, отправленные в CRM
Рекомендации по алертам
УсловиеPromQL-запрос
Всплеск ошибок LLMrate(aichat_llm_requests_total{status="error"}[5m]) > 0.1
Высокая задержка LLMhistogram_quantile(0.95, rate(aichat_llm_request_duration_seconds_bucket[5m])) > 30
Ошибки webhookrate(aichat_webhook_requests_total{status="error"}[5m]) > 0.05
Зависшие таймерыrate(aichat_timer_fires_total[1h]) == 0 при aichat_timer_active_sequences > 0
Ошибки CRMrate(aichat_crm_leads_total{status="error"}[5m]) > 0
Советы по дашборду Grafana
  • Группируйте метрики LLM по provider для сравнения производительности и частоты ошибок провайдеров
  • Отслеживайте aichat_agent_messages_total по stage, чтобы видеть конверсию воронки (general → service_specific → final)
  • Следите за aichat_timer_active_sequences как индикатором общего числа активных диалогов
  • Используйте aichat_webhook_request_duration_seconds p95, чтобы замечать медленные ответы (типично: 2-5 с, включая LLM + поиск)
  • Мониторьте aichat_crm_leads_total по project для отслеживания генерации лидов по арендаторам
Справочник меток
МеткаЗначенияИспользуется в
provideropenai, claude, together, openrouter, tieredМетрики LLM
methodcomplete, embed (LLM); sendMessage, sendMediaGroup и др. (Telegram)Метрики LLM, Telegram
statusok, errorВсе метрики-счётчики
projectСтрока имени проектаМетрики Webhook, агента, CRM
stageactive (активный диалог) или final (лид передан)Метрики агента
actionfollow_up, lead, skipМетрики таймера

Справочник конфигурации

Конфигурация через JSON-файл (--config config.json) или переменные окружения (fallback). Для продакшна рекомендуется JSON-конфиг.

Аннотированный config.example.json

{
  // Строка подключения PostgreSQL
  "database_url": "postgresql://user:pass@localhost/aichat",

  // Векторная база данных Qdrant
  "qdrant": {
    "url": "http://localhost:6333",
    "api_key": ""
  },

  // Публичный HTTP-сервер (webhook)
  "listen_addr": ":8080",
  // Внутренний HTTP-сервер (health, метрики, register-project)
  "internal_addr": "localhost:9090",
  // Базовый URL для регистрации webhook (в продакшне должен быть HTTPS)
  "webhook_url": "https://bot.example.com",

  // LLM-провайдеры: любой OpenAI-совместимый API
  "providers": {
    "openrouter": {
      "keys": ["sk-or-v1-your-key"],
      "base_url": "https://openrouter.ai/api/v1"
    },
    "together": {
      "keys": ["your-together-key"],
      "base_url": "https://api.together.xyz/v1"
    }
  },

  // Слоты моделей: формат "provider/model" (разделение по первому /)
  "models": {
    "toolcaller": "openrouter/z-ai/glm-5.1",                                // агентский tool-calling цикл
    "extract":    "openrouter/mistralai/mistral-small-3.1-24b-instruct",    // темы краулера, классификация в таймере
    "embedding":  "openrouter/qwen/qwen3-embedding-8b"                       // векторы
  },

  // Настройки краулера
  "crawler": {
    "chunk_size": 1500,       // символов на чанк (простой режим)
    "workers": 4,             // параллельные воркеры краулера
    "no_llm_chunk": false,     // true = по абзацам, false = семантический LLM
    "embedding_dim": 1024     // размерность Matryoshka-усечения
  },

  "debug": false
}
Fallback на переменные окружения
ПеременнаяОбязательнаяПо умолчаниюОписание
DATABASE_URLДаСтрока подключения PostgreSQL
OPENAI_API_KEYSХотя бы однаAPI-ключи OpenAI через запятую
CLAUDE_API_KEYSAPI-ключи Claude через запятую
QDRANT_URLНетhttp://localhost:6333URL REST API Qdrant
QDRANT_API_KEYНетКлюч аутентификации Qdrant
LISTEN_ADDRНет:8080Адрес публичного HTTP-сервера
INTERNAL_ADDRНетlocalhost:9090Адрес внутреннего HTTP-сервера
DEBUGНетВключить отладочное логирование (JSON)
AGENT_TRACEНетУстановить 1 для файлов трейсов по диалогам
Слоты моделей: подробности

Идентификаторы моделей используют формат "provider/model". Имя провайдера отделяется по первому символу / и сопоставляется с картой providers.

СлотКлюч конфигаНазначениеПримечания
toolcallermodels.toolcallerАгентский tool-calling циклОБЯЗАТЕЛЬНО tool-capable модель. Протестировано в проде: anthropic/claude-sonnet-4-6, openai/gpt-5.4, openrouter/z-ai/glm-5.1 (текущий дефолт), openrouter/z-ai/glm-4.6.
extractmodels.extractТемы чанков краулера, классификация статуса в таймере, текст follow-upБолее лёгкая, быстрая модель. Одношотовый completion.
embeddingmodels.embeddingВекторные эмбеддинги для поискаПакетный API, Matryoshka-усечение до настроенной размерности

Ограничение модели extract: Должна возвращать контент в поле choices[].message.content. Модели с thinking/reasoning, использующие поле reasoning, не подходят для слота extract.

Справочник ключевых порогов

Все настраиваемые и захардкоженные константы, управляющие поведением системы.

Пороги агента

КонстантаЗначениеНазначениеОпределена в
Лимит итераций tool-loop8Макс. число round-trip ToolCaller за один ход пользователя до fallback на стандартный safety-ответinternal/agent/toolagent.go
hybrid_search top-k3Жёсткое серверное ограничение количества результатов для модели независимо от запрошенного top_kinternal/agent/toolagent.go
minSearchScore0.5Минимальный DBSF-скор для включения результатаinternal/search/hybrid.go

Интервалы таймера

ШагЗадержкаПоведение
15 минутПервая проверка — агент сам решает, ответить или промолчать
215 минутВторая проверка
340 минутТретья проверка
424 часаПоследний интервал — страховка. Лид передаётся безусловно для любого незавершённого чата (независимо от client_status), чтобы ни один лид не потерялся.

Константы краулера

КонстантаЗначениеНазначениеОпределена в
defaultEmbeddingDim1536Размерность эмбеддинга по умолчанию (text-embedding-3-small)internal/crawler/indexer.go
upsertBatch50Точек на пакет upsert в Qdrantinternal/crawler/indexer.go
Размер чанка по умолчанию1500 символовЛимит символов в режиме простого чанкингаinternal/crawler/chunker.go
Размер пакета эмбеддинга32 чанкаЧанков на вызов API эмбеддингаinternal/crawler/pipeline.go
Воркеры по умолчанию3Параллельные воркеры краулераcmd/crawler/main.go

Температуры LLM

ТемператураИспользуется для
0.1Семантический чанкинг (краулер)
0.3Дефолт ToolCaller; классификация статуса в таймере; генерация follow-up

Некоторые tool-capable модели (GPT-5, o-серия) игнорируют явную температуру и используют фиксированное значение — OpenAI tools-бэкенд обрабатывает это автоматически.

LLM-провайдер

КонстантаЗначениеНазначение
Макс. задержка60 секундПотолок экспоненциальной задержки при retry 429
Формула задержки2^attempt + 10% jitterЭкспоненциальная с jitter, учитывает заголовок Retry-After
Retry на цепочку3Макс. retry при 429 до переключения на следующего провайдера

Telegram

КонстантаЗначениеНазначение
Макс. длина сообщения4096 символовЛимит API Telegram; сообщения автоматически разбиваются по этой границе
Retry при 429ДаУчитывает заголовок Retry-After от API Telegram

Справочник производительности

2-5 с
Задержка Webhook
~500 мс
Поиск (Qdrant)
~1 КБ
На ход
~8 КБ
На вектор

Задержка webhook включает один или несколько round-trip ToolCaller плюс поиск. Задержка поиска покрывает DBSF-слияние Qdrant. Оценки хранения: ~1 КБ на ход диалога, ~100 КБ на 10К чанков, ~8 КБ на плотный вектор (1024 измерения).

Команды сборки и тестирования
# Собрать все три бинарника
make build        # создаёт bin/aichat-bot, bin/aichat-crawler, bin/aichat-cli

# Запустить тесты
make test         # go test ./...

# Линтер
make lint         # go vet ./...

# Локальные сервисы для разработки (PostgreSQL + Qdrant через Docker)
make dev-services

# Запустить CLI-интерфейс для тестирования (без Telegram)
bin/aichat-cli --config config.dev.json --project myproject

aichat-go

Мультитенантная платформа AI-бота для продаж
Построена на Go, Qdrant, PostgreSQL и OpenRouter

3 точки входа: bot, crawler, cli 4 инструмента: hybrid_search, set_state, get_state, send_lead 2 локали (ru/en)

Документация сгенерирована из исходного кода и файлов docs/*.md.