Возможности 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:
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:
type FAQSearchRequest struct {
QueryText string `binding:"required"`
VectorThreshold float64 // порог векторного сходства (по умолчанию 0.7)
MatchCount int // число возвращаемых (по умолчанию 10, максимум 50)
FirstPriorityTagIDs []int64 // теги первого приоритета (результаты — в начало)
SecondPriorityTagIDs []int64 // теги второго приоритета
OnlyRecommended bool // возвращать только рекомендуемые элементы
}Процесс совпадения:
- Гибридный отзыв: после нормализации текста запроса выполняется векторный поиск + поиск по ключевым словам BM25 с объединением и дедупликацией;
- Двухуровневый приоритет тегов: элементы, совпавшие с
FirstPriorityTagIDs, идут первыми, затемSecondPriorityTagIDs; - Фильтрация негативных примеров (
filterByNegativeQuestions): если текст запроса полностью совпадает с любым антипримерным вопросом элемента (сравнение в нижнем регистре) → этот элемент исключается из результатов. Типичный случай: пользователь спрашивает «не поддерживается ли X», чтобы не вернуть элемент «поддерживается X»; - Итеративный отзыв (
applyFAQPostProcessing): когда после фильтрации число уникальных элементов меньшеmatch_countи какой-либо список векторных результатов заполнен до предела, запускаетсяiterativeRetrieveWithDeduplication— не более 5 итераций, TopK на каждой удваивается (начиная с удвоенной глубины первого раунда, с потолком 500, остановка при достижении потолка), каждый раунд оценивается так же, как первый, с кешем фильтрации негативных примеров, и при незаполненности всех списков происходит досрочное завершение; - Результат сопровождается
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):
// 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
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_mode | question_only / question_answer | question_answer | Включает ли индексируемое содержимое ответ |
question_index_mode | combined / separate | combined | Стандартный вопрос + похожие вопросы как один индексный элемент, либо каждый вопрос как отдельный индексный элемент |
В режиме 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 использует многоуровневую схему «хранить исходный текст, сравнивать по нормализованному тексту»:
// Запись: в 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)
| Измерение | FAQ | Document |
|---|---|---|
| Тип KB | faq | document |
| Knowledge.Type | faq (обычно один агрегирующий Knowledge на базу) | файл / manual / URL |
| Источник Chunk | Пользователь вводит структурированные элементы напрямую | Разборщик разбивает автоматически |
| Chunk.ChunkType | faq | text / image_ocr / summary и т. п. |
| Metadata | FAQChunkMetadata (вопрос / ответ / антипример / стратегия) | Метаданные документа (сгенерированные ИИ вопросы и т. п.) |
| Chunk.Content | Формируется buildFAQChunkContent: "Q: стандартный вопрос\nSimilar Questions:\n- ..."; в режиме question_answer добавляются Answers; антипримерные вопросы никогда не пишутся в Content (не участвуют в индексации) | Фрагмент исходного текста |
| ContentHash | Нормализованный хеш дедупликации (ключевой механизм) | Обычно не используется |
| Гранулярность индексации | Один или несколько индексных элементов согласно question_index_mode | Один индексный элемент на chunk (разбиение родитель-потомок считается отдельно) |
| Конвейер обработки | Синхронное создание / асинхронный массовый импорт, индексация применяется сразу | Асинхронный конвейер разбора DocReader |
| Постобработка поиска | Фильтрация негативных примеров + итеративный отзыв (см. стратегию совпадения при поиске) | Обычное объединение и переранжирование |
| Переключатели состояния | is_enabled + is_recommended (Flags) + answer_strategy | enable_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 Handler | internal/handler/faq.go |
| Сервис CRUD / экспорта элементов | internal/application/service/knowledge_faq.go |
| Сервис асинхронного импорта | internal/application/service/knowledge_faq_import.go |
| Клонирование / синхронизация | internal/application/service/faq_clone_sync.go |
| Постобработка поиска FAQ | internal/application/service/knowledgebase_search_faq.go |
| Конфигурация FAQ уровня KB | internal/types/knowledgebase.go (FAQConfig) |
