Skip to content

Возможности оценки (Evaluation) ​

Оценка использует наборы вопросов-ответов с эталонными ответами, чтобы сравнить эффект от разбиения на блоки, моделей и конфигураций поиска. Система создаёт оценочную базу знаний и импортирует корпус, по каждому вопросу выполняет поиск и генерацию и выводит метрики Precision, Recall, NDCG, MRR, MAP, BLEU и ROUGE.

Использование через API

У оценки пока нет отдельного входа в интерфейсе: запуск через POST /api/v1/evaluation, опрос результата через GET /api/v1/evaluation?task_id=...; для создания нужны права Admin, для запроса — Viewer. Набор данных имеет формат Parquet, требования к формату см. ниже.

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

Запуск одной оценки ​

  1. Используйте встроенный образец набора данных либо подготовьте Parquet-файлы по формату ниже и замените одноимённые файлы в dataset/samples/ рабочего каталога сервиса (в контейнере — /app/dataset/samples/).
  2. Выберите эталонную базу знаний, диалоговую модель и модель реранжирования и создайте задачу через POST /api/v1/evaluation. Эталонная база знаний используется для копирования конфигурации; для оценки применяется отдельная база знаний.
  3. Запишите возвращённый ID задачи и запрашивайте состояние и прогресс через GET /api/v1/evaluation?task_id=....
  4. При успехе задачи сравните метрики поиска и генерации; при сбое сначала проверьте ошибку задачи, затем скорректируйте конфигурацию и запустите снова.

Для создания задачи нужны права Admin, для запроса результата — Viewer; API Key также требует возможности run_evaluations или full-access.

Формат набора данных ​

Сервис набора данных (internal/application/service/dataset.go) загружает из ./dataset/samples/ 5 файлов Parquet:

ФайлSchemaЗначение
queries.parquetid: int64, text: stringНабор вопросов
corpus.parquetid: int64, text: stringАбзацы корпуса (при оценке загружаются в базу знаний)
answers.parquetid: int64, text: stringЭталонные ответы
qrels.parquetqid: int64, pid: int64Связь вопрос → релевантный абзац (ground truth) (основа для метрик поиска)
qas.parquetqid: int64, aid: int64Отображение вопрос → ответ (основа для метрик генерации)

Соответствующие структуры Go:

go
type TextInfo struct {
    ID   int64  `parquet:"id"`
    Text string `parquet:"text"`
}
type RelsInfo struct {
    QID int64 `parquet:"qid"`
    PID int64 `parquet:"pid"`
}
type QaInfo struct {
    QID int64 `parquet:"qid"`
    AID int64 `parquet:"aid"`
}

После загрузки данные собираются в пообразцовые QAPair (internal/types/dataset.go):

go
type QAPair struct {
    QID      int      // ID вопроса
    Question string   // текст вопроса
    PIDs     []int    // ID релевантных абзацев (ground truth)
    Passages []string // текст абзацев
    AID      int      // ID ответа
    Answer   string   // текст эталонного ответа
}

Сервис всегда читает эти 5 файлов из ./dataset/samples/; dataset_id пока используется только для формирования ID задачи и не переключает на другой каталог. При использовании пользовательского набора данных сгенерируйте одноимённые Parquet-файлы по указанной Schema и замените содержимое каталога (при развёртывании в Docker можно смонтировать в /app/dataset/samples/). При загрузке сервис выводит статистику (число вопросов, объём корпуса, среднее число релевантных абзацев, покрытие ответами и т. д.).

Запрос результатов ​

GET /api/v1/evaluation?task_id=<ID задачи, возвращённый при создании>, возвращает EvaluationDetail:

json
{
  "success": true,
  "data": {
    "task": {
      "id": "evaluation_1_1758600000000_1a2b3c4d_default",
      "dataset_id": "default",
      "status": 2,
      "total": 100,
      "finished": 100
    },
    "params": { "...": "снимок параметров оценки ChatManage" },
    "metric": {
      "retrieval_metrics": {
        "precision": 0.85, "recall": 0.92,
        "ndcg3": 0.88, "ndcg10": 0.86,
        "mrr": 0.95, "map": 0.87
      },
      "generation_metrics": {
        "bleu1": 0.72, "bleu2": 0.65, "bleu4": 0.58,
        "rouge1": 0.78, "rouge2": 0.71, "rougel": 0.75
      }
    }
  }
}

Во время выполнения задачи можно опрашивать этот интерфейс для получения прогресса finished / total; при status = 3 в err_msg передаётся причина сбоя.

Внимание: результаты оценки хранятся в памяти (evaluationMemoryStorage: map[string]*EvaluationDetail + sync.RWMutex, см. internal/application/service/evaluation.go); после перезапуска сервиса задачи и результаты теряются, оценку нужно запускать заново.

