Skip to content

Возможности FAQ ​

База знаний типа FAQ предназначена для ведения стандартных вопросов и ответов: политика возврата, порядок возмещения расходов, типовые сбои и т. п. Каждый элемент содержит стандартный вопрос, похожие вопросы, вопросы-антипримеры и ответ; при поиске выполняется сопоставление с вопросами, а ответ возвращается согласно стратегии совпадения.

Создав базу знаний типа FAQ, можно массово импортировать вопросы и ответы через Excel или CSV, а затем дополнить похожие и антипримерные вопросы. Базу FAQ можно использовать вместе с базой документов для поиска агентом; при включённом приоритете FAQ и выполнении порога прямого ответа можно сразу вернуть стандартный ответ.

Управление элементами, импорт и настройка поиска описаны ниже, а механизмы дедупликации и синхронизации вынесены в справочный раздел.

Создание и ведение вопросов и ответов ​

Создав базу знаний FAQ, для каждого элемента заполните стандартный вопрос и ответ. Похожие вопросы служат для охвата разных формулировок, а вопросы-антипримеры — для исключения легко ложно сопоставляемых вопросов. У одного элемента может быть несколько ответов, при этом выбирается либо возврат всех, либо случайного одного.

Элементы можно вести по категориям, отдельно отключать пока не используемые вопросы и ответы, а также запрещать показ элемента в рекомендациях. Категория FAQ является одиночным тегом и ведётся отдельно от механизма нескольких тегов в базе документов.

Импорт и экспорт ​

С помощью шаблона подготовьте стандартный вопрос, похожие вопросы, антипримерные вопросы и ответ, разделяя многозначные поля символом ##. При импорте выбирается режим добавления или замены: добавление сопоставляет по хешу содержимого и объединяет дубликаты; замена удаляет старые элементы и сохраняет только содержимое текущего пакета. Если нужно сначала проверить данные, можно через dry_run в API выполнить только валидацию.

Результат импорта различает успех, неудачу, частичную неудачу и пропуск, а также предоставляет причину неудачи. Экспорт в CSV и JSON можно использовать для резервного копирования, редактирования и повторного импорта.

Проверка результатов совпадения ​

Поиск по FAQ учитывает одновременно вектор, ключевые слова и приоритет тегов, а элементы с полным совпадением вопроса-антипримера исключаются. Результат сопровождается совпавшим вопросом, оценкой и типом совпадения, что помогает корректировать похожие вопросы, антипримеры и пороги.

Справочник по импорту и поиску ​

Массовый импорт ​

internal/application/service/knowledge_faq_import.go. Точка входа POST /knowledge-bases/:id/faq/entries:

go
type FAQBatchUpsertPayload struct {
    Entries     []FAQEntryPayload `json:"entries" binding:"required"` // также можно через EntriesURL вытянуть из объектного хранилища
    Mode        string            `json:"mode" binding:"oneof=append replace"`
    KnowledgeID string            `json:"knowledge_id"`
    TaskID      string            `json:"task_id"` // необязательно; если не передан — генерируется автоматически; пользовательское значение допускает только [A-Za-z0-9_-], ≤128 символов
    DryRun      bool              `json:"dry_run"` // только проверка, без записи в БД
}

