ソースを参照

【功能新增】AI:增加召回测试的实现

YunaiV 5 ヶ月 前
コミット
45ceacdcac

+ 16 - 8
src/api/ai/knowledge/segment/index.ts

@@ -30,20 +30,28 @@ export const KnowledgeSegmentApi = {
   deleteKnowledgeSegment: async (id: number) => {
     return await request.delete({ url: `/ai/knowledge/segment/delete?id=` + id })
   },
-  
+
   // 切片内容
   splitContent: async (url: string, segmentMaxTokens: number) => {
-    return await request.get({ 
-      url: `/ai/knowledge/segment/split`, 
-      params: { url, segmentMaxTokens } 
+    return await request.get({
+      url: `/ai/knowledge/segment/split`,
+      params: { url, segmentMaxTokens }
     })
   },
-  
+
   // 获取文档处理列表
   getKnowledgeSegmentProcessList: async (documentIds: number[]) => {
-    return await request.get({ 
-      url: `/ai/knowledge/segment/get-process-list`, 
-      params: { documentIds: documentIds.join(',') } 
+    return await request.get({
+      url: `/ai/knowledge/segment/get-process-list`,
+      params: { documentIds: documentIds.join(',') }
+    })
+  },
+
+  // 搜索知识库分片
+  searchKnowledgeSegment: async (params: any) => {
+    return await request.get({
+      url: `/ai/knowledge/segment/search`,
+      params
     })
   }
 }

+ 19 - 6
src/router/modules/remaining.ts

@@ -622,17 +622,18 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: 'console/knowledge/document',
+        path: 'knowledge/document',
         component: () => import('@/views/ai/knowledge/document/index.vue'),
         name: 'AiKnowledgeDocument',
         meta: {
           title: '知识库文档',
           icon: 'ep:document',
-          noCache: false
+          noCache: false,
+          activeMenu: '/ai/knowledge'
         }
       },
       {
-        path: 'console/knowledge/document/create',
+        path: 'knowledge/document/create',
         component: () => import('@/views/ai/knowledge/document/form/index.vue'),
         name: 'AiKnowledgeDocumentCreate',
         meta: {
@@ -640,11 +641,11 @@ const remainingRouter: AppRouteRecordRaw[] = [
           icon: 'ep:plus',
           noCache: true,
           hidden: true,
-          activeMenu: '/ai/console/knowledge/document'
+          activeMenu: '/ai/knowledge'
         }
       },
       {
-        path: 'console/knowledge/document/update',
+        path: 'knowledge/document/update',
         component: () => import('@/views/ai/knowledge/document/form/index.vue'),
         name: 'AiKnowledgeDocumentUpdate',
         meta: {
@@ -652,7 +653,19 @@ const remainingRouter: AppRouteRecordRaw[] = [
           icon: 'ep:edit',
           noCache: true,
           hidden: true,
-          activeMenu: '/ai/console/knowledge/document'
+          activeMenu: '/ai/knowledge'
+        }
+      },
+      {
+        path: 'knowledge/retrieval',
+        component: () => import('@/views/ai/knowledge/knowledge/retrieval/index.vue'),
+        name: 'AiKnowledgeRetrieval',
+        meta: {
+          title: '文档召回测试',
+          icon: 'ep:search',
+          noCache: true,
+          hidden: true,
+          activeMenu: '/ai/knowledge'
         }
       }
     ]

+ 0 - 2
src/views/ai/knowledge/document/form/SplitStep.vue

@@ -145,7 +145,6 @@ const splitContent = async (file: any) => {
     )
   } catch (error) {
     console.error('获取分段内容失败:', file, error)
-    message.error('获取分段内容失败')
   } finally {
     splitLoading.value = false
   }
@@ -214,7 +213,6 @@ const handleSave = async () => {
     }
   } catch (error: any) {
     console.error('保存失败:', modelData.value, error)
-    message.error(error.message)
   } finally {
     // 关闭按钮加载状态
     submitLoading.value = false

+ 17 - 1
src/views/ai/knowledge/knowledge/index.vue

@@ -95,6 +95,14 @@
           >
             文档
           </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="handleRetrieval(scope.row.id)"
+            v-hasPermi="['ai:knowledge:query']"
+          >
+            召回测试
+          </el-button>
           <el-button
             link
             type="danger"
@@ -191,11 +199,19 @@ const handleDelete = async (id: number) => {
 const router = useRouter()
 const handleDocument = (id: number) => {
   router.push({
-    path: '/ai/console/knowledge/document',
+    name: 'AiKnowledgeDocument',
     query: { knowledgeId: id }
   })
 }
 
+/** 跳转到文档召回测试页面 */
+const handleRetrieval = (id: number) => {
+  router.push({
+    name: 'AiKnowledgeRetrieval',
+    query: { id }
+  })
+}
+
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 163 - 0
src/views/ai/knowledge/knowledge/retrieval/index.vue

@@ -0,0 +1,163 @@
+<template>
+  <div class="flex gap-20px w-full">
+    <!-- 左侧输入区域 -->
+    <ContentWrap class="flex-1 min-w-300px">
+      <div class="mb-15px">
+        <h3 class="m-0 mb-5px">召回测试</h3>
+        <div class="text-gray-500 text-14px">根据给定的查询文本测试召回效果。</div>
+      </div>
+      <div>
+        <div class="relative mb-10px">
+          <el-input
+            v-model="queryParams.content"
+            type="textarea"
+            :rows="8"
+            placeholder="请输入文本"
+          />
+          <div class="absolute bottom-10px right-10px text-gray-400 text-12px">
+            {{ queryParams.content?.length }} / 200
+          </div>
+        </div>
+        <div class="flex items-center mb-10px">
+          <span class="w-60px text-gray-500">topK:</span>
+          <el-input-number v-model="queryParams.topK" :min="1" :max="20" />
+        </div>
+        <div class="flex items-center mb-15px">
+          <span class="w-60px text-gray-500">相似度:</span>
+          <el-input-number
+            v-model="queryParams.similarityThreshold"
+            :min="0"
+            :max="1"
+            :precision="2"
+            :step="0.01"
+          />
+        </div>
+        <div class="flex justify-end">
+          <el-button type="primary" @click="getRetrievalResult" :loading="loading">测试</el-button>
+        </div>
+      </div>
+    </ContentWrap>
+
+    <!-- 右侧召回结果区域 -->
+    <ContentWrap class="flex-1 min-w-300px">
+      <el-empty v-if="loading" description="正在检索中..." />
+      <div v-else-if="segments.length > 0" class="font-bold mb-15px">
+        {{ segments.length }} 个召回段落
+      </div>
+      <el-empty v-else description="暂无召回结果" />
+      <div>
+        <div
+          v-for="(segment, index) in segments"
+          :key="index"
+          class="mb-20px border border-solid border-gray-200 rounded p-15px"
+        >
+          <div class="flex justify-between text-12px text-gray-500 mb-5px">
+            <span>
+              分段({{ segment.id }}) · {{ segment.contentLength }} 字符数 ·
+              {{ segment.tokens }} Token
+            </span>
+            <span class="px-8px py-4px bg-blue-50 text-blue-500 rounded-full text-12px font-bold">
+              score: {{ segment.score }}
+            </span>
+          </div>
+          <div
+            class="bg-gray-50 p-10px rounded mb-10px whitespace-pre-wrap overflow-hidden transition-all duration-100 text-13px"
+            :class="{
+              'line-clamp-2 max-h-50px': !segment.expanded,
+              'max-h-500px': segment.expanded
+            }"
+          >
+            {{ segment.content }}
+          </div>
+          <div class="flex justify-between items-center">
+            <div class="flex items-center text-gray-500 text-13px">
+              <Icon icon="ep:document" class="mr-5px" />
+              <span>{{ segment.documentName || '未知文档' }}</span>
+            </div>
+            <el-button size="small" @click="toggleExpand(segment)">
+              {{ segment.expanded ? '收起' : '展开' }}
+              <Icon :icon="segment.expanded ? 'ep:arrow-up' : 'ep:arrow-down'" />
+            </el-button>
+          </div>
+        </div>
+      </div>
+    </ContentWrap>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useMessage } from '@/hooks/web/useMessage'
+import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
+import { KnowledgeApi } from '@/api/ai/knowledge/knowledge'
+/** 文档召回测试 */
+defineOptions({ name: 'KnowledgeDocumentRetrieval' })
+
+const message = useMessage() // 消息弹窗
+const route = useRoute() // 路由
+const router = useRouter() // 路由
+
+const loading = ref(false) // 加载状态
+const segments = ref<any[]>([]) // 召回结果
+const queryParams = reactive({
+  id: undefined,
+  content: '',
+  topK: 10,
+  similarityThreshold: 0.5
+})
+
+/** 调用文档召回测试接口 */
+const getRetrievalResult = async () => {
+  if (!queryParams.content) {
+    message.warning('请输入查询文本')
+    return
+  }
+
+  loading.value = true
+  segments.value = []
+
+  try {
+    const data = await KnowledgeSegmentApi.searchKnowledgeSegment({
+      knowledgeId: queryParams.id,
+      content: queryParams.content,
+      topK: queryParams.topK,
+      similarityThreshold: queryParams.similarityThreshold
+    })
+    segments.value = data || []
+  } catch (error) {
+    console.error(error)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 展开/收起段落内容 */
+const toggleExpand = (segment: any) => {
+  segment.expanded = !segment.expanded
+}
+
+/** 获取知识库信息 */
+const getKnowledgeInfo = async (id: number) => {
+  try {
+    const knowledge = await KnowledgeApi.getKnowledge(id)
+    if (knowledge) {
+      queryParams.topK = knowledge.topK || queryParams.topK
+      queryParams.similarityThreshold =
+        knowledge.similarityThreshold || queryParams.similarityThreshold
+    }
+  } catch (error) {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  // 如果知识库 ID 不存在,显示错误提示并关闭页面
+  if (!route.query.id) {
+    message.error('知识库 ID 不存在,无法进行召回测试')
+    router.back()
+    return
+  }
+  queryParams.id = route.query.id as any
+
+  // 获取知识库信息并设置默认值
+  getKnowledgeInfo(queryParams.id as any)
+})
+</script>