Список метрик ​

Реестр метрик см. в internal/application/service/metric_hook.go, всего 12 метрик в двух группах. Текст сначала разбивается на токены в metric/common.go: китайский — через Jieba, английский — по пробелам, предложения — по 。 / ..

Метрики поиска (Retrieval Metrics) ​

МетрикаПолеФайл реализацииЗначение
Precisionprecisionmetric/precision.goТочность поиска: число найденных релевантных документов / общее число возвращённых, среднее по множеству GT
Recallrecallmetric/recall.goПолнота поиска: число найденных релевантных документов / общее число релевантных документов
NDCG@3ndcg3metric/ndcg.goНормализованный дисконтированный накопленный выигрыш (по первым 3 позициям); поощряет размещение релевантных документов выше
NDCG@10ndcg10metric/ndcg.goТо же, по первым 10 позициям
MRRmrrmetric/mrr.goСреднее обратного ранга первого релевантного документа: sum(1/rank) / N
MAPmapmetric/map.goСредняя точность: для каждой позиции попадания накапливается Precision@k и затем нормализуется

Ключевое вычисление NDCG (metric/ndcg.go):

go
// DCG = sum((2^rel_i - 1) / log2(i+2)), rel равен 0/1
dcg += (math.Pow(2, float64(relevance)) - 1) / math.Log2(float64(i+2))
// NDCG = DCG / IDCG (DCG идеального порядка)

Ключевое вычисление MRR (metric/mrr.go):

go
for i, predID := range ids {
    if _, ok := gtSet[predID]; ok {
        sumRR += 1.0 / float64(i+1) // обратный ранг первой позиции попадания
        break
    }
}

Метрики генерации (Generation Metrics) ​

МетрикаПолеФайл реализацииЗначение
BLEU-1bleu1metric/bleu.goТочность 1-gram (веса [1.0, 0, 0, 0])
BLEU-2bleu2metric/bleu.go1/2-gram по 50% (веса [0.5, 0.5, 0, 0])
BLEU-4bleu4metric/bleu.go1~4-gram с равными весами ([0.25, 0.25, 0.25, 0.25]), включает brevity penalty
ROUGE-1rouge1metric/rouge.goF1 по перекрытию униграмм
ROUGE-2rouge2metric/rouge.goF1 по перекрытию биграмм
ROUGE-Lrougelmetric/rouge.goF1 по наибольшей общей подпоследовательности (LCS)

Ядро BLEU (metric/bleu.go): взвешенное геометрическое среднее скорректированных точностей n-gram, умноженное на штраф за краткость bp * exp(sum(w_i * log(p_i))). ROUGE берёт F1: F1 = 2PR / (P + R + 1e-8) (metric/rouge_score.go).

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

API ​

internal/router/routes_infra.go:

go
evaluationRoutes := g.apiKeyGroup(r.Group("/evaluation"), apiKeyRunEvaluations(apiKeyFullAccess()))
{
    evaluationRoutes.POST("", g.Admin(), handler.Evaluation)
    evaluationRoutes.GET("", g.Viewer(), handler.GetEvaluationResult)
}
МетодПутьПраваОписание
POST/api/v1/evaluationAdmin (API Key требует возможность run_evaluations или full-access)Создать задачу оценки, сразу возвращает информацию о задаче
GET/api/v1/evaluation?task_id=...ViewerЗапросить состояние, прогресс и результаты метрик задачи

Создание задачи оценки ​

Параметры запроса (internal/handler/evaluation.go):

go
type EvaluationRequest struct {
    DatasetID       string `json:"dataset_id"`        // ID набора данных, по умолчанию "default"
    KnowledgeBaseID string `json:"knowledge_base_id"` // эталонная база знаний (переиспользуется её конфигурация)
    ChatModelID     string `json:"chat_id"`           // диалоговая модель
    RerankModelID   string `json:"rerank_id"`         // модель реранжирования
}
ПараметрОбязательныйПоведение по умолчанию
dataset_idНетПо умолчанию default; используется только для ID задачи, данные всегда читаются из dataset/samples/
knowledge_base_idНетЕсли не указано, создаётся отдельная база знаний для оценки; если указано, её конфигурация копируется в оценочную KB
chat_idНетПо умолчанию автоматически выбирается Chat-модель по умолчанию
rerank_idНетПо умолчанию автоматически выбирается Rerank-модель по умолчанию

ID задачи генерируется utils.GenerateTaskID в формате evaluation_{tenantID}_{метка времени в мс}_{строка из 8 случайных символов}_{datasetID}; при каждом создании он разный, запрашивать нужно по ID из ответа на создание. Объект задачи (internal/types/evaluation.go):

