feat(eval): 添加评估基准分页和错误结果过滤功能

实现评估基准和结果的分页查询,支持按错误过滤结果
优化评估结果表格展示,增加文档链接和说明
提升文件上传验证逻辑和错误处理
This commit is contained in:
Wenjie Zhang 2025-12-14 18:13:46 +08:00
parent c196aaca03
commit 0875e9769c
10 changed files with 657 additions and 162 deletions

View File

@ -15,7 +15,7 @@
- 同名文件处理逻辑:遇到同名文件则在上传区域提示,是否删除旧文件
- conversation 待修改为异步的版本
- DBManager 需要将数据库修改为异步的aiosqlite或者异步mysql缓存使用Redis存储
- 【eval】缺少自动生成评估的功能
- 【eval】支持 easy dataset 的数据格式
- chat_model 的 call 需要异步
### Bugs

View File

@ -16,6 +16,8 @@ Yuxi-Know 提供了完整的 RAG 系统评估解决方案,帮助您科学地
- 当基准包含标准答案时,系统会使用 LLM 作为评判者评估生成答案的准确性
- 同时包含两者时,系统会进行全面的检索和生成质量评估
也可以从 easy dataset 自动生成评估基准。
#### JSONL 文件格式示例
```json

View File

@ -14,15 +14,27 @@ evaluation = APIRouter(prefix="/evaluation", tags=["evaluation"])
@evaluation.get("/databases/{db_id}/benchmarks/{benchmark_id}")
async def get_evaluation_benchmark_by_db(
db_id: str, benchmark_id: str, current_user: User = Depends(get_admin_user)
db_id: str,
benchmark_id: str,
page: int = 1,
page_size: int = 10,
current_user: User = Depends(get_admin_user)
):
"""根据 db_id 获取评估基准详情"""
"""根据 db_id 获取评估基准详情(支持分页)"""
from src.services.evaluation_service import EvaluationService
try:
# 验证分页参数
if page < 1:
raise HTTPException(status_code=400, detail="页码必须大于0")
if page_size < 1 or page_size > 100:
raise HTTPException(status_code=400, detail="每页大小必须在1-100之间")
service = EvaluationService()
benchmark = await service.get_benchmark_detail_by_db(db_id, benchmark_id)
benchmark = await service.get_benchmark_detail_by_db(db_id, benchmark_id, page, page_size)
return {"message": "success", "data": benchmark}
except HTTPException:
raise
except Exception as e:
logger.error(f"获取评估基准详情失败: {e}, {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"获取评估基准详情失败: {str(e)}")
@ -44,13 +56,28 @@ async def delete_evaluation_benchmark(benchmark_id: str, current_user: User = De
@evaluation.get("/databases/{db_id}/results/{task_id}")
async def get_evaluation_results_by_db(db_id: str, task_id: str, current_user: User = Depends(get_admin_user)):
"""获取评估结果(带 db_id"""
async def get_evaluation_results_by_db(
db_id: str,
task_id: str,
page: int = 1,
page_size: int = 20,
error_only: bool = False,
current_user: User = Depends(get_admin_user)
):
"""获取评估结果(带 db_id支持分页"""
from src.services.evaluation_service import EvaluationService
try:
# 验证分页参数
if page < 1:
raise HTTPException(status_code=400, detail="页码必须大于0")
if page_size < 1 or page_size > 100:
raise HTTPException(status_code=400, detail="每页大小必须在1-100之间")
service = EvaluationService()
results = await service.get_evaluation_results_by_db(db_id, task_id)
results = await service.get_evaluation_results_by_db(
db_id, task_id, page=page, page_size=page_size, error_only=error_only
)
return {"message": "success", "data": results}
except Exception as e:
logger.error(f"获取评估结果失败: {e}, {traceback.format_exc()}")

View File

@ -136,8 +136,8 @@ class EvaluationService:
logger.error(f"获取评估基准详情失败: {e}")
raise
async def get_benchmark_detail_by_db(self, db_id: str, benchmark_id: str) -> dict[str, Any]:
"""根据 db_id 直接获取评估基准详情"""
async def get_benchmark_detail_by_db(self, db_id: str, benchmark_id: str, page: int = 1, page_size: int = 10) -> dict[str, Any]:
"""根据 db_id 获取评估基准详情(支持分页)"""
try:
kb_instance = knowledge_base.get_kb(db_id)
benchmarks_map = kb_instance.benchmarks_meta.get(db_id, {})
@ -145,14 +145,46 @@ class EvaluationService:
raise ValueError("Benchmark not found")
meta = benchmarks_map[benchmark_id]
data_file_path = meta.get("benchmark_file")
# 获取总问题数和分页数据
total_questions = meta.get("question_count", 0)
questions = []
if data_file_path and os.path.exists(data_file_path):
# 计算分页范围
start_index = (page - 1) * page_size
end_index = start_index + page_size
# 读取指定范围的问题
with open(data_file_path, encoding="utf-8") as f:
current_index = 0
for line in f:
if line.strip():
if not line.strip():
continue
# 只处理指定范围内的问题
if current_index >= start_index and current_index < end_index:
questions.append(json.loads(line))
elif current_index >= end_index:
break # 已经读取到足够的问题,停止读取
current_index += 1
# 计算分页信息
total_pages = (total_questions + page_size - 1) // page_size
meta_with_q = meta.copy()
meta_with_q["questions"] = questions
meta_with_q.update({
"questions": questions,
"pagination": {
"current_page": page,
"page_size": page_size,
"total_questions": total_questions,
"total_pages": total_pages,
"has_next": page < total_pages,
"has_prev": page > 1
}
})
return meta_with_q
except Exception as e:
logger.error(f"获取评估基准详情失败: {e}")
@ -556,7 +588,8 @@ class EvaluationService:
f"基于以下上下文信息,请回答用户的问题。\n\n"
f"上下文信息:{context_text}\n\n"
f"用户问题:{question_data["query"]}\n\n"
"请根据上下文信息准确回答问题。如果上下文中没有相关信息,请说明。\n\n"
"请根据上下文信息准确回答问题。\n\n"
"如果上下文中缺少相关信息,请回答“信息不足,无法回答”。\n\n"
)
# 生成答案 - 使用 asyncio.to_thread 避免阻塞事件循环
@ -728,7 +761,14 @@ class EvaluationService:
raise
# 索引与回退逻辑已移除,统一通过 db_id 定位
async def get_evaluation_results_by_db(self, db_id: str, task_id: str) -> dict[str, Any]:
async def get_evaluation_results_by_db(
self,
db_id: str,
task_id: str,
page: int = 1,
page_size: int = 20,
error_only: bool = False
) -> dict[str, Any]:
result_file_path = os.path.join(self._get_result_dir(db_id), f"{task_id}.json")
if not os.path.exists(result_file_path):
task = await tasker.get_task(task_id)
@ -740,8 +780,63 @@ class EvaluationService:
"message": task.message,
}
raise ValueError(f"Result not found for task {task_id}")
# 加载JSON文件
with open(result_file_path, encoding="utf-8") as f:
return json.load(f)
data = json.load(f)
# 如果是分页请求,处理详细结果
if page and page_size:
all_results = data.get("interim_results", data.get("results", []))
# 如果只要错误结果,先过滤
if error_only:
filtered_results = []
for item in all_results:
# 检查答案评分是否为错误score <= 0.5
if item.get("metrics", {}).get("score", 1.0) <= 0.5:
filtered_results.append(item)
continue
# 检查检索指标是否明显偏低
metrics = item.get("metrics", {})
has_low_recall = any(
metrics.get(k, 1.0) < 0.3
for k in metrics
if k.startswith("recall@")
)
if has_low_recall:
filtered_results.append(item)
all_results = filtered_results
# 计算分页
total = len(all_results)
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
paged_results = all_results[start_idx:end_idx]
# 返回分页数据
return {
"task_id": data.get("task_id", task_id),
"status": data.get("status"),
"started_at": data.get("started_at"),
"completed_at": data.get("completed_at"),
"total_questions": data.get("total_questions", 0),
"completed_questions": data.get("completed_questions", 0),
"overall_score": data.get("overall_score"),
"retrieval_config": data.get("retrieval_config"),
"interim_results": paged_results,
"pagination": {
"current_page": page,
"page_size": page_size,
"total": total,
"total_pages": (total + page_size - 1) // page_size,
"error_only": error_only
}
}
# 非分页请求,返回完整数据(保持向后兼容)
return data
async def delete_evaluation_result_by_db(self, db_id: str, task_id: str) -> None:
result_file_path = os.path.join(self._get_result_dir(db_id), f"{task_id}.json")

View File

@ -360,8 +360,12 @@ export const evaluationApi = {
* @param {string} dbId - 知识库ID
* @param {string} benchmarkId - 基准ID
*/
getBenchmarkByDb: async (dbId, benchmarkId) => {
return apiAdminGet(`/api/evaluation/databases/${dbId}/benchmarks/${benchmarkId}`)
getBenchmarkByDb: async (dbId, benchmarkId, page = 1, pageSize = 50) => {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString()
})
return apiAdminGet(`/api/evaluation/databases/${dbId}/benchmarks/${benchmarkId}?${params}`)
},
/**
@ -420,8 +424,15 @@ export const evaluationApi = {
},
// 新接口:带 db_id 的评估结果查询与删除
getEvaluationResultsByDb: async (dbId, taskId) => {
return apiAdminGet(`/api/evaluation/databases/${dbId}/results/${taskId}`)
getEvaluationResultsByDb: async (dbId, taskId, params = {}) => {
const queryParams = new URLSearchParams();
if (params.page) queryParams.append('page', params.page);
if (params.pageSize) queryParams.append('page_size', params.pageSize);
if (params.errorOnly !== undefined) queryParams.append('error_only', params.errorOnly);
const url = `/api/evaluation/databases/${dbId}/results/${taskId}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
return apiAdminGet(url);
},
deleteEvaluationResultByDb: async (dbId, taskId) => {
return apiAdminDelete(`/api/evaluation/databases/${dbId}/results/${taskId}`)

View File

@ -120,7 +120,7 @@
<a-modal
v-model:open="previewModalVisible"
title="评估基准详情"
width="800px"
width="1200px"
:footer="null"
>
<div v-if="previewData" class="preview-content">
@ -146,30 +146,44 @@
</div>
</div>
<div class="preview-questions" v-if="previewQuestions.length > 0">
<h4>问题示例 (前5条)</h4>
<div class="question-list">
<div
v-for="(item, index) in previewQuestions.slice(0, 5)"
:key="index"
class="question-item"
>
<div class="question-header">
<span class="question-num">Q{{ index + 1 }}</span>
</div>
<div class="question-body">
<p class="question-text">{{ item.query }}</p>
<div v-if="item.gold_chunk_ids" class="question-chunk">
黄金Chunk: {{ item.gold_chunk_ids.slice(0, 3).join(', ') }}
<span v-if="item.gold_chunk_ids.length > 3">...等{{ item.gold_chunk_ids.length }}</span>
</div>
<div v-if="item.gold_answer" class="question-answer">
黄金答案: {{ item.gold_answer.slice(0, 150) }}
<span v-if="item.gold_answer.length > 150">...</span>
</div>
</div>
</div>
</div>
<div class="preview-questions" v-if="previewQuestions && previewQuestions.length > 0">
<h4>问题列表 ({{ previewPagination.total }})</h4>
<a-table
:dataSource="previewQuestions"
:columns="displayedQuestionColumns"
:pagination="paginationConfig"
size="small"
:rowKey="(_, index) => index"
:loading="previewPagination.loading"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'index'">
<span class="question-num">Q{{ (previewPagination.current - 1) * previewPagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.key === 'query'">
<a-tooltip :title="record?.query || ''" placement="topLeft">
<div class="question-text">{{ record?.query || '' }}</div>
</a-tooltip>
</template>
<template v-if="column.key === 'gold_chunk_ids'">
<a-tooltip v-if="record?.gold_chunk_ids && record.gold_chunk_ids.length > 0" :title="record.gold_chunk_ids.join(', ')" placement="topLeft">
<div class="question-chunk">
{{ record.gold_chunk_ids.slice(0, 3).join(', ') }}
<span v-if="record.gold_chunk_ids.length > 3">...等{{ record.gold_chunk_ids.length }}</span>
</div>
</a-tooltip>
<span v-else class="no-data">-</span>
</template>
<template v-if="column.key === 'gold_answer'">
<a-tooltip v-if="record?.gold_answer" :title="record.gold_answer" placement="topLeft">
<div class="question-answer">
{{ record.gold_answer }}
</div>
</a-tooltip>
<span v-else class="no-data">-</span>
</template>
</template>
</a-table>
</div>
</div>
</a-modal>
@ -177,7 +191,7 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, onMounted, computed } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
UploadOutlined,
@ -212,6 +226,64 @@ const generateModalVisible = ref(false);
const previewModalVisible = ref(false);
const previewData = ref(null);
const previewQuestions = ref([]);
const previewPagination = ref({
current: 1,
pageSize: 10,
total: 0,
loading: false
});
//
const questionColumns = [
{
title: '#',
key: 'index',
width: 60,
align: 'center'
},
{
title: '问题',
dataIndex: 'query',
key: 'query',
width: 280,
ellipsis: false
},
{
title: '黄金Chunk',
dataIndex: 'gold_chunk_ids',
key: 'gold_chunk_ids',
width: 200,
ellipsis: false
},
{
title: '黄金答案',
dataIndex: 'gold_answer',
key: 'gold_answer',
width: 420,
ellipsis: false
}
];
const displayedQuestionColumns = computed(() => {
if (previewData.value && previewData.value.has_gold_chunks === false) {
return questionColumns.filter(c => c.key !== 'gold_chunk_ids');
}
return questionColumns;
});
//
const paginationConfig = computed(() => ({
current: previewPagination.value.current,
pageSize: previewPagination.value.pageSize,
total: previewPagination.value.total,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`,
showSizeChanger: true,
pageSizeOptions: ['5', '10', '20', '50'],
showQuickJumper: true,
size: 'small',
onChange: handlePageChange,
onShowSizeChange: handlePageSizeChange
}));
//
const loadBenchmarks = async () => {
@ -264,13 +336,71 @@ const onGenerateSuccess = () => {
emit('refresh');
};
//
const handlePageChange = (page, pageSize) => {
previewPagination.value.current = page;
previewPagination.value.pageSize = pageSize;
loadPreviewQuestions();
};
const handlePageSizeChange = (current, size) => {
previewPagination.value.current = 1;
previewPagination.value.pageSize = size;
loadPreviewQuestions();
};
//
const loadPreviewQuestions = async () => {
if (!previewData.value?.benchmark_id) return;
try {
previewPagination.value.loading = true;
const response = await evaluationApi.getBenchmarkByDb(
props.databaseId,
previewData.value.benchmark_id,
previewPagination.value.current,
previewPagination.value.pageSize
);
if (response.message === 'success') {
previewQuestions.value = response.data.questions || [];
previewPagination.value.total = response.data.pagination?.total_questions || 0;
}
} catch (error) {
console.error('加载预览问题失败:', error);
message.error('加载预览问题失败');
} finally {
previewPagination.value.loading = false;
}
};
//
const previewBenchmark = async (benchmark) => {
try {
const response = await evaluationApi.getBenchmarkByDb(props.databaseId, benchmark.benchmark_id);
//
previewPagination.value = {
current: 1,
pageSize: 10,
total: 0,
loading: false
};
const response = await evaluationApi.getBenchmarkByDb(
props.databaseId,
benchmark.benchmark_id,
previewPagination.value.current,
previewPagination.value.pageSize
);
if (response.message === 'success') {
previewData.value = response.data;
// ID
previewData.value = {
...response.data,
benchmark_id: benchmark.benchmark_id // benchmark_id
};
previewQuestions.value = response.data.questions || [];
previewPagination.value.total = response.data.pagination?.total_questions || 0;
console.log('预览问题数据:', response.data.questions); //
previewModalVisible.value = true;
}
} catch (error) {
@ -563,42 +693,70 @@ onMounted(() => {
color: var(--gray-900);
}
.question-list {
display: flex;
flex-direction: column;
gap: 16px;
.question-num {
font-size: 14px;
font-weight: 600;
color: var(--gray-700);
}
.question-item {
padding: 16px;
background: var(--gray-50);
border-radius: 8px;
border: 1px solid var(--gray-200);
.question-text {
font-size: 14px;
line-height: 1.5;
color: var(--gray-800);
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
max-height: 6em; // 4 * 1.5em line-height
cursor: pointer;
}
.question-header {
margin-bottom: 8px;
.question-chunk,
.question-answer {
font-size: 13px;
color: var(--gray-600);
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
max-height: 6em; // 4 * 1.5em line-height for 13px font
cursor: pointer;
}
.question-num {
font-size: 14px;
font-weight: 600;
color: var(--gray-700);
}
.no-data {
color: var(--gray-400);
font-style: italic;
}
:deep(.ant-table) {
.ant-table-thead > tr > th {
background-color: var(--gray-50);
border-bottom: 1px solid var(--gray-200);
font-weight: 600;
font-size: 13px;
padding: 8px 12px;
white-space: nowrap;
}
.question-body {
.question-text {
margin: 0 0 12px;
font-size: 14px;
line-height: 1.6;
color: var(--gray-800);
}
.ant-table-tbody > tr > td {
padding: 8px 12px;
border-bottom: 1px solid var(--gray-150);
font-size: 13px;
vertical-align: top;
line-height: 1.4;
}
.question-chunk,
.question-answer {
margin: 8px 0;
font-size: 13px;
color: var(--gray-600);
}
.ant-table-tbody > tr:hover > td {
background-color: var(--gray-50);
}
//
.ant-table-cell {
white-space: normal !important;
word-wrap: break-word !important;
word-break: break-all !important;
}
}
}

View File

@ -37,6 +37,9 @@
>
<SettingOutlined /> 分块参数 ({{ chunkParams.chunk_size }}/{{ chunkParams.chunk_overlap }})
</a-button>
<a-button type="link" class="doc-link-btn" @click="openDocLink">
<InfoCircleOutlined /> 文档处理与 OCR 说明
</a-button>
</div>
</div>
@ -691,6 +694,10 @@ const getAuthHeaders = () => {
return userStore.getAuthHeaders();
};
const openDocLink = () => {
window.open('https://xerrors.github.io/Yuxi-Know/latest/advanced/document-processing.html', '_blank', 'noopener');
};
const chunkData = async () => {
if (!databaseId.value) {
message.error('请先选择知识库');
@ -866,6 +873,11 @@ const chunkData = async () => {
color: var(--color-warning-500);
}
.doc-link-btn {
color: var(--main-600);
margin-left: 12px;
}
//
.same-name-files-section {
margin-top: 16px;

View File

@ -260,23 +260,48 @@
</a-row>
<!-- 详细结果表格 -->
<h4 style="margin-bottom: 16px">详细评估结果</h4>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<div>
<h4 style="margin: 0;">详细评估结果</h4>
<span style="font-size: 12px; color: var(--gray-600); margin-left: 8px;">
{{ showErrorsOnly ? `仅显示错误结果(共 ${paginationTotal} 条)` : `显示全部结果(共 ${paginationTotal} 条)` }}
</span>
</div>
<a-button
type="default"
size="small"
@click="toggleErrorOnly"
:class="{ 'error-only-active': showErrorsOnly }"
>
{{ showErrorsOnly ? '显示全部' : '仅查看错误' }}
</a-button>
</div>
<a-table
:columns="resultColumns"
:data-source="detailedResults"
:pagination="{ pageSize: 10, showSizeChanger: true, showQuickJumper: true }"
:pagination="{
current: currentPage,
pageSize: pageSize,
total: paginationTotal,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`,
onChange: handlePageChange,
onShowSizeChange: handlePageSizeChange
}"
:scroll="{ x: 1000 }"
size="small"
:loading="resultsLoading"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'query'">
<a-tooltip :title="record.query">
<span class="query-text">{{ truncateText(record.query, 50) }}</span>
<div class="query-text">{{ record.query }}</div>
</a-tooltip>
</template>
<template v-else-if="column.key === 'generated_answer'">
<a-tooltip :title="record.generated_answer">
<span class="answer-text">{{ truncateText(record.generated_answer || '-', 80) }}</span>
<div class="answer-text">{{ record.generated_answer || '-' }}</div>
</a-tooltip>
</template>
<template v-else-if="column.key === 'retrieval_score'">
@ -299,7 +324,7 @@
</a-tag>
<div v-if="record.metrics.reasoning" class="answer-reasoning">
<a-tooltip :title="record.metrics.reasoning">
{{ truncateText(record.metrics.reasoning, 80) }}
{{ record.metrics.reasoning }}
</a-tooltip>
</div>
</div>
@ -359,6 +384,11 @@ const evaluationStats = ref({});
const resultsLoading = ref(false);
const searchConfigModalVisible = ref(false);
const refreshingHistory = ref(false);
const showErrorsOnly = ref(false);
const currentPage = ref(1);
const pageSize = ref(20);
const paginationTotal = ref(0);
const paginationTotalPages = ref(0);
// 使
@ -368,29 +398,48 @@ const configForm = reactive({
});
//
const resultColumns = [
{
title: '问题',
dataIndex: 'query',
key: 'query',
width: 200
},
{
title: '生成答案',
key: 'generated_answer',
width: 250
},
{
title: '检索指标',
key: 'retrieval_score',
width: 300
},
{
title: '答案评判',
key: 'answer_score',
width: 200
const resultColumns = computed(() => {
const columns = [
{
title: '问题',
dataIndex: 'query',
key: 'query',
width: 100
},
{
title: '生成答案',
key: 'generated_answer',
width: 180
},
{
title: '答案评判',
key: 'answer_score',
width: 260
}
];
//
const hasRetrievalMetrics = detailedResults.value.some(item => {
if (!item.metrics) return false;
return Object.keys(item.metrics).some(key =>
key.startsWith('recall') ||
key.startsWith('precision') ||
key === 'map' ||
key === 'ndcg'
);
});
//
if (hasRetrievalMetrics) {
columns.splice(2, 0, {
title: '检索指标',
key: 'retrieval_score',
width: 100
});
}
];
return columns;
});
const historyColumns = [
{
@ -463,6 +512,93 @@ const currentBenchmark = computed(() => {
return availableBenchmarks.value.find(b => b.benchmark_id === selectedBenchmarkId.value);
});
//
const toggleErrorOnly = async () => {
resultsLoading.value = true;
showErrorsOnly.value = !showErrorsOnly.value;
currentPage.value = 1; //
//
await loadResultsWithPagination();
};
//
const handlePageChange = (page, size) => {
currentPage.value = page;
if (size !== pageSize.value) {
pageSize.value = size;
}
loadResultsWithPagination();
};
//
const handlePageSizeChange = (current, size) => {
currentPage.value = 1;
pageSize.value = size;
loadResultsWithPagination();
};
//
const loadResultsWithPagination = async () => {
if (!selectedResult.value) return;
try {
resultsLoading.value = true;
const response = await evaluationApi.getEvaluationResultsByDb(
props.databaseId,
selectedResult.value.task_id,
{
page: currentPage.value,
pageSize: pageSize.value,
errorOnly: showErrorsOnly.value
}
);
if (response.message === 'success' && response.data) {
const resultData = response.data;
//
detailedResults.value = resultData.interim_results || [];
//
if (resultData.pagination) {
paginationTotal.value = resultData.pagination.total;
paginationTotalPages.value = resultData.pagination.total_pages;
} else {
//
paginationTotal.value = detailedResults.value.length;
paginationTotalPages.value = 1;
}
//
//
if (showErrorsOnly.value) {
//
evaluationStats.value = {
...evaluationStats.value,
totalQuestions: paginationTotal.value,
//
};
} else if (currentPage.value === 1) {
//
evaluationStats.value = calculateEvaluationStats(detailedResults.value);
}
//
if (resultData.started_at && resultData.completed_at) {
const startTime = new Date(resultData.started_at);
const endTime = new Date(resultData.completed_at);
evaluationStats.value.totalDuration = (endTime - startTime) / 1000;
}
}
} catch (error) {
console.error('加载评估结果失败:', error);
message.error('加载评估结果失败');
} finally {
resultsLoading.value = false;
}
};
//
const openSearchConfigModal = () => {
searchConfigModalVisible.value = true;
@ -646,17 +782,17 @@ const calculateEvaluationStats = (results) => {
const viewResults = async (taskId) => {
try {
resultsLoading.value = true;
//
currentPage.value = 1;
showErrorsOnly.value = false;
//
const response = await evaluationApi.getEvaluationResultsByDb(props.databaseId, taskId);
if (response.message === 'success' && response.data) {
// API
const resultData = response.data;
// - interim_results
detailedResults.value = resultData.interim_results || resultData.results || [];
//
evaluationStats.value = calculateEvaluationStats(detailedResults.value);
// 使API
selectedResult.value = evaluationHistory.value.find(r => r.task_id === taskId) || {
task_id: resultData.task_id,
@ -674,14 +810,11 @@ const viewResults = async (taskId) => {
selectedResult.value.retrieval_config = resultData.retrieval_config;
}
//
if (resultData.started_at && resultData.completed_at) {
const startTime = new Date(resultData.started_at);
const endTime = new Date(resultData.completed_at);
evaluationStats.value.totalDuration = (endTime - startTime) / 1000; //
}
//
resultModalVisible.value = true;
//
await loadResultsWithPagination();
} else {
message.error('获取评估结果失败:数据格式错误');
}
@ -965,21 +1098,25 @@ onMounted(() => {
}
.query-text {
display: inline-block;
max-width: 200px;
font-size: 12px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
line-height: 1.5;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.4;
}
.answer-text {
display: inline-block;
max-width: 250px;
font-size: 12px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
line-height: 1.5;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.4;
color: var(--gray-700);
}
@ -993,7 +1130,8 @@ onMounted(() => {
//
:deep(.ant-table) {
.ant-table-tbody > tr > td {
padding: 8px 12px;
padding: 12px 12px;
vertical-align: top;
}
.ant-table-thead > tr > th {
@ -1167,9 +1305,15 @@ onMounted(() => {
.answer-reasoning {
font-size: 12px;
color: var(--gray-600);
margin-top: 4px;
line-height: 1.3;
margin-top: 8px;
line-height: 1.4;
cursor: pointer;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
color: var(--gray-800);
@ -1320,4 +1464,21 @@ onMounted(() => {
}
}
//
.error-only-active {
background-color: var(--color-error-500) !important;
border-color: var(--color-error-500) !important;
color: white !important;
&:hover {
background-color: var(--color-error-600) !important;
border-color: var(--color-error-600) !important;
}
&:focus {
background-color: var(--color-error-500) !important;
border-color: var(--color-error-500) !important;
}
}
</style>

View File

@ -28,7 +28,7 @@
</a-textarea>
</a-form-item>
<a-form-item label="生成参数" name="params">
<a-form-item label="生成参数" name="params" :extra="extraText">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="问题数量" name="count" :labelCol="{ span: 24 }" :wrapperCol="{ span: 24 }">
@ -112,7 +112,7 @@
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue';
import { ref, reactive, computed, watch, h } from 'vue';
import { message } from 'ant-design-vue';
import { evaluationApi } from '@/apis/knowledge_api';
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
@ -166,6 +166,16 @@ const visible = computed({
set: (val) => emit('update:visible', val)
});
//
const extraText = computed(() => h('span', {}, [
'需要了解评估基准生成原理?查看',
h('a', {
href: 'https://xerrors.github.io/Yuxi-Know/latest/intro/evaluation.html',
target: '_blank',
rel: 'noopener noreferrer'
}, '使用说明')
]));
//
const handleGenerate = async () => {
try {

View File

@ -28,7 +28,11 @@
/>
</a-form-item>
<a-form-item label="基准文件" name="file">
<a-form-item
label="基准文件"
name="file"
:extra="extraText"
>
<a-upload-dragger
v-model:fileList="fileList"
name="file"
@ -52,7 +56,7 @@
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue';
import { ref, reactive, computed, watch, h } from 'vue';
import { message } from 'ant-design-vue';
import { FileTextOutlined } from '@ant-design/icons-vue';
import { evaluationApi } from '@/apis/knowledge_api';
@ -98,52 +102,67 @@ const visible = computed({
set: (val) => emit('update:visible', val)
});
//
const extraText = computed(() => h('span', {}, [
'需要了解评估基准格式?查看',
h('a', {
href: 'https://xerrors.github.io/Yuxi-Know/latest/intro/evaluation.html',
target: '_blank',
rel: 'noopener noreferrer'
}, '使用说明')
]));
//
const beforeUpload = (file) => {
const beforeUpload = async (file) => {
//
if (!file.name.endsWith('.jsonl')) {
message.error('仅支持 JSONL 格式文件');
return false;
}
// 10MB
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
message.error('文件大小不能超过 10MB');
// 100MB
const isLt100M = file.size / 1024 / 1024 < 100;
if (!isLt100M) {
message.error('文件大小不能超过 100MB');
return false;
}
//
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
const lines = content.trim().split('\n');
try {
//
const content = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = () => reject(new Error('文件读取失败'));
reader.readAsText(file);
});
//
if (lines.length === 0) {
message.error('文件不能为空');
return false;
}
const lines = content.trim().split('\n');
// JSON
for (let i = 0; i < Math.min(5, lines.length); i++) {
const line = lines[i].trim();
if (line) {
JSON.parse(line);
}
}
formState.file = file;
} catch (error) {
message.error('文件格式错误请检查JSONL格式');
//
if (lines.length === 0) {
message.error('文件不能为空');
return false;
}
};
reader.readAsText(file);
//
return false;
// JSON
for (let i = 0; i < Math.min(5, lines.length); i++) {
const line = lines[i].trim();
if (line) {
JSON.parse(line);
}
}
//
formState.file = file;
return true;
} catch (error) {
if (error instanceof SyntaxError) {
message.error('文件格式错误请检查JSONL格式');
} else {
message.error('文件验证失败: ' + error.message);
}
return false;
}
};
//