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;
}
/* 多行文本省略 */
.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 {
color: $green !important;

View File

@@ -80,7 +80,7 @@
</div>
</template>
<script>
import Cropper from 'cropperjs'
// import Cropper from 'cropperjs'
export default {
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;
}
/deep/.el-table {
/deep/ .el-table {
.el-radio__label {
padding: unset;
}
.el-form-item__error {
position: unset;
}
.el-form-item__content {
line-height: unset;
}
.el-form-item__error {
position: unset;
}
.el-form-item {
margin: 0;
}

View File

@@ -12,6 +12,7 @@ import RenderSwiper from './components/RenderSwiper'
import VueEditor from './components/VueEditor'
import MavonEditor from './components/MavonEditor'
import RenderMinerU from '@/components/RenderMinerU/index.vue'
import RenderSlider from '@/components/RenderSlider/Index.vue'
import utils from '@/assets/js/common'
// 生成的数据交互api
import generatedFormat from '@/assets/js/generatedFormat'
@@ -40,7 +41,8 @@ Vue.component('RMinerU', RenderMinerU)
Vue.component('VEditor', VueEditor)
// 富文本编辑器 可视化代码
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(
message ? message : '是否确认删除当前数据',
title ? title : '提示',

View File

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

View File

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

View File

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