feat: 知识库设置页面增加 retrieval_model 检索配置功能
1. 召回测试页面增加 Score 阈值参数配置 2. 知识库设置页面新增检索模型配置: - 检索方式 (向量/全文/混合/关键字检索) - Reranking 模型 (默认开启,不可关闭) - Top K 返回数量 - Score 阈值 (默认开启,可调节数值) 3. 修复 Dify API 字段名问题 (retrieval_model_dict) 4. 优化数据加载流程,使用详情接口获取完整配置 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { Dataset, DatasetsResponse } from '../type';
|
import type { Dataset, DatasetsResponse, UpdateDatasetRequest } from '../type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 基础 URL
|
* API 基础 URL
|
||||||
@@ -76,3 +76,27 @@ export async function updateDatasetName(
|
|||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新知识库设置(包含检索模型配置)
|
||||||
|
*
|
||||||
|
* @param datasetId - 知识库 ID
|
||||||
|
* @param settings - 更新的设置项
|
||||||
|
* @returns 更新后的知识库详情
|
||||||
|
*/
|
||||||
|
export async function updateDatasetSettings(
|
||||||
|
datasetId: string,
|
||||||
|
settings: UpdateDatasetRequest
|
||||||
|
): Promise<Dataset> {
|
||||||
|
console.log('[Dataset Client] 更新知识库设置:', { datasetId, settings });
|
||||||
|
|
||||||
|
const response = await axios.patch<Dataset>(
|
||||||
|
`${API_URL}/datasets/${datasetId}`,
|
||||||
|
settings,
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export interface Dataset {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
permission: 'only_me' | 'all_team_members';
|
permission: 'only_me' | 'all_team_members';
|
||||||
data_source_type: 'upload_file' | 'notion_import' | 'website_crawl';
|
data_source_type: 'upload_file' | 'notion_import' | 'website_crawl' | null;
|
||||||
indexing_technique: 'high_quality' | 'economy';
|
indexing_technique: 'high_quality' | 'economy' | null;
|
||||||
app_count: number;
|
app_count: number;
|
||||||
document_count: number;
|
document_count: number;
|
||||||
word_count: number;
|
word_count: number;
|
||||||
@@ -21,6 +21,20 @@ export interface Dataset {
|
|||||||
created_at: number;
|
created_at: number;
|
||||||
updated_by: string;
|
updated_by: string;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
|
/** 嵌入模型提供商 */
|
||||||
|
embedding_model_provider?: string | null;
|
||||||
|
/** 嵌入模型名称 */
|
||||||
|
embedding_model?: string | null;
|
||||||
|
/** 嵌入模型是否可用 */
|
||||||
|
embedding_available?: boolean;
|
||||||
|
/** 检索模型配置(Dify API 返回字段名为 retrieval_model_dict) */
|
||||||
|
retrieval_model_dict?: RetrievalModel;
|
||||||
|
/** 标签 */
|
||||||
|
tags?: string[];
|
||||||
|
/** 文档形式 */
|
||||||
|
doc_form?: string | null;
|
||||||
|
/** 供应商 */
|
||||||
|
provider?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import { Form, Input, Button, Card, Spin } from 'antd';
|
import { Form, Input, Button, Card, Spin, Divider, Select, Slider, InputNumber, Tooltip, Checkbox } from 'antd';
|
||||||
import { SaveOutlined } from '@ant-design/icons';
|
import { SaveOutlined, QuestionCircleOutlined, CheckCircleFilled } from '@ant-design/icons';
|
||||||
import { useDatasetSettings } from '~/hooks/dify-dataset-manager/dataset-settings';
|
import { useDatasetSettings, type SearchMethod } from '~/hooks/dify-dataset-manager/dataset-settings';
|
||||||
import type { DatasetSettingsProps } from '~/types/dify-dataset-manager/dataset-settings';
|
import type { DatasetSettingsProps } from '~/types/dify-dataset-manager/dataset-settings';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
// 检索方式选项
|
||||||
|
const SEARCH_METHOD_OPTIONS: { label: string; value: SearchMethod; description: string }[] = [
|
||||||
|
{ label: '向量检索', value: 'semantic_search', description: '基于语义理解的智能检索,适合需要理解上下文的场景' },
|
||||||
|
{ label: '全文检索', value: 'full_text_search', description: '基于关键词匹配的传统检索方式' },
|
||||||
|
{ label: '混合检索', value: 'hybrid_search', description: '结合向量和全文检索,综合效果最佳' },
|
||||||
|
{ label: '关键字检索', value: 'keyword_search', description: '精确关键字匹配' },
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库设置组件
|
* 知识库设置组件
|
||||||
* 用于修改知识库名称和描述
|
* 用于修改知识库名称和描述
|
||||||
@@ -18,11 +26,19 @@ export default function DatasetSettings({
|
|||||||
const {
|
const {
|
||||||
saving,
|
saving,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
|
retrievalSettings,
|
||||||
handleValuesChange,
|
handleValuesChange,
|
||||||
handleSave,
|
handleSave,
|
||||||
handleReset,
|
handleReset,
|
||||||
|
updateRetrievalSettings,
|
||||||
} = useDatasetSettings(dataset, form, onDatasetUpdated);
|
} = useDatasetSettings(dataset, form, onDatasetUpdated);
|
||||||
|
|
||||||
|
// 是否需要显示 Reranking 提示(语义检索和混合检索需要,且强制开启)
|
||||||
|
const showRerankingInfo = retrievalSettings.searchMethod === 'semantic_search' || retrievalSettings.searchMethod === 'hybrid_search';
|
||||||
|
// 权重设置:由于 Reranking 强制开启,混合检索时由 Reranking 模型决定排序,不需要手动设置权重
|
||||||
|
// 所以这里始终不显示权重设置
|
||||||
|
const showWeightsOption = false;
|
||||||
|
|
||||||
if (!dataset) {
|
if (!dataset) {
|
||||||
return (
|
return (
|
||||||
<div className="settings-loading">
|
<div className="settings-loading">
|
||||||
@@ -81,12 +97,6 @@ export default function DatasetSettings({
|
|||||||
{dataset.indexing_technique === 'high_quality' ? '高质量' : '经济'}
|
{dataset.indexing_technique === 'high_quality' ? '高质量' : '经济'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="info-item">
|
|
||||||
<span className="info-label">Embedding 模型:</span>
|
|
||||||
<span className="info-value">
|
|
||||||
{dataset.embedding_model || '默认模型'}
|
|
||||||
</span>
|
|
||||||
</div> */}
|
|
||||||
<div className="info-item">
|
<div className="info-item">
|
||||||
<span className="info-label">文档数量:</span>
|
<span className="info-label">文档数量:</span>
|
||||||
<span className="info-value">{dataset.document_count}</span>
|
<span className="info-value">{dataset.document_count}</span>
|
||||||
@@ -102,24 +112,167 @@ export default function DatasetSettings({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
|
||||||
<div className="form-actions">
|
|
||||||
<Button onClick={handleReset} disabled={!hasChanges}>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<SaveOutlined />}
|
|
||||||
onClick={handleSave}
|
|
||||||
loading={saving}
|
|
||||||
disabled={!hasChanges}
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 检索设置卡片 */}
|
||||||
|
<Card className="settings-card" style={{ marginTop: 16 }}>
|
||||||
|
<h3 style={{ marginBottom: 16, fontSize: 16, fontWeight: 500 }}>
|
||||||
|
检索设置
|
||||||
|
<Tooltip title="配置知识库的默认检索参数,影响召回效果">
|
||||||
|
<QuestionCircleOutlined style={{ marginLeft: 8, color: '#8c8c8c', fontSize: 14 }} />
|
||||||
|
</Tooltip>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* 检索方式 */}
|
||||||
|
<div className="setting-item" style={{ marginBottom: 16 }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||||||
|
检索方式
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={retrievalSettings.searchMethod}
|
||||||
|
onChange={(value) => updateRetrievalSettings('searchMethod', value)}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
options={SEARCH_METHOD_OPTIONS.map(opt => ({
|
||||||
|
value: opt.value,
|
||||||
|
label: (
|
||||||
|
<div>
|
||||||
|
<span>{opt.label}</span>
|
||||||
|
<span style={{ color: '#8c8c8c', fontSize: 12, marginLeft: 8 }}>
|
||||||
|
{opt.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reranking 设置(语义检索和混合检索时显示,默认开启不可关闭) */}
|
||||||
|
{showRerankingInfo && (
|
||||||
|
<div className="setting-item" style={{ marginBottom: 16 }}>
|
||||||
|
<Checkbox
|
||||||
|
checked={true}
|
||||||
|
disabled={true}
|
||||||
|
>
|
||||||
|
<span style={{ color: '#262626' }}>
|
||||||
|
启用 Reranking 模型
|
||||||
|
<CheckCircleFilled style={{ marginLeft: 6, color: '#52c41a', fontSize: 12 }} />
|
||||||
|
</span>
|
||||||
|
<Tooltip title="Reranking 模型已默认开启,用于对检索结果进行重新排序,提高相关性">
|
||||||
|
<QuestionCircleOutlined style={{ marginLeft: 4, color: '#8c8c8c' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 混合检索权重设置 */}
|
||||||
|
{showWeightsOption && (
|
||||||
|
<div className="setting-item" style={{ marginBottom: 16 }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||||||
|
语义权重
|
||||||
|
<Tooltip title="混合检索中语义检索的权重,值越大语义检索占比越高">
|
||||||
|
<QuestionCircleOutlined style={{ marginLeft: 4, color: '#8c8c8c' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<span style={{ fontSize: 12, color: '#8c8c8c' }}>关键词</span>
|
||||||
|
<Slider
|
||||||
|
value={retrievalSettings.weights}
|
||||||
|
onChange={(value) => updateRetrievalSettings('weights', value)}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.1}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 12, color: '#8c8c8c' }}>语义</span>
|
||||||
|
<InputNumber
|
||||||
|
value={retrievalSettings.weights}
|
||||||
|
onChange={(value) => updateRetrievalSettings('weights', value ?? 0.7)}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.1}
|
||||||
|
size="small"
|
||||||
|
style={{ width: 70 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Top K 设置 */}
|
||||||
|
<div className="setting-item" style={{ marginBottom: 16 }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||||||
|
返回数量 (Top K)
|
||||||
|
<Tooltip title="每次检索返回的最大结果数量">
|
||||||
|
<QuestionCircleOutlined style={{ marginLeft: 4, color: '#8c8c8c' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Slider
|
||||||
|
value={retrievalSettings.topK}
|
||||||
|
onChange={(value) => updateRetrievalSettings('topK', value)}
|
||||||
|
min={1}
|
||||||
|
max={20}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
value={retrievalSettings.topK}
|
||||||
|
onChange={(value) => updateRetrievalSettings('topK', value ?? 3)}
|
||||||
|
min={1}
|
||||||
|
max={20}
|
||||||
|
size="small"
|
||||||
|
style={{ width: 70 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Score 阈值设置(默认开启不可关闭,但可调节数值) */}
|
||||||
|
<div className="setting-item" style={{ marginBottom: 16 }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||||||
|
Score 阈值
|
||||||
|
<CheckCircleFilled style={{ marginLeft: 6, color: '#52c41a', fontSize: 12 }} />
|
||||||
|
<Tooltip title="Score 阈值已默认开启,只返回相似度分数高于阈值的结果,过滤低质量匹配">
|
||||||
|
<QuestionCircleOutlined style={{ marginLeft: 4, color: '#8c8c8c' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Slider
|
||||||
|
value={retrievalSettings.scoreThreshold}
|
||||||
|
onChange={(value) => updateRetrievalSettings('scoreThreshold', value)}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
value={retrievalSettings.scoreThreshold}
|
||||||
|
onChange={(value) => updateRetrievalSettings('scoreThreshold', value ?? 0.5)}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
size="small"
|
||||||
|
style={{ width: 70 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="form-actions" style={{ marginTop: 24, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||||
|
<Button onClick={handleReset} disabled={!hasChanges}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
onClick={handleSave}
|
||||||
|
loading={saving}
|
||||||
|
disabled={!hasChanges}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SearchOutlined, FileSearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined, FileSearchOutlined } from '@ant-design/icons';
|
||||||
import { Button, Tag, Input, Slider, Spin, Select, Flex } from 'antd';
|
import { Button, Tag, Input, Slider, Spin, Select, Flex, Switch, InputNumber, Tooltip } from 'antd';
|
||||||
import type { RetrieveRecord } from '~/api/dify-dataset/type';
|
import type { RetrieveRecord } from '~/api/dify-dataset/type';
|
||||||
import { useRetrieveTest } from '~/hooks/dify-dataset-manager/retrieve-test';
|
import { useRetrieveTest } from '~/hooks/dify-dataset-manager/retrieve-test';
|
||||||
import type { RetrieveTestProps } from '~/types/dify-dataset-manager/retrieve-test';
|
import type { RetrieveTestProps } from '~/types/dify-dataset-manager/retrieve-test';
|
||||||
@@ -97,6 +97,10 @@ export default function RetrieveTest({ datasetId }: RetrieveTestProps) {
|
|||||||
setSearchMethod,
|
setSearchMethod,
|
||||||
topK,
|
topK,
|
||||||
setTopK,
|
setTopK,
|
||||||
|
scoreThresholdEnabled,
|
||||||
|
setScoreThresholdEnabled,
|
||||||
|
scoreThreshold,
|
||||||
|
setScoreThreshold,
|
||||||
handleRetrieve,
|
handleRetrieve,
|
||||||
} = useRetrieveTest(datasetId);
|
} = useRetrieveTest(datasetId);
|
||||||
|
|
||||||
@@ -229,6 +233,46 @@ export default function RetrieveTest({ datasetId }: RetrieveTestProps) {
|
|||||||
{topK}
|
{topK}
|
||||||
</span>
|
</span>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
{/* Score 阈值设置 */}
|
||||||
|
<Flex align="center" gap={12}>
|
||||||
|
<Tooltip title="开启后,只返回相似度分数高于阈值的结果">
|
||||||
|
<span style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
cursor: 'help',
|
||||||
|
}}>
|
||||||
|
Score 阈值:
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={scoreThresholdEnabled}
|
||||||
|
onChange={setScoreThresholdEnabled}
|
||||||
|
/>
|
||||||
|
{scoreThresholdEnabled && (
|
||||||
|
<>
|
||||||
|
<Slider
|
||||||
|
value={scoreThreshold}
|
||||||
|
onChange={setScoreThreshold}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
value={scoreThreshold}
|
||||||
|
onChange={(value) => setScoreThreshold(value ?? 0.5)}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
size="small"
|
||||||
|
style={{ width: 70 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,77 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import type { FormInstance } from 'antd';
|
import type { FormInstance } from 'antd';
|
||||||
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
|
import type { Dataset, RetrievalModel } from '~/api/dify-dataset/type/datasetTypes';
|
||||||
import { updateDatasetName } from '~/api/dify-dataset/api/datasetApi';
|
import { updateDatasetSettings, fetchDataset } from '~/api/dify-dataset/api/datasetApi';
|
||||||
|
import { DIFY_CONFIG } from '~/config/api-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检索方法类型
|
||||||
|
*/
|
||||||
|
export type SearchMethod = 'keyword_search' | 'semantic_search' | 'full_text_search' | 'hybrid_search';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检索设置表单值
|
||||||
|
*/
|
||||||
|
export interface RetrievalSettingsFormValues {
|
||||||
|
searchMethod: SearchMethod;
|
||||||
|
topK: number;
|
||||||
|
scoreThresholdEnabled: boolean;
|
||||||
|
scoreThreshold: number;
|
||||||
|
rerankingEnable: boolean;
|
||||||
|
weights: number; // 混合检索的语义权重 (0-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认检索设置
|
||||||
|
*/
|
||||||
|
const DEFAULT_RETRIEVAL_SETTINGS: RetrievalSettingsFormValues = {
|
||||||
|
searchMethod: 'semantic_search',
|
||||||
|
topK: 3,
|
||||||
|
scoreThresholdEnabled: true, // 默认开启
|
||||||
|
scoreThreshold: 0.5,
|
||||||
|
rerankingEnable: true, // 默认开启
|
||||||
|
weights: 0.7,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Dataset 的 retrieval_model 转换为表单值
|
||||||
|
*/
|
||||||
|
function retrievalModelToFormValues(model?: RetrievalModel): RetrievalSettingsFormValues {
|
||||||
|
if (!model) {
|
||||||
|
return { ...DEFAULT_RETRIEVAL_SETTINGS };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
searchMethod: model.search_method || 'semantic_search',
|
||||||
|
topK: model.top_k ?? 3,
|
||||||
|
scoreThresholdEnabled: model.score_threshold_enabled ?? false,
|
||||||
|
scoreThreshold: model.score_threshold ?? 0.5,
|
||||||
|
rerankingEnable: model.reranking_enable ?? false,
|
||||||
|
weights: model.weights ?? 0.7,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从表单值转换为 API 请求的 retrieval_model
|
||||||
|
*/
|
||||||
|
function formValuesToRetrievalModel(values: RetrievalSettingsFormValues): RetrievalModel {
|
||||||
|
// 语义检索和混合检索需要 Reranking,强制开启
|
||||||
|
const needReranking = values.searchMethod === 'semantic_search' || values.searchMethod === 'hybrid_search';
|
||||||
|
|
||||||
|
return {
|
||||||
|
search_method: values.searchMethod,
|
||||||
|
reranking_enable: needReranking, // 强制开启,不受用户控制
|
||||||
|
reranking_mode: null,
|
||||||
|
reranking_model: {
|
||||||
|
reranking_provider_name: DIFY_CONFIG.rerankingProviderName,
|
||||||
|
reranking_model_name: DIFY_CONFIG.rerankingModelName,
|
||||||
|
},
|
||||||
|
weights: values.searchMethod === 'hybrid_search' ? values.weights : null,
|
||||||
|
top_k: values.topK,
|
||||||
|
score_threshold_enabled: true, // 强制开启,不受用户控制
|
||||||
|
score_threshold: values.scoreThreshold, // 用户可调节数值
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库设置状态管理 Hook
|
* 知识库设置状态管理 Hook
|
||||||
@@ -15,6 +84,11 @@ export function useDatasetSettings(
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
|
||||||
|
// 检索设置状态(注意:Dify API 返回的字段名是 retrieval_model_dict)
|
||||||
|
const [retrievalSettings, setRetrievalSettings] = useState<RetrievalSettingsFormValues>(
|
||||||
|
() => retrievalModelToFormValues(dataset?.retrieval_model_dict)
|
||||||
|
);
|
||||||
|
|
||||||
// 初始化表单数据
|
// 初始化表单数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dataset) {
|
if (dataset) {
|
||||||
@@ -22,20 +96,53 @@ export function useDatasetSettings(
|
|||||||
name: dataset.name,
|
name: dataset.name,
|
||||||
description: dataset.description || '',
|
description: dataset.description || '',
|
||||||
});
|
});
|
||||||
|
console.log('[DatasetSettings] 初始化检索设置, retrieval_model_dict:', dataset.retrieval_model_dict);
|
||||||
|
setRetrievalSettings(retrievalModelToFormValues(dataset.retrieval_model_dict));
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
}
|
}
|
||||||
}, [dataset, form]);
|
}, [dataset, form]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新检索设置
|
||||||
|
*/
|
||||||
|
const updateRetrievalSettings = useCallback(<K extends keyof RetrievalSettingsFormValues>(
|
||||||
|
key: K,
|
||||||
|
value: RetrievalSettingsFormValues[K]
|
||||||
|
) => {
|
||||||
|
setRetrievalSettings(prev => {
|
||||||
|
const newSettings = { ...prev, [key]: value };
|
||||||
|
// 检查是否有变化
|
||||||
|
checkForChanges(newSettings);
|
||||||
|
return newSettings;
|
||||||
|
});
|
||||||
|
}, [dataset]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有变化
|
||||||
|
*/
|
||||||
|
const checkForChanges = useCallback((newRetrievalSettings?: RetrievalSettingsFormValues) => {
|
||||||
|
const values = form.getFieldsValue();
|
||||||
|
const currentRetrieval = newRetrievalSettings || retrievalSettings;
|
||||||
|
const originalRetrieval = retrievalModelToFormValues(dataset?.retrieval_model_dict);
|
||||||
|
|
||||||
|
const nameChanged = values.name !== dataset?.name;
|
||||||
|
const retrievalChanged =
|
||||||
|
currentRetrieval.searchMethod !== originalRetrieval.searchMethod ||
|
||||||
|
currentRetrieval.topK !== originalRetrieval.topK ||
|
||||||
|
currentRetrieval.scoreThresholdEnabled !== originalRetrieval.scoreThresholdEnabled ||
|
||||||
|
currentRetrieval.scoreThreshold !== originalRetrieval.scoreThreshold ||
|
||||||
|
currentRetrieval.rerankingEnable !== originalRetrieval.rerankingEnable ||
|
||||||
|
currentRetrieval.weights !== originalRetrieval.weights;
|
||||||
|
|
||||||
|
setHasChanges(nameChanged || retrievalChanged);
|
||||||
|
}, [form, dataset, retrievalSettings]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理表单值变化
|
* 处理表单值变化
|
||||||
*/
|
*/
|
||||||
const handleValuesChange = useCallback(() => {
|
const handleValuesChange = useCallback(() => {
|
||||||
const values = form.getFieldsValue();
|
checkForChanges();
|
||||||
const changed =
|
}, [checkForChanges]);
|
||||||
values.name !== dataset?.name ||
|
|
||||||
values.description !== (dataset?.description || '');
|
|
||||||
setHasChanges(changed);
|
|
||||||
}, [form, dataset]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存设置
|
* 保存设置
|
||||||
@@ -50,11 +157,18 @@ export function useDatasetSettings(
|
|||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|
||||||
// 目前只支持修改名称
|
// 构建完整的更新请求
|
||||||
const updatedDataset = await updateDatasetName(dataset.id, values.name);
|
await updateDatasetSettings(dataset.id, {
|
||||||
|
name: values.name,
|
||||||
|
retrieval_model: formValuesToRetrievalModel(retrievalSettings),
|
||||||
|
});
|
||||||
|
|
||||||
|
// PATCH 接口返回的数据可能不完整,重新获取详情
|
||||||
|
const fullDataset = await fetchDataset(dataset.id);
|
||||||
|
console.log('[DatasetSettings] 保存后获取完整数据:', fullDataset);
|
||||||
|
|
||||||
message.success('保存成功');
|
message.success('保存成功');
|
||||||
onDatasetUpdated(updatedDataset);
|
onDatasetUpdated(fullDataset);
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('保存设置失败:', err);
|
console.error('保存设置失败:', err);
|
||||||
@@ -62,7 +176,7 @@ export function useDatasetSettings(
|
|||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}, [dataset, form, onDatasetUpdated]);
|
}, [dataset, form, retrievalSettings, onDatasetUpdated]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置表单
|
* 重置表单
|
||||||
@@ -73,6 +187,7 @@ export function useDatasetSettings(
|
|||||||
name: dataset.name,
|
name: dataset.name,
|
||||||
description: dataset.description || '',
|
description: dataset.description || '',
|
||||||
});
|
});
|
||||||
|
setRetrievalSettings(retrievalModelToFormValues(dataset.retrieval_model_dict));
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
}
|
}
|
||||||
}, [dataset, form]);
|
}, [dataset, form]);
|
||||||
@@ -81,11 +196,13 @@ export function useDatasetSettings(
|
|||||||
// 状态
|
// 状态
|
||||||
saving,
|
saving,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
|
retrievalSettings,
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
handleValuesChange,
|
handleValuesChange,
|
||||||
handleSave,
|
handleSave,
|
||||||
handleReset,
|
handleReset,
|
||||||
|
updateRetrievalSettings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
|
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
|
||||||
import type { Document } from '~/api/dify-dataset/type/documentTypes';
|
import type { Document } from '~/api/dify-dataset/type/documentTypes';
|
||||||
import { fetchDatasets } from '~/api/dify-dataset/api/datasetApi';
|
import { fetchDatasets, fetchDataset } from '~/api/dify-dataset/api/datasetApi';
|
||||||
import { fetchDocuments } from '~/api/dify-dataset/api/documentApi';
|
import { fetchDocuments } from '~/api/dify-dataset/api/documentApi';
|
||||||
import type { MenuTab } from '~/types/dify-dataset-manager/layout';
|
import type { MenuTab } from '~/types/dify-dataset-manager/layout';
|
||||||
import { DEFAULT_DOCUMENT_PAGE_SIZE } from '~/types/dify-dataset-manager/index';
|
import { DEFAULT_DOCUMENT_PAGE_SIZE } from '~/types/dify-dataset-manager/index';
|
||||||
@@ -58,20 +58,26 @@ export function useDatasetManager() {
|
|||||||
}, [documentPageSize]);
|
}, [documentPageSize]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载知识库(获取第一个知识库)
|
* 加载知识库(获取第一个知识库,再获取详情以包含 retrieval_model)
|
||||||
*/
|
*/
|
||||||
const loadDataset = useCallback(async () => {
|
const loadDataset = useCallback(async () => {
|
||||||
setLoadingDataset(true);
|
setLoadingDataset(true);
|
||||||
try {
|
try {
|
||||||
console.log('[DatasetManager] 加载知识库...');
|
console.log('[DatasetManager] 加载知识库...');
|
||||||
|
// 先获取列表,找到第一个知识库的 ID
|
||||||
const response = await fetchDatasets(1, 1);
|
const response = await fetchDatasets(1, 1);
|
||||||
console.log('[DatasetManager] 知识库响应:', response);
|
console.log('[DatasetManager] 知识库列表响应:', response);
|
||||||
|
|
||||||
if (response && response.data && response.data.length > 0) {
|
if (response && response.data && response.data.length > 0) {
|
||||||
const firstDataset = response.data[0];
|
const firstDatasetId = response.data[0].id;
|
||||||
setDataset(firstDataset);
|
|
||||||
|
// 再获取详情,包含完整的 retrieval_model 等字段
|
||||||
|
const fullDataset = await fetchDataset(firstDatasetId);
|
||||||
|
console.log('[DatasetManager] 知识库详情响应:', fullDataset);
|
||||||
|
|
||||||
|
setDataset(fullDataset);
|
||||||
// 立即加载文档
|
// 立即加载文档
|
||||||
await loadDocuments(firstDataset.id, 1);
|
await loadDocuments(firstDatasetId, 1);
|
||||||
} else {
|
} else {
|
||||||
setError('未找到知识库,请先在Dify中创建知识库');
|
setError('未找到知识库,请先在Dify中创建知识库');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import type { SearchMethod } from '~/types/dify-dataset-manager/retrieve-test';
|
|||||||
* 构建完整的 retrieval_model 参数(匹配 Dify API 规范)
|
* 构建完整的 retrieval_model 参数(匹配 Dify API 规范)
|
||||||
* 根据检索方式启用 Reranking(语义搜索和混合搜索需要启用)
|
* 根据检索方式启用 Reranking(语义搜索和混合搜索需要启用)
|
||||||
*/
|
*/
|
||||||
function buildRetrievalModel(searchMethod: SearchMethod, topK: number): RetrievalModel {
|
function buildRetrievalModel(
|
||||||
|
searchMethod: SearchMethod,
|
||||||
|
topK: number,
|
||||||
|
scoreThresholdEnabled: boolean,
|
||||||
|
scoreThreshold: number
|
||||||
|
): RetrievalModel {
|
||||||
// 语义搜索和混合搜索需要启用 Reranking
|
// 语义搜索和混合搜索需要启用 Reranking
|
||||||
const needReranking = searchMethod === 'semantic_search' || searchMethod === 'hybrid_search';
|
const needReranking = searchMethod === 'semantic_search' || searchMethod === 'hybrid_search';
|
||||||
|
|
||||||
@@ -23,8 +28,8 @@ function buildRetrievalModel(searchMethod: SearchMethod, topK: number): Retrieva
|
|||||||
},
|
},
|
||||||
weights: null,
|
weights: null,
|
||||||
top_k: topK,
|
top_k: topK,
|
||||||
score_threshold_enabled: false,
|
score_threshold_enabled: scoreThresholdEnabled,
|
||||||
score_threshold: null,
|
score_threshold: scoreThresholdEnabled ? scoreThreshold : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +43,9 @@ export function useRetrieveTest(datasetId: string) {
|
|||||||
// 默认使用语义搜索
|
// 默认使用语义搜索
|
||||||
const [searchMethod, setSearchMethod] = useState<SearchMethod>('semantic_search');
|
const [searchMethod, setSearchMethod] = useState<SearchMethod>('semantic_search');
|
||||||
const [topK, setTopK] = useState<number>(5);
|
const [topK, setTopK] = useState<number>(5);
|
||||||
|
// Score 阈值相关状态
|
||||||
|
const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(false);
|
||||||
|
const [scoreThreshold, setScoreThreshold] = useState<number>(0.5);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行检索
|
* 执行检索
|
||||||
@@ -55,7 +63,7 @@ export function useRetrieveTest(datasetId: string) {
|
|||||||
|
|
||||||
setRetrieving(true);
|
setRetrieving(true);
|
||||||
try {
|
try {
|
||||||
const retrievalModel = buildRetrievalModel(searchMethod, topK);
|
const retrievalModel = buildRetrievalModel(searchMethod, topK, scoreThresholdEnabled, scoreThreshold);
|
||||||
console.log('[Hook] 检索参数:', { datasetId, query: searchQuery, retrievalModel });
|
console.log('[Hook] 检索参数:', { datasetId, query: searchQuery, retrievalModel });
|
||||||
|
|
||||||
const response = await retrieveDataset(datasetId, searchQuery, retrievalModel);
|
const response = await retrieveDataset(datasetId, searchQuery, retrievalModel);
|
||||||
@@ -69,7 +77,7 @@ export function useRetrieveTest(datasetId: string) {
|
|||||||
} finally {
|
} finally {
|
||||||
setRetrieving(false);
|
setRetrieving(false);
|
||||||
}
|
}
|
||||||
}, [datasetId, searchQuery, searchMethod, topK]);
|
}, [datasetId, searchQuery, searchMethod, topK, scoreThresholdEnabled, scoreThreshold]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
@@ -81,6 +89,10 @@ export function useRetrieveTest(datasetId: string) {
|
|||||||
setSearchMethod,
|
setSearchMethod,
|
||||||
topK,
|
topK,
|
||||||
setTopK,
|
setTopK,
|
||||||
|
scoreThresholdEnabled,
|
||||||
|
setScoreThresholdEnabled,
|
||||||
|
scoreThreshold,
|
||||||
|
setScoreThreshold,
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
handleRetrieve,
|
handleRetrieve,
|
||||||
|
|||||||
@@ -54,15 +54,22 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PATCH /api/dataset/datasets/:datasetId - 修改知识库名称
|
* PATCH /api/dataset/datasets/:datasetId - 修改知识库设置
|
||||||
*
|
*
|
||||||
* Dify API: PATCH /datasets/{dataset_id}
|
* Dify API: PATCH /datasets/{dataset_id}
|
||||||
*
|
*
|
||||||
* 请求体: { "name": "新的知识库名称" }
|
* 请求体支持以下字段:
|
||||||
|
* - name (string): 知识库名称(必填)
|
||||||
|
* - retrieval_model (object): 检索模型配置(选填)
|
||||||
|
* - search_method: 'keyword_search' | 'semantic_search' | 'full_text_search' | 'hybrid_search'
|
||||||
|
* - reranking_enable: boolean
|
||||||
|
* - reranking_model: { reranking_provider_name, reranking_model_name }
|
||||||
|
* - weights: number | null (混合检索的语义权重)
|
||||||
|
* - top_k: number
|
||||||
|
* - score_threshold_enabled: boolean
|
||||||
|
* - score_threshold: number | null
|
||||||
*
|
*
|
||||||
* 注意:
|
* 注意:删除知识库功能不对外开放
|
||||||
* - 仅允许修改知识库名称,其他字段不开放修改
|
|
||||||
* - 删除知识库功能不对外开放
|
|
||||||
*/
|
*/
|
||||||
export async function action({ request, params }: ActionFunctionArgs) {
|
export async function action({ request, params }: ActionFunctionArgs) {
|
||||||
try {
|
try {
|
||||||
@@ -89,7 +96,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
|||||||
if (method === 'PATCH') {
|
if (method === 'PATCH') {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
||||||
// 只允许修改 name 字段
|
// name 是必填字段
|
||||||
if (!body.name || typeof body.name !== 'string') {
|
if (!body.name || typeof body.name !== 'string') {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: '请提供有效的知识库名称 (name)' }),
|
JSON.stringify({ error: '请提供有效的知识库名称 (name)' }),
|
||||||
@@ -97,17 +104,48 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只传递 name 字段,忽略其他字段
|
const trimmedName = body.name.trim();
|
||||||
const allowedBody = { name: body.name.trim() };
|
if (trimmedName.length === 0) {
|
||||||
|
|
||||||
if (allowedBody.name.length === 0) {
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: '知识库名称不能为空' }),
|
JSON.stringify({ error: '知识库名称不能为空' }),
|
||||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[API] Update Dataset Name:', { datasetId, name: allowedBody.name });
|
// 构建允许的请求体
|
||||||
|
const allowedBody: Record<string, any> = {
|
||||||
|
name: trimmedName,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 可选: retrieval_model 检索模型配置
|
||||||
|
if (body.retrieval_model && typeof body.retrieval_model === 'object') {
|
||||||
|
const rm = body.retrieval_model;
|
||||||
|
|
||||||
|
// 验证 search_method
|
||||||
|
const validSearchMethods = ['keyword_search', 'semantic_search', 'full_text_search', 'hybrid_search'];
|
||||||
|
if (rm.search_method && !validSearchMethods.includes(rm.search_method)) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: '无效的检索方法 (search_method)' }),
|
||||||
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedBody.retrieval_model = {
|
||||||
|
search_method: rm.search_method,
|
||||||
|
reranking_enable: rm.reranking_enable ?? false,
|
||||||
|
reranking_mode: rm.reranking_mode ?? null,
|
||||||
|
reranking_model: rm.reranking_model ?? {
|
||||||
|
reranking_provider_name: '',
|
||||||
|
reranking_model_name: '',
|
||||||
|
},
|
||||||
|
weights: rm.weights ?? null,
|
||||||
|
top_k: rm.top_k ?? 3,
|
||||||
|
score_threshold_enabled: rm.score_threshold_enabled ?? false,
|
||||||
|
score_threshold: rm.score_threshold_enabled ? (rm.score_threshold ?? null) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[API] Update Dataset Settings:', { datasetId, body: allowedBody });
|
||||||
|
|
||||||
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}`;
|
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}`;
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export type SearchMethod = 'semantic_search' | 'full_text_search' | 'hybrid_sear
|
|||||||
export interface RetrieveOptions {
|
export interface RetrieveOptions {
|
||||||
searchMethod: SearchMethod;
|
searchMethod: SearchMethod;
|
||||||
topK: number;
|
topK: number;
|
||||||
|
scoreThresholdEnabled: boolean;
|
||||||
|
scoreThreshold: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,4 +34,6 @@ export interface RetrieveTestState {
|
|||||||
retrieving: boolean;
|
retrieving: boolean;
|
||||||
searchMethod: SearchMethod;
|
searchMethod: SearchMethod;
|
||||||
topK: number;
|
topK: number;
|
||||||
|
scoreThresholdEnabled: boolean;
|
||||||
|
scoreThreshold: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user