feat(knowledge): 优化 hit-test 功能

- 新增 Rerank 模型支持
- 添加权重设置功能
- 优化界面布局和交互
- 增加内容详情弹窗
- 调整样式,提升可读性
This commit is contained in:
Huangzhe
2025-04-30 09:42:12 +08:00
parent 97180035e0
commit 3bad79f9b8
9 changed files with 379 additions and 114 deletions

View File

@@ -0,0 +1,12 @@
import request from '@/assets/js/utils/request'
import getUrl from '@/assets/js/utils/get-url'
/**
* @description 获取所有的 rerank 模型列表
*/
export function getRerankModels() {
return request({
url: getUrl("/third/models/rerank"),
method: 'get',
})
}

View File

@@ -258,6 +258,29 @@ h3 {
@include text-overflow; @include text-overflow;
} }
/* 多行文本省略 */
.multi-ellipsis-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.multi-ellipsis-3 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
/* 可以根据需要添加更多行数 */
.multi-ellipsis-4 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
overflow: hidden;
}
//颜色 //颜色
.green { .green {
color: $green !important; color: $green !important;

View File

@@ -80,7 +80,7 @@
</div> </div>
</template> </template>
<script> <script>
import Cropper from 'cropperjs' // import Cropper from 'cropperjs'
export default { export default {
name: 'index', name: 'index',

View File

@@ -0,0 +1,65 @@
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'RenderSlider',
props: {
/**
* @description 当前值
*/
value: {
type: Number,
default: 0,
},
/**
* @description 是否显示 tooltip
*/
showTooltip: {
type: Boolean,
default: true,
},
max: {
type: Number,
default: 1,
},
step: {
type: Number,
default: 0.1,
},
min: {
type: Number,
default: 0,
},
marks: {
type: Object,
default: () => {},
},
hitTest: {
type: Boolean,
default: false,
},
},
model: {
prop: 'value',
event: 'input',
},
})
</script>
<template>
<div class="grid grid-cols-3 grid-rows-2">
<el-slider
class="col-span-3 row-span-1"
v-model="value"
:show-tooltip="showTooltip"
@input="$emit('input', $event)"
:marks="hitTest ? marks : {}"
:step="step"
:min="min"
:max="max"
/>
<!-- 靠左显示 -->
<div v-if="hitTest" class="col-span-2 row-span-1" style="justify-self: start">语义 {{ (max - value).toFixed(1) }}</div>
<!-- 靠右显示 -->
<div v-if="hitTest" class="col-span-1 row-span-1" style="justify-self: end">关键词 {{ value.toFixed(1) }}</div>
</div>
</template>

View File

@@ -410,19 +410,23 @@ export default {
padding: 5px; padding: 5px;
} }
/deep/.el-table { /deep/ .el-table {
.el-radio__label { .el-radio__label {
padding: unset; padding: unset;
} }
.el-form-item__error { .el-form-item__error {
position: unset; position: unset;
} }
.el-form-item__content { .el-form-item__content {
line-height: unset; line-height: unset;
} }
.el-form-item__error { .el-form-item__error {
position: unset; position: unset;
} }
.el-form-item { .el-form-item {
margin: 0; margin: 0;
} }

View File

@@ -12,6 +12,7 @@ import RenderSwiper from './components/RenderSwiper'
import VueEditor from './components/VueEditor' import VueEditor from './components/VueEditor'
import MavonEditor from './components/MavonEditor' import MavonEditor from './components/MavonEditor'
import RenderMinerU from '@/components/RenderMinerU/index.vue' import RenderMinerU from '@/components/RenderMinerU/index.vue'
import RenderSlider from '@/components/RenderSlider/Index.vue'
import utils from '@/assets/js/common' import utils from '@/assets/js/common'
// 生成的数据交互api // 生成的数据交互api
import generatedFormat from '@/assets/js/generatedFormat' import generatedFormat from '@/assets/js/generatedFormat'
@@ -40,7 +41,8 @@ Vue.component('RMinerU', RenderMinerU)
Vue.component('VEditor', VueEditor) Vue.component('VEditor', VueEditor)
// 富文本编辑器 可视化代码 // 富文本编辑器 可视化代码
Vue.component('MEditor', MavonEditor) Vue.component('MEditor', MavonEditor)
Vue.prototype.$messageBox = function(isOk, message, type, title) { Vue.component('RSlider', RenderSlider)
Vue.prototype.$messageBox = function (isOk, message, type, title) {
this.$confirm( this.$confirm(
message ? message : '是否确认删除当前数据', message ? message : '是否确认删除当前数据',
title ? title : '提示', title ? title : '提示',

View File

@@ -1,53 +1,132 @@
<script> <script>
import { hitTest } from '@/api/generatedApi' import { hitTest } from '@/api/generatedApi'
import { getRerankModels } from '@/api/knowledge/hit-test'
export default { export default {
name: 'hitTest', name: 'hitTest',
data() { data() {
return { return {
testForm: {
isRanking: true
},
datasetId: void 0, datasetId: void 0,
params: {
search_method: 'hybrid_search',
score_threshold_enable: false,
score_threshold: 0,
top_k: 5,
reranking_enable: false,
reranking_model: {
reranking_provider_name: '',
reranking_model_name: ''
}
},
paramsConfig: { paramsConfig: {
weights: 0,
visible: false, visible: false,
searchMode: 'hybrid_search', // 默认选择向量检索 /**
score_threshold: 0.6, // 默认相似度 * @description 是否启用 Rerank 模型
*/
reranking_enable: true,
/**
* @description 检索方法
*/
search_method: 'hybrid_search', // 默认选择混合检索
/**
* @description score 阈值
*/
score_threshold: 0,
/**
* @description TOPK 设置
*/
top_k: 5, // 默认引用分段数, top_k: 5, // 默认引用分段数,
reranking_model: '' /**
* @description rerank 模型名称
*/
reranking_model: '',
/**
* @description 模型列表
*/
rerankModelOptions: []
}, },
inputMessage: '', inputMessage: '',
loading: false, loading: false,
messages: [] messages: [],
// 对话框配置
dialogConfig: {
visible: false,
title: '查看详情',
width: '500px',
isShowFooter: false
}
}
},
watch: {
'paramsConfig.reranking_enable': {
handler() {
// 如果 isRanking 为 false清空 reranking_model, 防止错误提交
if (!this.paramsConfig.reranking_enable) {
// 删除 params 中的 reranking_model
delete this.params.reranking_model
return
}
this.paramsConfig.reranking_model = void 0
},
deep: true
} }
}, },
created() { created() {
// 从 路由参数中获取datasetId // 从 路由参数中获取datasetId
this.datasetId = this.$route.query.datasetId this.datasetId = this.$route.query.datasetId
// 获取 rerank 模型列表
getRerankModels().then(res => {
const { content } = res.content
this.paramsConfig.rerankModelOptions = content.flatMap(
item => item.models
)
})
}, },
methods: { methods: {
sendMessage() { handleEnsureClick() {
if (!this.inputMessage.trim()) return // 关闭内容
this.paramsConfig.visible = false
// 更新 params
this.params = {
search_method: this.paramsConfig.searchMode,
score_threshold: this.paramsConfig.score_threshold,
top_k: this.paramsConfig.top_k,
reranking_model: this.paramsConfig.reranking_model,
reranking_enable: this.paramsConfig.reranking_enable
}
},
sendMessage() {
const contentArea = this.$refs.contentArea
if (!this.inputMessage.trim()) return
// 显示骨架屏 // 显示骨架屏
this.loading = true this.loading = true
// 发送请求 // 发送请求
hitTest({ hitTest({
datasetsId: this.datasetId, datasetsId: this.datasetId,
query: this.inputMessage query: this.inputMessage,
retrieval_model: this.params
}).then(res => { }).then(res => {
const { content } = res.content const { content } = res.content
this.messages.push(content) this.messages.push(content)
this.loading = false this.loading = false
this.$nextTick(() => {
// 滚动到底部
contentArea.scrollTop = contentArea.scrollHeight
})
}) })
// 清空输入框 // 清空输入框
this.inputMessage = '' this.inputMessage = ''
}, },
formatScore(score) { formatScore(score) {
return score.toFixed(3) return score.toFixed(3)
},
handleCardClick(contentItem) {
this.dialogConfig.visible = true
this.dialogConfig.content = contentItem
} }
} }
} }
@@ -56,7 +135,7 @@ export default {
<template> <template>
<div class="hit-test-container"> <div class="hit-test-container">
<!-- 中间内容区域 --> <!-- 中间内容区域 -->
<div class="content-area"> <div ref="contentArea" class="content-area">
<div class="empty-state" v-if="messages.length === 0"> <div class="empty-state" v-if="messages.length === 0">
<el-image alt="当前会话尚无记录"></el-image> <el-image alt="当前会话尚无记录"></el-image>
<p>这里会显示命中测试的记录</p> <p>这里会显示命中测试的记录</p>
@@ -64,60 +143,85 @@ export default {
<div <div
class="messages mb10" class="messages mb10"
v-else v-else
v-for="content in messages" v-for="(content, index) in messages"
:key="content" :key="index"
> >
<!-- 对话区域 --> <!-- 对话区域 -->
<el-card> <el-card>
<!-- 头像 会话内容 --> <!-- 头像 会话内容 -->
<div class="flex align-items-c mb10"> <div class="flex align-items-c mb10">
<el-avatar :size="40" class="user-avatar" shape="square"> <!-- <el-avatar
:size="40"
class="user-avatar"
shape="square"
>
admin admin
</el-avatar> </el-avatar> -->
<div style="margin: 0 10px;"> <div style="margin: 0 10px">根据您的问题我们为你提供以下建议</div>
根据您的问题我们为你提供以下建议
</div>
</div> </div>
<el-card <el-card
v-for="(contentItem, index) in content" v-for="contentItem in content"
:key="index" :key="contentItem.segment.content"
shadow="hover" shadow="hover"
body-style="padding: 10px;backgroundColor: f2f2f2" body-style="padding: 10px;backgroundColor: f2f2f2"
class="mb10" class="mb10"
> >
<div class="flex justify-content-b"> <div
class="flex justify-content-b"
@click="handleCardClick(contentItem)"
>
<!-- 内容详情 --> <!-- 内容详情 -->
<section> <section>
<p class="mb10">{{ contentItem.segment.content }}</p> <p class="mb10 fs13 multi-ellipsis-2">
{{ contentItem.segment.content }}
</p>
<el-tag <el-tag
type="success" class="mr5 mb5"
type="info"
size="small" size="small"
v-for="(keyword, index) in contentItem.segment.keywords" v-for="(keyword, index) in contentItem.segment.keywords"
:key="index" :key="index + keyword"
> >
{{ keyword }} {{ index + keyword }}
#{{ keyword }}
</el-tag> </el-tag>
</section> </section>
<!-- score --> <!-- score -->
<span class="score">{{ contentItem.score.toFixed(3) }}</span> <span class="score"
><el-tag type="success"
>score: {{ contentItem.score.toFixed(3) }}</el-tag
></span
>
</div> </div>
</el-card> </el-card>
</el-card> </el-card>
<!-- 加载状态 --> <!-- 加载状态 -->
<div v-if="loading" class="loading-message"> <!-- <div
<el-skeleton style="width: 100%" animated> v-if="loading"
class="loading-message"
>
<el-skeleton
style="width: 100%"
animated
>
<template slot="template"> <template slot="template">
<el-skeleton-item <el-skeleton-item
variant="p" variant="p"
style="width: 100%; height: 60px;" style="width: 100%; height: 60px"
/>
<el-skeleton-item
variant="p"
style="width: 90%; height: 20px"
/>
<el-skeleton-item
variant="p"
style="width: 80%; height: 20px"
/> />
<el-skeleton-item variant="p" style="width: 90%; height: 20px;" />
<el-skeleton-item variant="p" style="width: 80%; height: 20px;" />
</template> </template>
</el-skeleton> </el-skeleton>
</div> </div> -->
</div> </div>
</div> </div>
@@ -136,17 +240,17 @@ export default {
<h3>检索模式</h3> <h3>检索模式</h3>
<el-radio-group <el-radio-group
v-model="paramsConfig.searchMode" v-model="paramsConfig.search_method"
class="search-mode-options" class="search-mode-options"
> >
<div <div
class="search-mode-option" class="search-mode-option"
:class="{ :class="{
active: paramsConfig.searchMode === 'semantic_search' active: paramsConfig.search_method === 'semantic_search'
}" }"
> >
<el-radio <el-radio
v-model="paramsConfig.searchMode" v-model="paramsConfig.search_method"
label="semantic_search" label="semantic_search"
key="semantic_search" key="semantic_search"
> >
@@ -160,11 +264,11 @@ export default {
<div <div
class="search-mode-option" class="search-mode-option"
:class="{ :class="{
active: paramsConfig.searchMode === 'full_text_search' active: paramsConfig.search_method === 'full_text_search'
}" }"
> >
<el-radio <el-radio
v-model="paramsConfig.searchMode" v-model="paramsConfig.search_method"
label="full_text_search" label="full_text_search"
key="full_text_search" key="full_text_search"
> >
@@ -178,11 +282,11 @@ export default {
<div <div
class="search-mode-option" class="search-mode-option"
:class="{ :class="{
active: paramsConfig.searchMode === 'hybrid_search' active: paramsConfig.search_method === 'hybrid_search'
}" }"
> >
<el-radio <el-radio
v-model="paramsConfig.searchMode" v-model="paramsConfig.search_method"
label="hybrid_search" label="hybrid_search"
key="hybrid_search" key="hybrid_search"
> >
@@ -201,7 +305,7 @@ export default {
<el-button <el-button
type="primary" type="primary"
size="small" size="small"
@click="paramsConfig.visible = false" @click="handleEnsureClick"
>确定 >确定
</el-button> </el-button>
</div> </div>
@@ -211,9 +315,15 @@ export default {
<!-- 具体的参数内容 --> <!-- 具体的参数内容 -->
<section> <section>
<div class="grid grid-cols-2"> <div class="grid grid-cols-2">
<div class="col-span-1 flex align-items-c"> <div class="col-span-1 flex align-items-c ">
<span class="param-label mr5">Score 阈值</span> <span class="mr5 flex" style="flex-flow: column nowrap">
<el-switch
v-model="paramsConfig.score_threshold_enable"
></el-switch>
<span>Score 阈值</span>
</span>
<el-input-number <el-input-number
v-if="paramsConfig.score_threshold_enable"
v-model="paramsConfig.score_threshold" v-model="paramsConfig.score_threshold"
:precision="3" :precision="3"
:step="0.001" :step="0.001"
@@ -236,45 +346,80 @@ export default {
</div> </div>
<!-- 参数设置 --> <!-- 参数设置 -->
<section <section
class="mt10 flex fs12" class="mt10"
v-if="paramsConfig.searchMode === 'hybrid_search'" v-if="paramsConfig.search_method === 'hybrid_search'"
> >
<el-card shadow="never" class="mr5"> <el-radio-group
<div>权重设置</div> v-model="paramsConfig.reranking_enable"
<div> class="search-mode-options"
通过调整分配的权重重新排序策略确定是优先进行语义匹配还是关键字匹配 >
<div
class="search-mode-option"
:class="{ active: !paramsConfig.reranking_enable }"
>
<el-radio :label="false">
权重设置
<p class="option-desc">
通过调整分配的权重重新排序策略确定是优先进行语义匹配还是关键字匹配
</p>
</el-radio>
</div> </div>
</el-card> <div
<el-card shadow="never"> class="search-mode-option"
<div>Rerank 模型</div> :class="{ active: paramsConfig.reranking_enable }"
<div> >
重排序模型将根据候选文档列表与用户问题语义匹配度进行重新排序从而改进语义排序的结果 <el-radio :label="true">
Rerank 模型
<p class="option-desc">
重排序模型将根据候选文档列表与用户问题语义匹配度进行重新排序从而改进语义排序的结果
</p>
</el-radio>
</div> </div>
</el-card> </el-radio-group>
</section> </section>
<section class="mt10"> <!-- 当选择了 rerank 模型之后或者不是混合搜索的选项时显示 -->
<el-form> <section
<el-form-item> class="mt15"
<template #label> v-if="
<el-switch v-model="testForm.isRanking"></el-switch> paramsConfig.reranking_enable ||
Rerank 模型 paramsConfig.search_method !== 'hybrid_search'
</template> "
<el-select >
v-model="paramsConfig.rerank_model" <!-- 当没有设置权重或者不是混合检索时显示-->
size="medium" <div
placeholder="选择一个模型" class="mb15"
> v-if="
<el-option paramsConfig.reranking_enable ||
v-for="item in rerankModelOptions" paramsConfig.search_method !== 'hybrid_search'
:key="item.value" "
:label="item.label" >
:value="item.value" <el-switch v-model="paramsConfig.reranking_enable"></el-switch>
> <span> Rerank 模型</span>
</el-option> </div>
</el-select> <el-select
</el-form-item> v-if="paramsConfig.reranking_enable"
</el-form> v-model="paramsConfig.rerank_model"
size="medium"
placeholder="选择一个模型"
>
<el-option
v-for="item in paramsConfig.rerankModelOptions"
:key="item.model"
:label="item.model"
:value="item.value"
>
{{ item.model }}
</el-option>
</el-select>
</section>
<!-- 权重设置 -->
<section v-else>
<RSlider
v-model="paramsConfig.weights"
:hit-test="true"
:show-tooltip="false"
/>
</section> </section>
</section> </section>
</el-popover> </el-popover>
@@ -298,11 +443,27 @@ export default {
</template> </template>
</el-input> </el-input>
</div> </div>
<el-drawer
append-to-body
:modal="false"
:visible.sync="dialogConfig.visible"
:title="dialogConfig.title"
:width="dialogConfig.width"
:is-show-footer="dialogConfig.isShowFooter"
>
<div class="flex">
<div>{{ dialogConfig.content }}</div>
</div>
</el-drawer>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.hit-test-container { .hit-test-container {
font-family: 'PingFang SC', sans-serif;
transition: all 0.5s;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 86vh; height: 86vh;
@@ -312,7 +473,7 @@ export default {
overflow: hidden; overflow: hidden;
.header { .header {
padding: 10px 20px; // padding: 10px 20px;
border-bottom: 1px solid #e4e7ed; border-bottom: 1px solid #e4e7ed;
background-color: #fff; background-color: #fff;
} }
@@ -320,7 +481,7 @@ export default {
.content-area { .content-area {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -493,6 +654,31 @@ export default {
} }
} }
.search-mode-options {
width: 100%;
margin-bottom: 20px;
.search-mode-option {
padding: 10px;
border: 1px solid #e4e7ed;
border-radius: 4px;
margin-bottom: 10px;
&.active {
border-color: #409eff;
background-color: #ecf5ff;
}
.option-desc {
margin: 5px 0 0 24px;
font-size: 12px;
text-wrap: wrap;
color: #909399;
line-height: 1.4;
}
}
}
// 参数设置弹窗样式 // 参数设置弹窗样式
.search-params-container { .search-params-container {
padding: 10px; padding: 10px;
@@ -504,31 +690,6 @@ export default {
font-weight: 500; font-weight: 500;
} }
.search-mode-options {
width: 100%;
margin-bottom: 20px;
.search-mode-option {
padding: 10px;
border: 1px solid #e4e7ed;
border-radius: 4px;
margin-bottom: 10px;
&.active {
border-color: #409eff;
background-color: #ecf5ff;
}
.option-desc {
margin: 5px 0 0 24px;
font-size: 12px;
text-wrap: wrap;
color: #909399;
line-height: 1.4;
}
}
}
.action-buttons { .action-buttons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@@ -538,8 +699,4 @@ export default {
} }
} }
} }
.ts {
transition: all 0.5;
}
</style> </style>

View File

@@ -51,6 +51,7 @@ import {
import RenderFile from '@/components/RenderFile/Index.vue' import RenderFile from '@/components/RenderFile/Index.vue'
import knowledgePng_2 from '@/assets/images/konwledge/knowledge-2.png' import knowledgePng_2 from '@/assets/images/konwledge/knowledge-2.png'
import MetadataOperator from '@/views/knowledge/detail/components/metaData/MetadataOperator.vue' import MetadataOperator from '@/views/knowledge/detail/components/metaData/MetadataOperator.vue'
export default { export default {
components: { components: {
MetadataOperator, MetadataOperator,

View File

@@ -36,7 +36,8 @@ module.exports = {
}, },
proxy: { proxy: {
[DIFY_URL]: { [DIFY_URL]: {
target: 'http://localhost:3000', // target: 'http://localhost:3000',
target: 'http://10.147.17.115:3000',
changeOrigin: true, changeOrigin: true,
onProxyRes: (proxyRes, req, res) => { onProxyRes: (proxyRes, req, res) => {
delete proxyRes.headers['x-frame-options'] delete proxyRes.headers['x-frame-options']