feat: 完善Dify知识库管理召回测试模块,优化知识库上传文件时的分段配置设置
This commit is contained in:
@@ -1,202 +1,306 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
Card,
|
||||
Select,
|
||||
Slider,
|
||||
Table,
|
||||
Tag,
|
||||
Empty,
|
||||
Spin,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { FileSearchOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { SearchOutlined, FileSearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Tag, Input, Slider, Spin, Select, Flex } from 'antd';
|
||||
import type { RetrieveRecord } from '~/api/dify-dataset/type';
|
||||
import { retrieveDataset } from '~/api/dify-dataset/api/segmentApi';
|
||||
import { useRetrieveTest } from '~/hooks/dify-dataset-manager/retrieve-test';
|
||||
import type { RetrieveTestProps } from '~/types/dify-dataset-manager/retrieve-test';
|
||||
|
||||
interface RetrieveTestProps {
|
||||
datasetId: string;
|
||||
// 颜色常量
|
||||
const colors = {
|
||||
bgContainer: '#fff',
|
||||
bgLayout: '#f5f5f5',
|
||||
bgElevated: '#fafafa',
|
||||
border: '#e8e8e8',
|
||||
text: '#262626',
|
||||
textSecondary: '#8c8c8c',
|
||||
textTertiary: '#bfbfbf',
|
||||
textQuaternary: '#d9d9d9',
|
||||
fillTertiary: '#f0f0f0',
|
||||
};
|
||||
|
||||
/**
|
||||
* 检索结果项组件
|
||||
*/
|
||||
function ResultItem({ record, index }: { record: RetrieveRecord; index: number }) {
|
||||
const scorePercent = (record.score * 100).toFixed(1);
|
||||
const scoreColor = record.score > 0.8 ? '#52c41a' : record.score > 0.5 ? '#faad14' : '#666';
|
||||
|
||||
return (
|
||||
<Flex
|
||||
vertical
|
||||
gap={12}
|
||||
style={{
|
||||
padding: 16,
|
||||
background: colors.bgContainer,
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${colors.border}`,
|
||||
}}
|
||||
>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Flex gap={8} align="center">
|
||||
<Tag style={{ background: scoreColor, color: '#fff', border: 'none' }}>
|
||||
{scorePercent}%
|
||||
</Tag>
|
||||
<span style={{ color: colors.textSecondary, fontSize: 12 }}>
|
||||
#{index + 1} · {record.segment.word_count} 字 · 命中 {record.segment.hit_count} 次
|
||||
</span>
|
||||
</Flex>
|
||||
{record.segment.document && (
|
||||
<span style={{ color: colors.textTertiary, fontSize: 12 }}>
|
||||
来源: {record.segment.document.name}
|
||||
</span>
|
||||
)}
|
||||
</Flex>
|
||||
<div style={{
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.6,
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}>
|
||||
{record.segment.content.length > 500
|
||||
? record.segment.content.substring(0, 500) + '...'
|
||||
: record.segment.content}
|
||||
</div>
|
||||
{record.segment.answer && (
|
||||
<Flex
|
||||
vertical
|
||||
gap={4}
|
||||
style={{
|
||||
padding: 12,
|
||||
background: colors.fillTertiary,
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: colors.textSecondary, fontSize: 12 }}>
|
||||
答案:
|
||||
</span>
|
||||
<span style={{ color: colors.text, fontSize: 14 }}>
|
||||
{record.segment.answer.length > 200
|
||||
? record.segment.answer.substring(0, 200) + '...'
|
||||
: record.segment.answer}
|
||||
</span>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 召回测试组件
|
||||
* 用于测试知识库的检索效果
|
||||
*/
|
||||
export default function RetrieveTest({ datasetId }: RetrieveTestProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [retrieveResults, setRetrieveResults] = useState<RetrieveRecord[]>([]);
|
||||
const [retrieving, setRetrieving] = useState(false);
|
||||
const [searchMethod, setSearchMethod] = useState<string>('hybrid_search');
|
||||
const [topK, setTopK] = useState<number>(5);
|
||||
const {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
retrieveResults,
|
||||
retrieving,
|
||||
searchMethod,
|
||||
setSearchMethod,
|
||||
topK,
|
||||
setTopK,
|
||||
handleRetrieve,
|
||||
} = useRetrieveTest(datasetId);
|
||||
|
||||
/**
|
||||
* 执行检索
|
||||
*/
|
||||
const handleRetrieve = async () => {
|
||||
if (!searchQuery.trim()) {
|
||||
message.warning('请输入检索关键词');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!datasetId) {
|
||||
message.warning('知识库ID不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
setRetrieving(true);
|
||||
try {
|
||||
const response = await retrieveDataset(datasetId, searchQuery, {
|
||||
search_method: searchMethod as any,
|
||||
top_k: topK,
|
||||
});
|
||||
setRetrieveResults(response.records || []);
|
||||
if (response.records?.length === 0) {
|
||||
message.info('未找到匹配的结果');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('检索失败:', err);
|
||||
message.error(err.message || '检索失败');
|
||||
} finally {
|
||||
setRetrieving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 检索结果列定义
|
||||
const columns: ColumnsType<RetrieveRecord> = [
|
||||
{
|
||||
title: '相关度',
|
||||
dataIndex: 'score',
|
||||
key: 'score',
|
||||
width: 100,
|
||||
render: (score: number) => (
|
||||
<Tag color={score > 0.8 ? 'green' : score > 0.5 ? 'orange' : 'default'}>
|
||||
{(score * 100).toFixed(1)}%
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '内容',
|
||||
key: 'content',
|
||||
render: (_, record) => (
|
||||
<div className="retrieve-result-content">
|
||||
<div className="content-text">
|
||||
{record.segment.content.length > 300
|
||||
? record.segment.content.substring(0, 300) + '...'
|
||||
: record.segment.content}
|
||||
</div>
|
||||
{record.segment.answer && (
|
||||
<div className="answer-text">
|
||||
<strong>答案:</strong>
|
||||
{record.segment.answer.length > 150
|
||||
? record.segment.answer.substring(0, 150) + '...'
|
||||
: record.segment.answer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '字数',
|
||||
key: 'word_count',
|
||||
width: 80,
|
||||
render: (_, record) => record.segment.word_count,
|
||||
},
|
||||
{
|
||||
title: '命中次数',
|
||||
key: 'hit_count',
|
||||
width: 100,
|
||||
render: (_, record) => record.segment.hit_count,
|
||||
},
|
||||
// 检索方式选项(只有3种)
|
||||
const searchMethodOptions = [
|
||||
{ label: '向量检索', value: 'semantic_search' },
|
||||
{ label: '全文检索', value: 'full_text_search' },
|
||||
{ label: '混合检索', value: 'hybrid_search' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="retrieve-test-page">
|
||||
{/* 页面标题 */}
|
||||
<div className="page-header">
|
||||
<h1>召回测试</h1>
|
||||
<p className="page-description">
|
||||
输入查询内容,测试知识库的检索效果
|
||||
</p>
|
||||
</div>
|
||||
<Flex
|
||||
style={{
|
||||
height: '100%',
|
||||
minHeight: 'calc(100vh - 120px)',
|
||||
}}
|
||||
>
|
||||
{/* 左侧面板 - 输入区域 */}
|
||||
<Flex
|
||||
vertical
|
||||
gap={16}
|
||||
style={{
|
||||
width: 400,
|
||||
minWidth: 400,
|
||||
padding: 20,
|
||||
background: colors.bgLayout,
|
||||
borderRight: `1px solid ${colors.border}`,
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Flex vertical gap={4}>
|
||||
<h2 style={{
|
||||
margin: 0,
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
color: colors.text,
|
||||
}}>
|
||||
召回测试
|
||||
</h2>
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
color: colors.textSecondary,
|
||||
}}>
|
||||
根据给定的查询文本测试知识库召回效果
|
||||
</span>
|
||||
</Flex>
|
||||
|
||||
{/* 检索设置 */}
|
||||
<Card className="retrieve-settings" size="small">
|
||||
<div className="search-row">
|
||||
<Input
|
||||
placeholder="输入检索关键词..."
|
||||
prefix={<FileSearchOutlined />}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onPressEnter={handleRetrieve}
|
||||
className="search-input"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleRetrieve}
|
||||
loading={retrieving}
|
||||
>
|
||||
检索
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="options-row">
|
||||
<div className="option-item">
|
||||
<span className="option-label">检索方式:</span>
|
||||
{/* 查询输入区 */}
|
||||
<Flex vertical gap={8}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
color: colors.text,
|
||||
fontWeight: 500,
|
||||
}}>
|
||||
源文本
|
||||
</span>
|
||||
<Select
|
||||
value={searchMethod}
|
||||
onChange={setSearchMethod}
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: 'keyword_search', label: '关键词搜索' },
|
||||
{ value: 'semantic_search', label: '语义搜索' },
|
||||
{ value: 'full_text_search', label: '全文搜索' },
|
||||
{ value: 'hybrid_search', label: '混合搜索' },
|
||||
]}
|
||||
onChange={(value) => setSearchMethod(value as any)}
|
||||
options={searchMethodOptions}
|
||||
style={{ width: 130 }}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div className="option-item">
|
||||
<span className="option-label">返回数量 (Top K):</span>
|
||||
</Flex>
|
||||
<Input.TextArea
|
||||
placeholder="请输入文本,建议使用简短的陈述句。"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onPressEnter={(e) => {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleRetrieve();
|
||||
}
|
||||
}}
|
||||
autoSize={{ minRows: 6, maxRows: 12 }}
|
||||
style={{
|
||||
background: colors.bgContainer,
|
||||
resize: 'none',
|
||||
}}
|
||||
/>
|
||||
<Flex justify="space-between" align="center">
|
||||
<span style={{
|
||||
fontSize: 12,
|
||||
color: colors.textTertiary,
|
||||
}}>
|
||||
{searchQuery.length} / 200
|
||||
</span>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
onClick={handleRetrieve}
|
||||
loading={retrieving}
|
||||
>
|
||||
测试
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* 检索设置 */}
|
||||
<Flex vertical gap={12}>
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
color: colors.text,
|
||||
fontWeight: 500,
|
||||
}}>
|
||||
检索设置
|
||||
</span>
|
||||
<Flex align="center" gap={12}>
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
color: colors.textSecondary,
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
返回数量 (Top K):
|
||||
</span>
|
||||
<Slider
|
||||
value={topK}
|
||||
onChange={setTopK}
|
||||
min={1}
|
||||
max={20}
|
||||
style={{ width: 120 }}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<span className="option-value">{topK}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
color: colors.text,
|
||||
minWidth: 24,
|
||||
textAlign: 'right',
|
||||
}}>
|
||||
{topK}
|
||||
</span>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* 检索结果 */}
|
||||
<div className="retrieve-results">
|
||||
{/* 右侧面板 - 结果展示 */}
|
||||
<Flex
|
||||
vertical
|
||||
flex={1}
|
||||
gap={16}
|
||||
style={{
|
||||
padding: 20,
|
||||
background: colors.bgElevated,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{retrieving ? (
|
||||
<div className="loading-state">
|
||||
<Flex
|
||||
flex={1}
|
||||
align="center"
|
||||
justify="center"
|
||||
vertical
|
||||
gap={12}
|
||||
>
|
||||
<Spin size="large" />
|
||||
<div className="loading-text">检索中...</div>
|
||||
</div>
|
||||
<span style={{ color: colors.textSecondary }}>
|
||||
检索中...
|
||||
</span>
|
||||
</Flex>
|
||||
) : retrieveResults.length === 0 ? (
|
||||
<Empty
|
||||
description="请输入关键词进行检索"
|
||||
className="empty-state"
|
||||
/>
|
||||
<Flex
|
||||
flex={1}
|
||||
align="center"
|
||||
justify="center"
|
||||
vertical
|
||||
gap={12}
|
||||
>
|
||||
<FileSearchOutlined style={{
|
||||
fontSize: 48,
|
||||
color: colors.textQuaternary,
|
||||
}} />
|
||||
<span style={{ color: colors.textTertiary }}>
|
||||
召回测试结果将展示在这里
|
||||
</span>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
<div className="results-header">
|
||||
<span>找到 {retrieveResults.length} 条结果</span>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={retrieveResults}
|
||||
rowKey={(record) => record.segment.id}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
<Flex justify="space-between" align="center">
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
color: colors.text,
|
||||
fontWeight: 500,
|
||||
}}>
|
||||
检索结果
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
color: colors.textSecondary,
|
||||
}}>
|
||||
共找到 {retrieveResults.length} 条结果
|
||||
</span>
|
||||
</Flex>
|
||||
<Flex vertical gap={12}>
|
||||
{retrieveResults.map((record, index) => (
|
||||
<ResultItem
|
||||
key={record.segment.id}
|
||||
record={record}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user