Возможности оценки (Evaluation)
Оценка использует наборы вопросов-ответов с эталонными ответами, чтобы сравнить эффект от разбиения на блоки, моделей и конфигураций поиска. Система создаёт оценочную базу знаний и импортирует корпус, по каждому вопросу выполняет поиск и генерацию и выводит метрики Precision, Recall, NDCG, MRR, MAP, BLEU и ROUGE.
Использование через API
У оценки пока нет отдельного входа в интерфейсе: запуск через POST /api/v1/evaluation, опрос результата через GET /api/v1/evaluation?task_id=...; для создания нужны права Admin, для запроса — Viewer. Набор данных имеет формат Parquet, требования к формату см. ниже.
При сравнении конфигураций следует фиксировать набор данных, менять по одному параметру за раз и анализировать результат по одному и тому же набору метрик.
Запуск одной оценки
- Используйте встроенный образец набора данных либо подготовьте Parquet-файлы по формату ниже и замените одноимённые файлы в
dataset/samples/рабочего каталога сервиса (в контейнере —/app/dataset/samples/). - Выберите эталонную базу знаний, диалоговую модель и модель реранжирования и создайте задачу через
POST /api/v1/evaluation. Эталонная база знаний используется для копирования конфигурации; для оценки применяется отдельная база знаний. - Запишите возвращённый ID задачи и запрашивайте состояние и прогресс через
GET /api/v1/evaluation?task_id=.... - При успехе задачи сравните метрики поиска и генерации; при сбое сначала проверьте ошибку задачи, затем скорректируйте конфигурацию и запустите снова.
Для создания задачи нужны права Admin, для запроса результата — Viewer; API Key также требует возможности run_evaluations или full-access.
Формат набора данных
Сервис набора данных (internal/application/service/dataset.go) загружает из ./dataset/samples/ 5 файлов Parquet:
| Файл | Schema | Значение |
|---|---|---|
queries.parquet | id: int64, text: string | Набор вопросов |
corpus.parquet | id: int64, text: string | Абзацы корпуса (при оценке загружаются в базу знаний) |
answers.parquet | id: int64, text: string | Эталонные ответы |
qrels.parquet | qid: int64, pid: int64 | Связь вопрос → релевантный абзац (ground truth) (основа для метрик поиска) |
qas.parquet | qid: int64, aid: int64 | Отображение вопрос → ответ (основа для метрик генерации) |
Соответствующие структуры 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):
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:
{
"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)
| Метрика | Поле | Файл реализации | Значение |
|---|---|---|---|
| Precision | precision | metric/precision.go | Точность поиска: число найденных релевантных документов / общее число возвращённых, среднее по множеству GT |
| Recall | recall | metric/recall.go | Полнота поиска: число найденных релевантных документов / общее число релевантных документов |
| NDCG@3 | ndcg3 | metric/ndcg.go | Нормализованный дисконтированный накопленный выигрыш (по первым 3 позициям); поощряет размещение релевантных документов выше |
| NDCG@10 | ndcg10 | metric/ndcg.go | То же, по первым 10 позициям |
| MRR | mrr | metric/mrr.go | Среднее обратного ранга первого релевантного документа: sum(1/rank) / N |
| MAP | map | metric/map.go | Средняя точность: для каждой позиции попадания накапливается Precision@k и затем нормализуется |
Ключевое вычисление NDCG (metric/ndcg.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):
for i, predID := range ids {
if _, ok := gtSet[predID]; ok {
sumRR += 1.0 / float64(i+1) // обратный ранг первой позиции попадания
break
}
}Метрики генерации (Generation Metrics)
| Метрика | Поле | Файл реализации | Значение |
|---|---|---|---|
| BLEU-1 | bleu1 | metric/bleu.go | Точность 1-gram (веса [1.0, 0, 0, 0]) |
| BLEU-2 | bleu2 | metric/bleu.go | 1/2-gram по 50% (веса [0.5, 0.5, 0, 0]) |
| BLEU-4 | bleu4 | metric/bleu.go | 1~4-gram с равными весами ([0.25, 0.25, 0.25, 0.25]), включает brevity penalty |
| ROUGE-1 | rouge1 | metric/rouge.go | F1 по перекрытию униграмм |
| ROUGE-2 | rouge2 | metric/rouge.go | F1 по перекрытию биграмм |
| ROUGE-L | rougel | metric/rouge.go | F1 по наибольшей общей подпоследовательности (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:
evaluationRoutes := g.apiKeyGroup(r.Group("/evaluation"), apiKeyRunEvaluations(apiKeyFullAccess()))
{
evaluationRoutes.POST("", g.Admin(), handler.Evaluation)
evaluationRoutes.GET("", g.Viewer(), handler.GetEvaluationResult)
}| Метод | Путь | Права | Описание |
|---|---|---|---|
| POST | /api/v1/evaluation | Admin (API Key требует возможность run_evaluations или full-access) | Создать задачу оценки, сразу возвращает информацию о задаче |
| GET | /api/v1/evaluation?task_id=... | Viewer | Запросить состояние, прогресс и результаты метрик задачи |
Создание задачи оценки
Параметры запроса (internal/handler/evaluation.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):
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):
const (
EvaluationStatuePending EvaluationStatue = iota // 0 ожидает запуска
EvaluationStatueRunning // 1 выполняется
EvaluationStatueSuccess // 2 успех
EvaluationStatueFailed // 3 сбой
)Процесс оценки
В internal/application/service/evaluation.go POST-интерфейс синхронно завершает подготовку и асинхронно выполняет оценку:
- Подготовка базы знаний: создаётся (или клонируется по конфигурации эталонной KB) отдельная база знаний для оценки, берутся модели Embedding и LLM по умолчанию;
- Сборка параметров: из системной конфигурации собираются параметры оценки
ChatManage—VectorThreshold,KeywordThreshold,EmbeddingTopK,RerankTopK,RerankThreshold,MaxRounds,SummaryConfig(MaxTokens / TopK / TopP / RepeatPenalty / Prompt / ContextTemplate и т. д.),FallbackResponse, промпты перезаписи и т. д.; - Регистрация задачи: задача регистрируется в хранилище в памяти под своим ID, состояние
Pending, ответ возвращается сразу; - Фоновое выполнение (goroutine): корпус набора данных загружается в оценочную KB → параллельно оценивается каждая пара QA → метрики агрегируются → ресурсы очищаются.
Степень параллелизма берётся как max(GOMAXPROCS - 1, 1) (ограничение errgroup):
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):
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 Handler | internal/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 |
