ForcePilot/web/src/components/RAGEvaluationTab.vue

1569 lines
41 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="rag-evaluation-container">
<!-- 顶部工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<!-- 基准选择 -->
<div class="benchmark-selector">
<label class="selector-label">评估基准</label>
<a-select
v-model:value="selectedBenchmarkId"
placeholder="请选择评估基准"
style="width: 240px"
@change="onBenchmarkChanged"
:loading="benchmarksLoading"
>
<a-select-option
v-for="benchmark in availableBenchmarks"
:key="benchmark.benchmark_id"
:value="benchmark.benchmark_id"
>
{{ benchmark.name }} ({{ benchmark.question_count }} 个问题)
</a-select-option>
</a-select>
<a-button
type="text"
size="middle"
:loading="benchmarksLoading"
@click="() => loadBenchmarks(true)"
:icon="h(ReloadOutlined)"
class="refresh-benchmarks-btn"
title="刷新评估基准列表"
/>
</div>
</div>
<div class="toolbar-right">
<!-- 检索配置按钮 -->
<a-button size="middle" @click="openSearchConfigModal" :icon="h(SettingOutlined)" />
<!-- 开始评估按钮 -->
<a-button
type="primary"
:loading="startingEvaluation"
@click="startEvaluation"
:disabled="!selectedBenchmark"
size="middle"
>
开始评估
</a-button>
</div>
</div>
<!-- 评估结果区域 -->
<div class="evaluation-results">
<!-- 模型配置(仅在选中基准且需要时显示) -->
<div
v-if="
selectedBenchmark &&
(selectedBenchmark.has_gold_chunks || selectedBenchmark.has_gold_answers)
"
class="model-config-section"
>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item
:label="
selectedBenchmark.has_gold_answers ? '答案生成模型(可选)' : '答案生成模型(当前基准无需)'
"
>
<ModelSelectorComponent
v-model:model_spec="configForm.answer_llm"
size="small"
:disabled="!selectedBenchmark || !selectedBenchmark.has_gold_answers"
@select-model="(value) => (configForm.answer_llm = value)"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
:label="
selectedBenchmark.has_gold_answers ? '答案评判模型(可选)' : '答案评判模型(当前基准无需)'
"
>
<ModelSelectorComponent
v-model:model_spec="configForm.judge_llm"
size="small"
:disabled="!selectedBenchmark || !selectedBenchmark.has_gold_answers"
@select-model="(value) => (configForm.judge_llm = value)"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</div>
<template v-if="!selectedBenchmark">
<div class="empty-state">
<a-empty description="请在顶部选择评估基准或前往基准管理">
<a-space>
<a-button @click="$emit('switch-to-benchmarks')"> 前往基准管理 </a-button>
</a-space>
</a-empty>
</div>
</template>
<template v-else>
<!-- 历史评估记录 -->
<div class="history-section">
<div class="section-header">
<h4 class="section-title">历史评估记录</h4>
<a-button
type="text"
size="small"
:loading="refreshingHistory"
@click="refreshHistory"
:icon="h('ReloadOutlined')"
class="refresh-btn"
>
刷新
</a-button>
</div>
<a-table
class="history-table"
:columns="historyColumns"
:data-source="evaluationHistory"
:pagination="{ pageSize: 10 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'overall_score'">
<span v-if="record.overall_score !== null">
<a-tag :color="getScoreTagColor(record.overall_score)">
{{ (record.overall_score * 100).toFixed(0) }}%
</a-tag>
</span>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button
v-if="record.status === 'completed'"
type="link"
size="small"
@click="viewResults(record.task_id)"
>
查看结果
</a-button>
<a-popconfirm
title="确定要删除这条评估记录吗"
description="删除后将无法恢复"
@confirm="deleteEvaluationRecord(record.task_id)"
ok-text="确定"
cancel-text="取消"
>
<a-button type="link" size="small" danger :loading="record.deleting">
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</div>
</template>
</div>
</div>
<!-- 评估结果详情模态框 -->
<a-modal
v-model:open="resultModalVisible"
:title="`评估结果 - ${selectedResult?.task_id?.slice(0, 8) || ''}`"
width="1200px"
:footer="null"
>
<div v-if="resultsLoading" class="loading-container">
<a-spin size="large" />
<p style="margin-top: 16px; color: var(--gray-600)">正在加载评估结果...</p>
</div>
<div v-else-if="selectedResult && detailedResults.length > 0">
<!-- 基本信息 -->
<a-descriptions
title="基本信息"
:column="3"
size="small"
bordered
style="margin-bottom: 20px"
>
<a-descriptions-item label="任务ID">{{ selectedResult.task_id }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="getStatusColor(selectedResult.status)">
{{ getStatusText(selectedResult.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="总体评分">
<span v-if="selectedResult.overall_score !== null">
<a-tag :color="getScoreTagColor(selectedResult.overall_score)">
{{ (selectedResult.overall_score * 100).toFixed(1) }}%
</a-tag>
</span>
<span v-else>-</span>
</a-descriptions-item>
<a-descriptions-item label="总问题数">{{
selectedResult.total_questions
}}</a-descriptions-item>
<a-descriptions-item label="完成数">{{
selectedResult.completed_questions
}}</a-descriptions-item>
<a-descriptions-item label="总耗时">
<span v-if="evaluationStats.totalDuration">
{{ formatDuration(evaluationStats.totalDuration) }}
</span>
<span v-else>-</span>
</a-descriptions-item>
</a-descriptions>
<!-- 检索配置和整体评估报告 -->
<a-row :gutter="16" style="margin-bottom: 20px">
<!-- 检索配置 -->
<a-col :span="12" v-if="selectedResult.retrieval_config">
<a-card size="small" title="检索配置">
<div class="json-viewer-container">
<pre class="json-viewer">{{
JSON.stringify(selectedResult.retrieval_config, null, 2)
}}</pre>
</div>
</a-card>
</a-col>
<!-- 整体评估报告 -->
<a-col :span="selectedResult.retrieval_config ? 12 : 24">
<a-card size="small" title="整体评估报告">
<!-- 检索指标 -->
<div style="margin-bottom: 20px">
<h5 style="margin-bottom: 12px; font-size: 14px; font-weight: 500">检索指标</h5>
<div v-if="Object.keys(evaluationStats.retrievalMetrics || {}).length > 0">
<div
v-for="(value, key) in evaluationStats.retrievalMetrics"
:key="key"
class="report-metric"
>
<span class="metric-label">{{ getMetricTitle(key) }}</span>
<span class="metric-value" :style="{ color: getScoreColor(value) }">
{{ formatMetricValue(value) }}
</span>
</div>
</div>
<span v-else class="no-metrics">-</span>
</div>
<!-- 答案准确性 -->
<div>
<h5 style="margin-bottom: 12px; font-size: 14px; font-weight: 500">答案准确性</h5>
<div class="accuracy-stats">
<div class="accuracy-item">
<span class="accuracy-label">正确答案数:</span>
<span class="accuracy-value"
>{{ evaluationStats.correctAnswers || 0 }} /
{{ evaluationStats.totalQuestions || 0 }}</span
>
</div>
<div class="accuracy-item">
<span class="accuracy-label">准确率:</span>
<span
class="accuracy-value"
:style="{ color: getScoreColor(evaluationStats.answerAccuracy) }"
>
{{ (evaluationStats.answerAccuracy * 100).toFixed(1) }}%
</span>
</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
<!-- 详细结果表格 -->
<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="{
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">
<div class="query-text">{{ record.query }}</div>
</a-tooltip>
</template>
<template v-else-if="column.key === 'generated_answer'">
<a-tooltip :title="record.generated_answer">
<div class="answer-text">{{ record.generated_answer || '-' }}</div>
</a-tooltip>
</template>
<template v-else-if="column.key === 'retrieval_score'">
<div
v-if="
record.metrics &&
Object.keys(record.metrics).some(
(k) =>
k.startsWith('recall') ||
k.startsWith('precision') ||
k === 'map' ||
k === 'ndcg'
)
"
class="retrieval-metrics"
>
<div v-for="(val, key) in record.metrics" :key="key" class="metric-item">
<span
v-if="
key.startsWith('recall') ||
key.startsWith('precision') ||
key === 'map' ||
key === 'ndcg'
"
class="metric-content"
:class="`metric-${getMetricType(key)}`"
>
<span class="metric-name">{{ getMetricShortName(key) }}</span>
<span class="metric-value">{{ formatMetricValue(val) }}</span>
</span>
</div>
</div>
<span v-else class="no-metrics">-</span>
</template>
<template v-else-if="column.key === 'answer_score'">
<div v-if="record.metrics && record.metrics.score !== undefined">
<a-tag :color="record.metrics.score > 0.5 ? 'green' : 'red'">
{{ record.metrics.score === 1.0 ? '正确' : '错误' }}
</a-tag>
<div v-if="record.metrics.reasoning" class="answer-reasoning">
<a-tooltip :title="record.metrics.reasoning">
{{ record.metrics.reasoning }}
</a-tooltip>
</div>
</div>
<span v-else>-</span>
</template>
</template>
</a-table>
</div>
<div v-else-if="selectedResult" class="empty-results">
<a-empty description="暂无详细结果数据">
<a-button @click="viewDetails(selectedResult)">查看基本信息</a-button>
</a-empty>
</div>
</a-modal>
<!-- 检索配置弹窗 -->
<SearchConfigModal
v-model="searchConfigModalVisible"
:database-id="databaseId"
@save="handleSearchConfigSave"
/>
</template>
<script setup>
import { ref, reactive, onMounted, computed, h } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { evaluationApi } from '@/apis/knowledge_api'
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
import SearchConfigModal from './SearchConfigModal.vue'
import { SettingOutlined, ReloadOutlined } from '@ant-design/icons-vue'
import { useTaskerStore } from '@/stores/tasker'
const props = defineProps({
databaseId: {
type: String,
required: true
}
})
const emit = defineEmits(['switch-to-benchmarks'])
// 使用任务中心 store
const taskerStore = useTaskerStore()
// 状态
const selectedBenchmarkId = ref(null)
const selectedBenchmark = ref(null)
const benchmarksLoading = ref(false)
const availableBenchmarks = ref([])
const startingEvaluation = ref(false)
const evaluationHistory = ref([])
const resultModalVisible = ref(false)
const selectedResult = ref(null)
const detailedResults = ref([])
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)
// 评估配置表单(使用知识库默认配置)
const configForm = reactive({
answer_llm: '', // 答案生成模型
judge_llm: '' // 评判模型
})
// 表格列定义
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 = [
{
title: '开始时间',
dataIndex: 'started_at',
key: 'started_at',
width: 180,
customRender: ({ record }) => formatTime(record.started_at)
},
{
title: '评估基准',
key: 'benchmark_name',
width: 200,
customRender: ({ record }) => {
// 根据 benchmark_id 查找基准名称
const benchmark = availableBenchmarks.value.find(
(b) => b.benchmark_id === record.benchmark_id
)
return benchmark ? benchmark.name : record.benchmark_id?.slice(0, 8) || '-'
}
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: 'Recall@10',
key: 'recall_10',
width: 100,
customRender: ({ record }) => {
// 使用后端返回的 metrics.recall@10 数据
if (
record.metrics &&
record.metrics['recall@10'] !== undefined &&
record.metrics['recall@10'] !== null
) {
const recallValue = record.metrics['recall@10']
const displayValue = formatMetricValue(recallValue)
return h(
'a-tag',
{
color: getScoreTagColor(recallValue)
},
displayValue
)
}
// 如果是运行中的任务,显示计算中
if (record.status === 'running') {
return h(
'a-tag',
{
color: 'processing'
},
'计算中'
)
}
// 已完成但没有 recall@10 数据
if (record.status === 'completed') {
return h(
'a-tag',
{
color: 'default'
},
'无数据'
)
}
// 其他情况显示横线
return h('span', '-')
}
},
{
title: '操作',
key: 'actions',
width: 150
}
]
// 计算属性:当前选中的基准对象
const currentBenchmark = computed(() => {
if (!selectedBenchmarkId.value || !availableBenchmarks.value) {
return null
}
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
}
// 处理检索配置保存
const handleSearchConfigSave = (config) => {
console.log('RAG评估中的检索配置已更新:', config)
// 可以在这里添加配置更新后的处理逻辑
}
// 加载基准列表
const loadBenchmarks = async (showSuccessMessage = false) => {
if (!props.databaseId) return
benchmarksLoading.value = true
try {
const response = await evaluationApi.getBenchmarks(props.databaseId)
if (response && response.message === 'success' && Array.isArray(response.data)) {
availableBenchmarks.value = response.data
// 如果没有选中的基准,且有可用基准,默认选中第一个
if (!selectedBenchmarkId.value && response.data.length > 0) {
selectedBenchmarkId.value = response.data[0].benchmark_id
selectedBenchmark.value = response.data[0]
} else if (selectedBenchmarkId.value) {
// 如果之前有选中的基准,重新验证其有效性
const exists = response.data.some((b) => b.benchmark_id === selectedBenchmarkId.value)
if (!exists) {
selectedBenchmarkId.value = null
selectedBenchmark.value = null
} else {
// 更新选中的基准对象
selectedBenchmark.value = response.data.find(
(b) => b.benchmark_id === selectedBenchmarkId.value
)
}
}
// 如果是手动刷新,显示成功提示
if (showSuccessMessage) {
message.success(`已刷新,找到 ${response.data.length} 个评估基准`)
}
} else {
console.error('响应格式不符合预期:', response)
message.error('基准数据格式错误')
}
} catch (error) {
console.error('加载评估基准失败:', error)
message.error('加载评估基准失败')
} finally {
benchmarksLoading.value = false
}
}
// 下拉框选择变化
const onBenchmarkChanged = (benchmarkId) => {
const benchmark = availableBenchmarks.value.find((b) => b.benchmark_id === benchmarkId)
selectedBenchmark.value = benchmark || null
}
// 刷新历史评估记录
const refreshHistory = async () => {
refreshingHistory.value = true
try {
await loadEvaluationHistory()
message.success('历史记录已刷新')
} catch (error) {
console.error('刷新历史记录失败:', error)
message.error('刷新历史记录失败')
} finally {
refreshingHistory.value = false
}
}
// 开始评估
const startEvaluation = async () => {
if (!selectedBenchmark.value) {
message.error('请先选择评估基准')
return
}
// 校验模型选择:必须同时选择或同时不选择
const hasAnswerModel = !!configForm.answer_llm
const hasJudgeModel = !!configForm.judge_llm
if (hasAnswerModel !== hasJudgeModel) {
message.warning('生成模型和评估模型必须同时选择或者同时不选择')
return
}
const runEvaluation = async () => {
startingEvaluation.value = true
// 只传递模型配置,检索配置由服务器从知识库读取
const params = {
benchmark_id: selectedBenchmark.value.benchmark_id,
model_config: {
answer_llm: configForm.answer_llm, // 传递答案生成模型
judge_llm: configForm.judge_llm // 传递评判模型
}
}
try {
const response = await evaluationApi.runEvaluation(props.databaseId, params)
if (response.message === 'success') {
message.success('评估任务已开始')
loadEvaluationHistory()
// 刷新任务中心的任务列表
taskerStore.loadTasks()
} else {
message.error(response.message || '启动评估失败')
}
} catch (error) {
console.error('启动评估失败:', error)
message.error('启动评估失败')
} finally {
startingEvaluation.value = false
}
}
if (!hasAnswerModel) {
Modal.confirm({
title: '确认评估模式',
content: '您未选择答案生成模型,将仅进行检索测试,不会进行问答测试。是否继续?',
okText: '继续',
cancelText: '取消',
onOk: runEvaluation
})
} else {
runEvaluation()
}
}
// 加载评估历史
const loadEvaluationHistory = async () => {
try {
const response = await evaluationApi.getEvaluationHistory(props.databaseId)
if (response.message === 'success') {
evaluationHistory.value = response.data || []
}
} catch (error) {
console.error('加载评估历史失败:', error)
message.error('加载评估历史失败')
}
}
// 计算评估统计信息
const calculateEvaluationStats = (results) => {
if (!results || results.length === 0) {
return {}
}
const stats = {
totalQuestions: results.length,
retrievalMetrics: {},
answerAccuracy: 0,
correctAnswers: 0,
averageResponseTime: 0,
totalResponseTime: 0
}
const metricSums = {}
const metricCounts = {}
results.forEach((item) => {
// 答案准确率
if (item.metrics && item.metrics.score !== undefined) {
if (item.metrics.score > 0.5) {
stats.correctAnswers++
}
}
// 检索指标统计
if (item.metrics) {
Object.keys(item.metrics).forEach((key) => {
if (
key.startsWith('recall') ||
key.startsWith('precision') ||
key === 'map' ||
key === 'ndcg'
) {
if (!metricSums[key]) {
metricSums[key] = 0
metricCounts[key] = 0
}
metricSums[key] += item.metrics[key]
metricCounts[key]++
}
})
}
})
// 计算平均值
Object.keys(metricSums).forEach((key) => {
stats.retrievalMetrics[key] = metricSums[key] / metricCounts[key]
})
// 计算答案准确率
stats.answerAccuracy = stats.totalQuestions > 0 ? stats.correctAnswers / stats.totalQuestions : 0
return stats
}
// 查看结果
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) {
const resultData = response.data
// 从历史记录中找到对应的任务信息如果没有则使用API返回的数据
selectedResult.value = evaluationHistory.value.find((r) => r.task_id === taskId) || {
task_id: resultData.task_id,
status: resultData.status,
started_at: resultData.started_at,
completed_at: resultData.completed_at,
total_questions: resultData.total_questions || 0,
completed_questions: resultData.completed_questions || 0,
overall_score: resultData.overall_score,
retrieval_config: resultData.retrieval_config
}
// 如果是从历史记录获取的,确保也有 retrieval_config
if (selectedResult.value && !selectedResult.value.retrieval_config) {
selectedResult.value.retrieval_config = resultData.retrieval_config
}
// 打开模态框
resultModalVisible.value = true
// 加载分页数据
await loadResultsWithPagination()
} else {
message.error('获取评估结果失败:数据格式错误')
}
} catch (error) {
console.error('获取评估结果失败:', error)
message.error('获取评估结果失败')
} finally {
resultsLoading.value = false
}
}
// 删除评估记录
const deleteEvaluationRecord = async (taskId) => {
try {
// 找到对应的记录并设置loading状态
const record = evaluationHistory.value.find((r) => r.task_id === taskId)
if (record) {
record.deleting = true
}
const response = await evaluationApi.deleteEvaluationResultByDb(props.databaseId, taskId)
if (response.message === 'success') {
message.success('删除成功')
// 重新加载评估历史
await loadEvaluationHistory()
}
} catch (error) {
console.error('删除评估记录失败:', error)
message.error('删除评估记录失败')
} finally {
// 清除loading状态
const record = evaluationHistory.value.find((r) => r.task_id === taskId)
if (record) {
record.deleting = false
}
}
}
// 工具函数
const truncateText = (text, maxLength) => {
if (!text) return ''
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text
}
const formatTime = (timeStr) => {
if (!timeStr) return '-'
const date = new Date(timeStr)
return date.toLocaleString('zh-CN')
}
const getScoreColor = (score) => {
if (score >= 0.8) return 'var(--color-success-500)'
if (score >= 0.6) return 'var(--color-warning-500)'
return 'var(--color-error-500)'
}
const getScoreTagColor = (score) => {
if (score >= 0.8) return 'success'
if (score >= 0.6) return 'warning'
return 'error'
}
const getStatusColor = (status) => {
const colors = {
running: 'blue',
completed: 'green',
failed: 'red',
paused: 'orange'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
running: '运行中',
completed: '已完成',
failed: '失败',
paused: '已暂停'
}
return texts[status] || status
}
const getMetricTitle = (key) => {
const titles = {
precision: '精确率',
recall: '召回率',
map: '平均精度',
ndcg: 'NDCG',
bleu: 'BLEU分数',
rouge: 'ROUGE分数',
answer_correctness: '答案准确性',
score: '评分',
reasoning: '理由',
overall_score: '综合评分'
}
// 处理 recall@k
if (key.startsWith('recall@')) return `召回率 (${key.split('@')[1]})`
if (key.startsWith('precision@')) return `精确率 (${key.split('@')[1]})`
return titles[key] || key
}
// 获取指标类型
const getMetricType = (key) => {
if (key.startsWith('recall')) return 'recall'
if (key.startsWith('precision')) return 'precision'
if (key === 'map') return 'map'
if (key === 'ndcg') return 'ndcg'
return 'default'
}
// 获取指标短名称
const getMetricShortName = (key) => {
if (key.startsWith('recall@')) return `R@${key.split('@')[1]}`
if (key.startsWith('precision@')) return `P@${key.split('@')[1]}`
if (key === 'precision') return 'Precision'
if (key === 'recall') return 'Recall'
if (key === 'map') return 'MAP'
if (key === 'ndcg') return 'NDCG'
return key
}
// 格式化指标值
const formatMetricValue = (val) => {
if (typeof val !== 'number') return '-'
// 检索指标recall, precision, f1 等)范围是 0.0-1.0,统一转换为百分比
if (val <= 1) return (val * 100).toFixed(1) + '%'
return val.toFixed(3)
}
// 格式化持续时间
const formatDuration = (seconds) => {
if (seconds < 60) {
return `${Math.round(seconds)}秒`
} else if (seconds < 3600) {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = Math.round(seconds % 60)
return `${minutes}分${remainingSeconds}秒`
} else {
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
return `${hours}小时${minutes}分`
}
}
// 组件挂载时加载数据
onMounted(() => {
loadBenchmarks()
loadEvaluationHistory()
})
</script>
<style lang="less" scoped>
.rag-evaluation-container {
height: 100%;
background: var(--gray-0);
display: flex;
flex-direction: column;
}
// 顶部工具栏
.toolbar {
padding: 12px 16px;
background: var(--gray-0);
border-bottom: 1px solid var(--gray-200);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
.toolbar-left {
display: flex;
align-items: center;
}
.benchmark-selector {
display: flex;
align-items: center;
gap: 12px;
.selector-label {
font-size: 14px;
font-weight: 500;
color: var(--gray-700);
margin: 0;
white-space: nowrap;
}
.refresh-benchmarks-btn {
color: var(--gray-600);
}
}
.toolbar-right {
display: flex;
align-items: center;
gap: 6px;
}
}
// 评估内容区域
.evaluation-content {
flex: 1;
overflow: hidden;
min-height: 0;
padding: 10px 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
// 评估结果区域
.evaluation-results {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
overflow: auto;
padding: 16px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 3px;
&:hover {
background-color: rgba(0, 0, 0, 0.3);
}
}
}
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: var(--gray-0);
border-radius: 8px;
border: 1px solid var(--gray-200);
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: flex-start;
.progress-stats {
flex: 1;
margin-right: 24px;
min-width: 300px;
.ant-statistic {
margin-bottom: 12px;
.ant-statistic-title {
font-size: 13px;
color: var(--gray-600);
}
.ant-statistic-content {
font-size: 18px;
font-weight: 500;
}
}
}
.progress-actions {
flex-shrink: 0;
padding-top: 24px;
}
}
.query-text {
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;
}
.answer-text {
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;
color: var(--gray-700);
}
.log-time {
color: var(--gray-500);
margin-left: 8px;
font-size: 12px;
}
// 优化表格样式
:deep(.ant-table) {
.ant-table-tbody > tr > td {
padding: 12px 12px;
vertical-align: top;
}
.ant-table-thead > tr > th {
padding: 8px 12px;
font-weight: 500;
background-color: var(--gray-50);
}
}
// 优化卡片间距
:deep(.ant-card) {
.ant-card-head {
padding: 8px 16px;
min-height: 40px;
.ant-card-head-title {
font-size: 14px;
font-weight: 500;
padding: 4px 0;
}
}
}
// 优化时间线样式
:deep(.ant-timeline) {
.ant-timeline-item-content {
margin-left: 20px;
padding-bottom: 12px;
}
}
// 优化描述列表样式
:deep(.ant-descriptions) {
.ant-descriptions-item-label {
font-size: 13px;
font-weight: 500;
color: var(--gray-600);
}
.ant-descriptions-item-content {
font-size: 13px;
}
}
// 优化表单项间距
:deep(.ant-form) {
.ant-form-item {
margin-bottom: 16px;
}
}
:deep(.ant-form-inline) {
.ant-form-item {
margin-right: 24px;
margin-bottom: 16px;
&:last-child {
margin-right: 0;
}
}
}
// 优化模型配置表单项
.model-config-section {
padding: 6px 8px;
background: var(--gray-10);
border-radius: 8px;
border: 1px solid var(--gray-150);
.ant-form-item {
margin-bottom: 0;
.ant-form-item-label {
font-weight: 500;
}
.ant-form-item-extra {
font-size: 12px;
color: var(--gray-500);
margin-top: 4px;
}
}
// 为模型配置部分内的列添加特定样式
.ant-col {
&:not(:last-child) .ant-form-item {
padding-right: 12px;
}
}
}
// 优化统计数字样式
:deep(.ant-row) {
.ant-col {
.ant-statistic {
padding: 12px;
border: 1px solid var(--gray-200);
border-radius: 6px;
text-align: center;
transition: all 0.3s;
&:hover {
border-color: var(--gray-300);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
}
}
}
// 检索指标样式
.retrieval-metrics {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-height: 20px;
align-items: center;
}
.metric-item {
display: flex;
align-items: center;
}
.metric-content {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
line-height: 1.4;
font-weight: 500;
white-space: nowrap;
&.metric-recall {
background-color: var(--color-info-50);
color: var(--color-info-900);
}
&.metric-precision {
background-color: var(--color-success-50);
color: var(--color-success-900);
}
&.metric-map,
&.metric-ndcg {
background-color: var(--color-accent-50);
color: var(--color-accent-900);
}
}
.metric-name {
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
}
.metric-value {
font-weight: 700;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
}
.no-metrics {
color: var(--gray-400);
font-style: italic;
}
// 答案推理样式
.answer-reasoning {
font-size: 12px;
color: var(--gray-600);
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);
}
}
// 加载和空状态样式
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
}
.empty-results {
padding: 40px 0;
text-align: center;
}
// 评估报告样式
.evaluation-report {
margin-bottom: 20px;
.report-metric {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--gray-100);
&:last-child {
border-bottom: none;
}
.metric-label {
font-size: 14px;
padding-right: 18px;
color: var(--gray-700);
}
.metric-value {
font-size: 14px;
font-weight: 600;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
}
}
.accuracy-stats {
.accuracy-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--gray-100);
&:last-child {
border-bottom: none;
}
.accuracy-label {
font-size: 14px;
color: var(--gray-700);
}
.accuracy-value {
font-size: 16px;
font-weight: 600;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
}
}
}
:deep(.ant-card) {
.ant-card-head {
border-bottom: 1px solid var(--gray-200);
.ant-card-head-title {
font-size: 14px;
font-weight: 500;
}
}
}
}
// 历史评估记录区域
.history-section {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.section-title {
margin: 0;
font-size: 14px;
font-weight: 500;
color: var(--gray-700);
}
.refresh-btn {
color: var(--gray-600);
border: none;
box-shadow: none;
padding: 4px 8px;
height: auto;
font-size: 13px;
&:hover {
color: var(--color-primary-600);
background-color: var(--color-primary-50);
}
&:active {
color: var(--color-primary-700);
background-color: var(--color-primary-100);
}
.anticon {
font-size: 14px;
}
}
}
:deep(.ant-table) {
border: 1px solid var(--gray-100);
}
}
// JSON 查看器样式
.json-viewer-container {
max-height: 400px;
overflow: auto;
.json-viewer {
margin: 0;
padding: 0;
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Menlo', monospace;
font-size: 13px;
line-height: 1.5;
color: var(--gray-800);
white-space: pre-wrap;
word-wrap: break-word;
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: 6px;
padding: 12px;
}
}
// 仅查看错误按钮样式
.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>