Поля импорта (столбцы шаблона CSV, симметричны формату экспорта, многозначные значения разделяются ##): стандартный вопрос (обязательно), похожие вопросы, антипримерные вопросы, ответ (обязательно), возвращать ли всё, отключён ли, запрещён ли для рекомендаций, категория (по умолчанию «Без категории»).

Статистические поля объекта прогресса FAQImportProgress: success_count / failed_count / partial_failed_count (похожие или антипримерные вопросы исключены, но элемент всё же импортирован) / skipped_count (пропущен как дубликат) / merged_count / added_count, failed_entries[] (с причиной неудачи и исходным содержимым) и failed_entries_url, import_mode, processing_time; состояния задачи pending → processing → completed / failed.

Экспорт поддерживает два формата: CSV (столбцы: категория, вопрос, похожие вопросы, антипримерные вопросы, ответ бота, возвращать ли всё, отключён ли, запрещён ли для рекомендаций; с BOM для совместимости с UTF-8 в Excel) и JSON (FAQExportEntry, совместим с payload импорта, поддерживает цикл «экспорт → редактирование → повторный импорт»).

Стратегия совпадения при поиске ​

SearchFAQ из internal/handler/faq.go + internal/application/service/knowledgebase_search_faq.go:

go
type FAQSearchRequest struct {
    QueryText            string  `binding:"required"`
    VectorThreshold      float64 // порог векторного сходства (по умолчанию 0.7)
    MatchCount           int     // число возвращаемых (по умолчанию 10, максимум 50)
    FirstPriorityTagIDs  []int64 // теги первого приоритета (результаты — в начало)
    SecondPriorityTagIDs []int64 // теги второго приоритета
    OnlyRecommended      bool    // возвращать только рекомендуемые элементы
}

Процесс совпадения:

  1. Гибридный отзыв: после нормализации текста запроса выполняется векторный поиск + поиск по ключевым словам BM25 с объединением и дедупликацией;
  2. Двухуровневый приоритет тегов: элементы, совпавшие с FirstPriorityTagIDs, идут первыми, затем SecondPriorityTagIDs;
  3. Фильтрация негативных примеров (filterByNegativeQuestions): если текст запроса полностью совпадает с любым антипримерным вопросом элемента (сравнение в нижнем регистре) → этот элемент исключается из результатов. Типичный случай: пользователь спрашивает «не поддерживается ли X», чтобы не вернуть элемент «поддерживается X»;
  4. Итеративный отзыв (applyFAQPostProcessing): когда после фильтрации число уникальных элементов меньше match_count и какой-либо список векторных результатов заполнен до предела, запускается iterativeRetrieveWithDeduplication — не более 5 итераций, TopK на каждой удваивается (начиная с удвоенной глубины первого раунда, с потолком 500, остановка при достижении потолка), каждый раунд оценивается так же, как первый, с кешем фильтрации негативных примеров, и при незаполненности всех списков происходит досрочное завершение;
  5. Результат сопровождается score, match_type, matched_question (что именно совпало — стандартный вопрос или какой похожий вопрос), а ответ возвращается согласно answer_strategy (all / random).

Если в области поиска нет базы FAQ, эта постобработка просто пропускается, и обычный гибридный поиск не затрагивается. Пока в области есть база FAQ (даже если основная KB — база документов, или база FAQ пришла из общего доступа организации), фильтрация негативных примеров действует; итеративный отзыв запускается только когда основная KB — база FAQ. Цепочка поиска агента для баз FAQ также проходит через этот путь постобработки.

Справочник по элементам и интерфейсам ​

Модель данных ​

Форма хранения: элемент FAQ = один Chunk ​

Каждый элемент FAQ соответствует одной записи Chunk (chunk_type = "faq") и принадлежит Knowledge типа faq в базе знаний (этот Knowledge создаётся автоматически при создании первого элемента). Структурированное содержимое элемента хранится в Chunk.Metadata (JSON):

go
// internal/types/faq.go
type FAQChunkMetadata struct {
    StandardQuestion  string         `json:"standard_question"`
    SimilarQuestions  []string       `json:"similar_questions,omitempty"`
    NegativeQuestions []string       `json:"negative_questions,omitempty"` // вопрос-антипример: при совпадении — отфильтровать
    Answers           []string       `json:"answers,omitempty"`
    AnswerStrategy    AnswerStrategy `json:"answer_strategy,omitempty"`    // all | random
    Version           int            `json:"version,omitempty"`            // увеличивается при каждом обновлении
    Source            string         `json:"source,omitempty"`
}

const (
    AnswerStrategyAll    AnswerStrategy = "all"    // вернуть все ответы
    AnswerStrategyRandom AnswerStrategy = "random" // вернуть один случайный
)

Общие поля, переиспользуемые в Chunk: SeqID (целое с автоинкрементом, ID элемента во внешнем API), TagID (тег категории, постоянная имени тега по умолчанию UntaggedTagName = "Без категории"), IsEnabled (переключатель отключения), Flags (bit0 ChunkFlagRecommended — можно ли рекомендовать), ContentHash (хеш дедупликации, см. нормализацию и хеш содержимого (ядро дедупликации)).

Проекция API: FAQEntry ​

go
type FAQEntry struct {
    ID                int64          `json:"id"`        // chunk.SeqID
    ChunkID           string         `json:"chunk_id"`
    KnowledgeID       string         `json:"knowledge_id"`
    KnowledgeBaseID   string         `json:"knowledge_base_id"`
    TagID             int64          `json:"tag_id"`
    TagName           string         `json:"tag_name"`
    IsEnabled         bool           `json:"is_enabled"`
    IsRecommended     bool           `json:"is_recommended"`
    StandardQuestion  string         `json:"standard_question"`
    SimilarQuestions  []string       `json:"similar_questions"`
    NegativeQuestions []string       `json:"negative_questions"`
    Answers           []string       `json:"answers"`
    AnswerStrategy    AnswerStrategy `json:"answer_strategy"`
    IndexMode         FAQIndexMode   `json:"index_mode"`
    Score             float64        `json:"score,omitempty"`            // оценка поиска
    MatchType         MatchType      `json:"match_type,omitempty"`
    MatchedQuestion   string         `json:"matched_question,omitempty"` // текст фактически совпавшего вопроса
}

Конфигурация FAQ уровня KB (FAQConfig) ​

КонфигурацияЗначенияПо умолчаниюОписание
index_modequestion_only / question_answerquestion_answerВключает ли индексируемое содержимое ответ
question_index_modecombined / separatecombinedСтандартный вопрос + похожие вопросы как один индексный элемент, либо каждый вопрос как отдельный индексный элемент

В режиме separate для каждого похожего вопроса формируется отдельный индексный элемент, SourceID = fmt.Sprintf("%s-%s", chunk.ID, hashQuestion(similarQ)), что поддерживает тонкое добавление и удаление на уровне похожих вопросов.

Эндпоинты API ​

internal/handler/faq.go (маршруты регистрируются в internal/router/routes_knowledge.go, контроль доступа к KB такой же, как у базы знаний: чтение через KBAccessRead, запись через KBAccessWrite; API Key требует возможностей ingest / retrieve):

МетодПутьФункция
GET/knowledge-bases/:id/faq/entriesСписок элементов (пагинация / тег / ключевое слово / статус включения)
GET/knowledge-bases/:id/faq/entries/:entry_idДетали одной записи
POST/knowledge-bases/:id/faq/entryСинхронное создание одной записи
PUT/knowledge-bases/:id/faq/entries/:entry_idОбновление одной записи (инкрементальная индексация)
POST/knowledge-bases/:id/faq/entriesМассовый импорт / обновление (асинхронно, append/replace)
POST/knowledge-bases/:id/faq/entries/:entry_id/similar-questionsДобавить похожие вопросы
PUT/knowledge-bases/:id/faq/entries/fieldsМассовое обновление полей (включение / рекомендация / стратегия)
PUT/knowledge-bases/:id/faq/entries/tagsМассовое обновление тегов
DELETE/knowledge-bases/:id/faq/entriesМассовое удаление
POST/knowledge-bases/:id/faq/searchПоиск по FAQ (гибридный поиск)
GET/knowledge-bases/:id/faq/entries/exportЭкспорт (CSV / JSON)
GET/faq/import/progress/:task_idПрогресс задачи импорта
PUT/knowledge-bases/:id/faq/import/last-result/displayСостояние отображения панели результатов импорта (open/close)

Параметры запроса списка: page / page_size, tag_id (seq_id тега, совместим со старым одиночным тегом) или tag_ids (UUID тегов через запятую, семантика OR), keyword + search_field (standard_question / similar_questions / answers, по умолчанию поиск по всем), sort_order (asc, по умолчанию сортировка по времени обновления в обратном порядке), is_enabled (true / false — фильтр по статусу включения, без параметра возвращаются все).

Валидация при записи (sanitizeFAQEntryPayload + checkFAQQuestionDuplicate): стандартный вопрос обязателен; ответ минимум один; answer_strategy может быть только all / random (по умолчанию all); похожие / антипримерные вопросы / ответы очищаются от пробелов и дубликатов; выполняется четырёхуровневая проверка на дубликаты — похожие вопросы против стандартного вопроса, взаимная проверка похожих вопросов, антипримеры против стандартного и похожих вопросов, конфликты между элементами в БД (с возвратом подробной информации о конфликте).

Нормализация и хеш содержимого (ядро дедупликации) ​

FAQ использует многоуровневую схему «хранить исходный текст, сравнивать по нормализованному тексту»:

go
// Запись: в DB хранятся исходные данные, ContentHash рассчитывается по нормализованной копии
func (c *Chunk) SetFAQMetadata(meta *FAQChunkMetadata) error {
    meta.Sanitize()                          // только базовая очистка
    c.Metadata, _ = json.Marshal(meta)
    normalized := meta.Normalize()           // нормализованная копия
    c.ContentHash = CalculateFAQContentHash(normalized)
    return nil
}

Цепочка обработки NormalizeQuestion (порядок важен): удаление пробелов в начале и конце → удаление URL → приведение к нижнему регистру → удаление крайних знаков препинания (?。,;、:!?.,;!:'" и т. п.) → преобразование традиционных иероглифов в упрощённые → преобразование полноширинных символов в полуширинные → умные пробелы (пробелы между иероглифами удаляются, между английскими буквами / цифрами сохраняются).

CalculateFAQContentHash = SHA256(нормализованный стандартный вопрос + отсортированные похожие вопросы + отсортированные антипримеры + отсортированные ответы). Файл internal/types/faq_test.go закрепляет ключевые инварианты хеша: нечувствительность к регистру / пунктуации, нечувствительность к традиционному и упрощённому написанию, нечувствительность к полной и половинной ширине, нечувствительность к порядку массива, согласованность путей записи и чтения. Этот хеш используется для дедупликации при импорте и для сопоставления элементов при клонировании и синхронизации.

Отличия от обычного знания (Document) ​

ИзмерениеFAQDocument
Тип KBfaqdocument
Knowledge.Typefaq (обычно один агрегирующий Knowledge на базу)файл / manual / URL
Источник ChunkПользователь вводит структурированные элементы напрямуюРазборщик разбивает автоматически
Chunk.ChunkTypefaqtext / image_ocr / summary и т. п.
MetadataFAQChunkMetadata (вопрос / ответ / антипример / стратегия)Метаданные документа (сгенерированные ИИ вопросы и т. п.)
Chunk.ContentФормируется buildFAQChunkContent: "Q: стандартный вопрос\nSimilar Questions:\n- ..."; в режиме question_answer добавляются Answers; антипримерные вопросы никогда не пишутся в Content (не участвуют в индексации)Фрагмент исходного текста
ContentHashНормализованный хеш дедупликации (ключевой механизм)Обычно не используется
Гранулярность индексацииОдин или несколько индексных элементов согласно question_index_modeОдин индексный элемент на chunk (разбиение родитель-потомок считается отдельно)
Конвейер обработкиСинхронное создание / асинхронный массовый импорт, индексация применяется сразуАсинхронный конвейер разбора DocReader
Постобработка поискаФильтрация негативных примеров + итеративный отзыв (см. стратегию совпадения при поиске)Обычное объединение и переранжирование
Переключатели состоянияis_enabled + is_recommended (Flags) + answer_strategyenable_status

Обновление элемента идёт через инкрементальную индексацию (incrementalIndexFAQEntry): переэмбеддинг выполняется только для изменившейся части — при изменении стандартного вопроса выполняется реиндексация; похожие вопросы попарно диффятся и добавляются/удаляются; изменение ответа запускает реиндексацию только в режиме question_answer; с помощью SourceID точно удаляются недействительные индексные элементы.

Механизм клонирования / синхронизации общего доступа ​

internal/application/service/faq_clone_sync.go. Триггерные сценарии: клонирование базы знаний (copy) и синхронизация содержимого базы знаний общего доступа — FAQ chunk целевой базы, порождённой клонированием, является новой записью, и операционное состояние (включение / рекомендация / теги / стратегия ответа) нужно выровнять с исходной базой:

  • Сопоставление: сопоставление исходных / целевых элементов по ContentHash даёт FAQChunkSyncPair{SrcChunkID, DstChunkID} (нормализованный хеш гарантирует, что различия в традиционном и упрощённом написании / полной и половинной ширине / порядке не нарушают сопоставление, что подтверждает internal/types/faq_sync_test.go);
  • Синхронизируемое содержимое: состояние включения IsEnabled, рекомендательная позиция ChunkFlagRecommended из Flags, принадлежность тегу TagID, стратегия ответа AnswerStrategy;
  • Применение на стороне индекса: после обновления БД массово обновляются флаги enabled / tag / recommended соответствующих индексных элементов в векторном хранилище, и фильтрация поиска вступает в силу сразу (расчёт различий см. в internal/application/repository/chunk_faq_diff_test.go).

Справочник по реализации ​

Все приведённые ниже пути отсчитываются от корня репозитория:

СлойФайл
Тип FAQ и нормализация / хешinternal/types/faq.go (а также faq_test.go, faq_sync_test.go)
FAQ Handlerinternal/handler/faq.go
Сервис CRUD / экспорта элементовinternal/application/service/knowledge_faq.go
Сервис асинхронного импортаinternal/application/service/knowledge_faq_import.go
Клонирование / синхронизацияinternal/application/service/faq_clone_sync.go
Постобработка поиска FAQinternal/application/service/knowledgebase_search_faq.go
Конфигурация FAQ уровня KBinternal/types/knowledgebase.go (FAQConfig)

Версия v0.8.3 · MIT License