refactor(eval): 重构检索配置为弹窗并优化评估界面

- 将检索配置从标签页改为弹窗形式
- 在查询区和评估区添加检索配置入口
- 优化评估历史记录显示和刷新功能
- 移除不再需要的组件和样式
This commit is contained in:
Wenjie Zhang 2025-12-09 22:11:27 +08:00
parent e5a06405ba
commit 6d3bef49dc
6 changed files with 239 additions and 124 deletions

View File

@ -16,16 +16,13 @@
- conversation 待修改为异步的版本
- DBManager 需要将数据库修改为异步的aiosqlite或者异步mysql缓存使用Redis存储
- 【eval】缺少自动生成评估的功能
- chat_model 的 call 需要异步
### Bugs
- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)
- DeepSeek 官方接口适配会出现问题
- 目前的知识库的图片存在公开访问风险
- 深度分析智能体需要考虑上下文超限的问题
- 【eval】检索评估缺少检索参数的内容
- 【eval】总体评分替换为确定的某个指标最好是在右侧可以选择可行的指标
- 【eval】检索配置应该修改为一个弹窗
- 【eval】当前的检索配置仅保存在本地实际并没有保存在知识库的元数据中
### 新增
- 优化知识库详情页面,更加简洁清晰

View File

@ -1,15 +1,5 @@
<template>
<div class="mindmap-section">
<div class="section-header">
<div class="header-left">
<BrainCircuit :size="16" />
<span>知识导图</span>
<a-tag v-if="!loading && mindmapData" color="blue" size="small">
已生成
</a-tag>
</div>
</div>
<div class="section-content">
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
@ -65,7 +55,6 @@
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { message } from 'ant-design-vue'
import {
BrainCircuit,
RefreshCw,
Network,
Sparkles,
@ -315,21 +304,6 @@ onUnmounted(() => {
min-height: 0;
}
.section-header {
padding: 10px 16px;
background: var(--gray-25);
border-bottom: 1px solid var(--border-color);
.header-left {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 500;
color: var(--text-color);
}
}
.section-content {
flex: 1;
min-height: 200px;
@ -374,7 +348,7 @@ onUnmounted(() => {
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: flex-start;
justify-content: flex-end;
.toolbar-text {
margin-left: 4px;
@ -382,7 +356,6 @@ onUnmounted(() => {
}
:deep(.ant-btn-text) {
padding: 4px 12px;
display: flex;
align-items: center;

View File

@ -14,6 +14,17 @@
/>
<div class="search-actions">
<div class="query-examples-compact">
<!-- 检索配置按钮 -->
<a-tooltip title="检索配置" placement="bottom">
<a-button
type="text"
size="small"
class="config-label-btn"
@click="openSearchConfigModal"
>
<SettingOutlined />
</a-button>
</a-tooltip>
<div class="examples-label-group">
<a-tooltip title="点击手动生成测试问题" placement="bottom">
<a-button
@ -138,6 +149,13 @@
</div>
</div>
</div>
<!-- 检索配置弹窗 -->
<SearchConfigModal
v-model="searchConfigModalVisible"
:database-id="store.database?.db_id"
@save="handleSearchConfigSave"
/>
</template>
<script setup>
@ -148,7 +166,9 @@ import { queryApi } from '@/apis/knowledge_api';
import {
SearchOutlined,
ReloadOutlined,
SettingOutlined,
} from '@ant-design/icons-vue';
import SearchConfigModal from './SearchConfigModal.vue';
const store = useDatabaseStore();
@ -175,10 +195,24 @@ const queryExamples = ref([]);
const currentExampleIndex = ref(0);
const loadingQuestions = ref(false);
const generatingQuestions = ref(false);
const searchConfigModalVisible = ref(false);
//
let exampleCarouselInterval = null;
//
//
const openSearchConfigModal = () => {
searchConfigModalVisible.value = true;
};
//
const handleSearchConfigSave = (config) => {
console.log('查询测试中的检索配置已更新:', config);
//
};
//
const loadSampleQuestions = async () => {
if (!store.database?.db_id) return;
@ -595,12 +629,31 @@ defineExpose({
flex-wrap: wrap;
}
.config-label-btn {
color: var(--gray-500);
font-size: 12px;
display: flex;
align-items: center;
padding: 0 4px;
margin-left: -4px;
&:hover {
color: var(--main-color);
background-color: var(--gray-100);
}
.anticon {
font-size: 14px;
}
}
.examples-label-btn {
color: var(--gray-500);
font-size: 12px;
display: flex;
align-items: center;
margin-left: -8px;
margin-right: -6px;
&:hover {
color: var(--main-color);

View File

@ -9,7 +9,7 @@
<a-select
v-model:value="selectedBenchmarkId"
placeholder="请选择评估基准"
style="width: 300px"
style="width: 180px"
@change="onBenchmarkChanged"
:loading="benchmarksLoading"
>
@ -22,8 +22,15 @@
</a-select-option>
</a-select>
</div>
</div>
</div>
<div class="toolbar-right">
<!-- 检索配置按钮 -->
<a-button
size="middle"
@click="openSearchConfigModal"
:icon="h(SettingOutlined)"
/>
<!-- 开始评估按钮 -->
<a-button
type="primary"
@ -84,10 +91,22 @@
</div>
</template>
<template v-else>
<!-- 历史评估记录 -->
<div class="history-section">
<h4 class="section-title">历史评估记录</h4>
<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"
@ -242,9 +261,9 @@
</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('f1') || k.startsWith('precision') || k === 'map' || k === 'ndcg')" class="retrieval-metrics">
<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('f1') || key.startsWith('precision') || key === 'map' || key === 'ndcg'"
<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>
@ -277,13 +296,22 @@
</a-empty>
</div>
</a-modal>
<!-- 检索配置弹窗 -->
<SearchConfigModal
v-model="searchConfigModalVisible"
:database-id="databaseId"
@save="handleSearchConfigSave"
/>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { ref, reactive, onMounted, computed, h } from 'vue';
import { message } 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';
const props = defineProps({
databaseId: {
@ -306,6 +334,9 @@ const selectedResult = ref(null);
const detailedResults = ref([]);
const evaluationStats = ref({});
const resultsLoading = ref(false);
const searchConfigModalVisible = ref(false);
const refreshingHistory = ref(false);
// 使
const configForm = reactive({
@ -358,10 +389,36 @@ const historyColumns = [
width: 100
},
{
title: '总体评分',
dataIndex: 'overall_score',
key: 'overall_score',
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: '操作',
@ -378,6 +435,17 @@ const currentBenchmark = computed(() => {
return availableBenchmarks.value.find(b => b.benchmark_id === selectedBenchmarkId.value);
});
//
const openSearchConfigModal = () => {
searchConfigModalVisible.value = true;
};
//
const handleSearchConfigSave = (config) => {
console.log('RAG评估中的检索配置已更新:', config);
//
};
//
const loadBenchmarks = async () => {
if (!props.databaseId) return;
@ -389,12 +457,19 @@ const loadBenchmarks = async () => {
if (response && response.message === 'success' && Array.isArray(response.data)) {
availableBenchmarks.value = response.data;
//
if (selectedBenchmarkId.value) {
//
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);
}
}
} else {
@ -415,6 +490,21 @@ const onBenchmarkChanged = (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) {
@ -464,6 +554,7 @@ const loadEvaluationHistory = async () => {
}
};
//
const calculateEvaluationStats = (results) => {
if (!results || results.length === 0) {
@ -493,7 +584,7 @@ const calculateEvaluationStats = (results) => {
//
if (item.metrics) {
Object.keys(item.metrics).forEach(key => {
if (key.startsWith('recall') || key.startsWith('f1') || key.startsWith('precision') || key === 'map' || key === 'ndcg') {
if (key.startsWith('recall') || key.startsWith('precision') || key === 'map' || key === 'ndcg') {
if (!metricSums[key]) {
metricSums[key] = 0;
metricCounts[key] = 0;
@ -643,7 +734,6 @@ const getMetricTitle = (key) => {
const titles = {
precision: '精确率',
recall: '召回率',
f1: 'F1分数',
map: '平均精度',
ndcg: 'NDCG',
bleu: 'BLEU分数',
@ -653,9 +743,8 @@ const getMetricTitle = (key) => {
reasoning: '理由',
overall_score: '综合评分'
};
// recall@k, f1@k
// recall@k
if (key.startsWith('recall@')) return `召回率 (${key.split('@')[1]})`;
if (key.startsWith('f1@')) return `F1 (${key.split('@')[1]})`;
if (key.startsWith('precision@')) return `精确率 (${key.split('@')[1]})`;
return titles[key] || key;
@ -665,7 +754,6 @@ const getMetricTitle = (key) => {
const getMetricType = (key) => {
if (key.startsWith('recall')) return 'recall';
if (key.startsWith('precision')) return 'precision';
if (key.startsWith('f1')) return 'f1';
if (key === 'map') return 'map';
if (key === 'ndcg') return 'ndcg';
return 'default';
@ -675,10 +763,8 @@ const getMetricType = (key) => {
const getMetricShortName = (key) => {
if (key.startsWith('recall@')) return `R@${key.split('@')[1]}`;
if (key.startsWith('precision@')) return `P@${key.split('@')[1]}`;
if (key.startsWith('f1@')) return `F1@${key.split('@')[1]}`;
if (key === 'precision') return 'Precision';
if (key === 'recall') return 'Recall';
if (key === 'f1') return 'F1';
if (key === 'map') return 'MAP';
if (key === 'ndcg') return 'NDCG';
return key;
@ -753,12 +839,9 @@ onMounted(() => {
}
.toolbar-right {
.ant-btn {
display: flex;
align-items: center;
gap: 6px;
border-radius: 6px;
}
display: flex;
align-items: center;
gap: 6px;
}
}
@ -1006,10 +1089,6 @@ onMounted(() => {
color: var(--color-success-900);
}
&.metric-f1 {
background-color: var(--color-warning-50);
color: var(--color-warning-900);
}
&.metric-map,
&.metric-ndcg {
@ -1128,12 +1207,43 @@ onMounted(() => {
//
.history-section {
.section-title {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 500;
color: var(--gray-700);
.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);
}

View File

@ -1,14 +1,15 @@
<template>
<div class="search-config-tab">
<div class="config-header">
<p class="config-title">检索配置</p>
<div class="config-actions">
<a-button size="small" @click="resetToDefaults">重置默认</a-button>
<a-button size="small" type="primary" @click="saveConfig">保存配置</a-button>
</div>
</div>
<div class="config-content">
<a-modal
:open="props.modelValue"
title="检索配置"
width="800px"
:confirm-loading="loading"
@ok="handleSave"
@cancel="handleCancel"
ok-text="保存"
cancel-text="取消"
>
<div class="search-config-modal">
<div v-if="loading" class="config-loading">
<a-spin size="large" />
<p>加载配置参数中...</p>
@ -66,22 +67,28 @@
</a-form>
</div>
</div>
</div>
</a-modal>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { ref, reactive, computed, watch } from 'vue';
import { useDatabaseStore } from '@/stores/database';
import { message } from 'ant-design-vue';
import { queryApi } from '@/apis/knowledge_api';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
databaseId: {
type: String,
required: true
}
});
const emit = defineEmits(['update:modelValue', 'save']);
const store = useDatabaseStore();
//
@ -193,7 +200,7 @@ const resetToDefaults = () => {
};
//
const saveConfig = async () => {
const handleSave = async () => {
// include_distances true
meta['include_distances'] = true;
@ -204,6 +211,13 @@ const saveConfig = async () => {
// localStorage
localStorage.setItem(`search-config-${props.databaseId}`, JSON.stringify(meta));
message.success('配置已保存');
// store
Object.assign(store.meta, meta);
//
emit('save', { ...meta });
emit('update:modelValue', false);
} else {
throw new Error(response.message || '保存失败');
}
@ -211,49 +225,22 @@ const saveConfig = async () => {
console.error('保存配置到知识库失败:', error);
message.error('保存配置失败:' + error.message);
}
// store
Object.assign(store.meta, meta);
};
//
onMounted(() => {
loadQueryParams();
//
const handleCancel = () => {
emit('update:modelValue', false);
};
//
watch(() => props.modelValue, (newVal) => {
if (newVal && props.databaseId) {
loadQueryParams();
}
});
</script>
<style lang="less" scoped>
.search-config-tab {
height: 100%;
display: flex;
flex-direction: column;
background: var(--gray-0);
}
.config-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
// border-bottom: 1px solid var(--gray-200);
flex-shrink: 0;
.config-title {
margin: 0;
color: var(--gray-800);
}
.config-actions {
display: flex;
gap: 8px;
}
}
.config-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
.config-loading,
.config-error {
@ -261,7 +248,7 @@ onMounted(() => {
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
height: 300px;
color: var(--gray-500);
p {
@ -271,10 +258,9 @@ onMounted(() => {
}
.config-forms {
max-width: 800px;
max-width: 100%;
}
//
:deep(.ant-form-item) {
margin-bottom: 16px;

View File

@ -41,9 +41,6 @@
ref="mindmapSectionRef"
/>
</a-tab-pane>
<a-tab-pane key="config" tab="检索配置">
<SearchConfigTab :database-id="databaseId" />
</a-tab-pane>
<a-tab-pane key="evaluation" tab="RAG评估Beta">
<RAGEvaluationTab
v-if="databaseId"
@ -84,7 +81,6 @@ import FileDetailModal from '@/components/FileDetailModal.vue';
import FileUploadModal from '@/components/FileUploadModal.vue';
import KnowledgeGraphSection from '@/components/KnowledgeGraphSection.vue';
import QuerySection from '@/components/QuerySection.vue';
import SearchConfigTab from '@/components/SearchConfigTab.vue';
import MindMapSection from '@/components/MindMapSection.vue';
import RAGEvaluationTab from '@/components/RAGEvaluationTab.vue';
import EvaluationBenchmarks from '@/components/EvaluationBenchmarks.vue';