go
type EvaluationTask struct {
    ID        string           `json:"id"`
    TenantID  uint64           `json:"tenant_id"`
    DatasetID string           `json:"dataset_id"`
    StartTime time.Time        `json:"start_time"`
    Status    EvaluationStatue `json:"status"`
    ErrMsg    string           `json:"err_msg,omitempty"`
    Total     int              `json:"total,omitempty"`    // общее число образцов
    Finished  int              `json:"finished,omitempty"` // число завершённых
}

Перечисление состояний задачи (обратите внимание: в исходном коде написано EvaluationStatue):

go
const (
    EvaluationStatuePending EvaluationStatue = iota // 0 ожидает запуска
    EvaluationStatueRunning                          // 1 выполняется
    EvaluationStatueSuccess                          // 2 успех
    EvaluationStatueFailed                           // 3 сбой
)

Процесс оценки ​

В internal/application/service/evaluation.go POST-интерфейс синхронно завершает подготовку и асинхронно выполняет оценку:

  1. Подготовка базы знаний: создаётся (или клонируется по конфигурации эталонной KB) отдельная база знаний для оценки, берутся модели Embedding и LLM по умолчанию;
  2. Сборка параметров: из системной конфигурации собираются параметры оценки ChatManage — VectorThreshold, KeywordThreshold, EmbeddingTopK, RerankTopK, RerankThreshold, MaxRounds, SummaryConfig (MaxTokens / TopK / TopP / RepeatPenalty / Prompt / ContextTemplate и т. д.), FallbackResponse, промпты перезаписи и т. д.;
  3. Регистрация задачи: задача регистрируется в хранилище в памяти под своим ID, состояние Pending, ответ возвращается сразу;
  4. Фоновое выполнение (goroutine): корпус набора данных загружается в оценочную KB → параллельно оценивается каждая пара QA → метрики агрегируются → ресурсы очищаются.

Степень параллелизма берётся как max(GOMAXPROCS - 1, 1) (ограничение errgroup):

go
var g errgroup.Group
metricHook := NewHookMetric(len(dataset))
g.SetLimit(max(runtime.GOMAXPROCS(0)-1, 1))
for i, qaPair := range dataset {
    g.Go(func() error {
        // 1. клонировать конфигурацию ChatManage
        // 2. пройти полный конвейер KnowledgeQAByEvent (поиск + реранжирование + генерация)
        // 3. записать MetricInput (ID найденных passage, сгенерированный текст, GT)
        // 4. под блокировкой обновить прогресс finished
    })
}
g.Wait()

Каждый образец выдаёт один MetricInput (internal/types/evaluation.go):

go
type MetricInput struct {
    RetrievalGT    [][]int // ground truth поиска (список ID релевантных passage)
    RetrievalIDs   []int   // ID passage, фактически возвращённых поиском
    GeneratedTexts string  // текст, сгенерированный моделью
    GeneratedGT    string  // эталонный ответ
}

metric_hook.go для каждого образца проходит по всем зарегистрированным вычислителям метрик и считает оценку, а затем Avg() берёт среднее по всем образцам для каждой метрики и записывает в MetricResult.

Трактовка RetrievalIDs

RetrievalIDs должны быть passage ID из набора данных; нельзя напрямую использовать ChunkIndex результата поиска — последний является лишь порядковым номером блока в базе знаний и не соответствует passage ID, поэтому прямое использование обнулит все метрики поиска. Поэтому recordFinish выполняет двустороннее сопоставление с включением: текст каждого результата поиска сверяется с ground truth passage образца, по нему находится соответствующий pid и выполняется дедупликация. Если результат реранжирования пуст, используется исходный результат поиска, чтобы весь образец не был записан как «ничего не найдено».

Загрузка корпуса также должна синхронно дождаться завершения индексации (CreateKnowledgeFromPassageSync): при асинхронной загрузке запрос оценки выполняется до построения индекса, что также проявляется как постоянные нулевые метрики. Кроме того, список passage выделяется длиной maxPID + 1; pid нумеруется с 0 и включает последний элемент.

Диаграмма процесса оценки ​

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

Все пути ниже указаны относительно корня репозитория:

СлойФайл
HTTP Handlerinternal/handler/evaluation.go
Сервис оценкиinternal/application/service/evaluation.go
Регистрация и агрегация метрикinternal/application/service/metric_hook.go
Реализация метрикinternal/application/service/metric/ (precision.go, recall.go, ndcg.go, mrr.go, map.go, bleu.go, rouge.go, rouge_score.go, common.go)
Загрузка набора данныхinternal/application/service/dataset.go
Определения типовinternal/types/evaluation.go, internal/types/dataset.go
Встроенный образец набора данныхdataset/samples/ (файлы Parquet)
Регистрация маршрутовRegisterEvaluationRoutes из internal/router/routes_infra.go

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