From 5b1fd51b2741f04ac1a99d5e23cf45d6e6b484a0 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Tue, 26 May 2026 17:23:53 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=20spaCy=20?= =?UTF-8?q?=E5=9B=BE=E8=B0=B1=E6=8A=BD=E5=8F=96=E5=99=A8=EF=BC=8C=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E6=8A=BD=E5=8F=96=E6=96=B9=E6=A1=88=E6=94=B6=E6=95=9B?= =?UTF-8?q?=E4=B8=BA=E4=BB=85=20LLM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 backend/package/yuxi/knowledge/graphs/extractors/spacy.py - 清理 __init__.py 和 factory.py 中的 spacy 引用 - KnowledgeGraphSection.vue 抽取器类型仅保留 LLM + "更多拓展中"占位 - 补充测试:factory 仅支持 llm、reject spacy、service configure 拒绝 spacy - 更新 docs/intro/knowledge-base.md 文档描述 --- .../knowledge/graphs/extractors/__init__.py | 2 - .../knowledge/graphs/extractors/factory.py | 2 - .../yuxi/knowledge/graphs/extractors/spacy.py | 64 ------- .../unit/graphs/test_milvus_graph_build.py | 37 ++++- docs/intro/knowledge-base.md | 2 +- web/src/components/KnowledgeGraphSection.vue | 157 +++++++++--------- 6 files changed, 113 insertions(+), 151 deletions(-) delete mode 100644 backend/package/yuxi/knowledge/graphs/extractors/spacy.py diff --git a/backend/package/yuxi/knowledge/graphs/extractors/__init__.py b/backend/package/yuxi/knowledge/graphs/extractors/__init__.py index 104c53db..d8250dc6 100644 --- a/backend/package/yuxi/knowledge/graphs/extractors/__init__.py +++ b/backend/package/yuxi/knowledge/graphs/extractors/__init__.py @@ -1,12 +1,10 @@ from .base import GraphExtractor, normalize_extraction_result from .factory import GraphExtractorFactory from .llm import LLMGraphExtractor -from .spacy import SpacyGraphExtractor __all__ = [ "GraphExtractor", "GraphExtractorFactory", "LLMGraphExtractor", - "SpacyGraphExtractor", "normalize_extraction_result", ] diff --git a/backend/package/yuxi/knowledge/graphs/extractors/factory.py b/backend/package/yuxi/knowledge/graphs/extractors/factory.py index 864f1774..746b3064 100644 --- a/backend/package/yuxi/knowledge/graphs/extractors/factory.py +++ b/backend/package/yuxi/knowledge/graphs/extractors/factory.py @@ -4,13 +4,11 @@ from typing import Any from .base import GraphExtractor from .llm import LLMGraphExtractor -from .spacy import SpacyGraphExtractor class GraphExtractorFactory: _registry: dict[str, type[GraphExtractor]] = { "llm": LLMGraphExtractor, - "spacy": SpacyGraphExtractor, } @classmethod diff --git a/backend/package/yuxi/knowledge/graphs/extractors/spacy.py b/backend/package/yuxi/knowledge/graphs/extractors/spacy.py deleted file mode 100644 index 8a4b466a..00000000 --- a/backend/package/yuxi/knowledge/graphs/extractors/spacy.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import annotations - -import threading -from typing import Any - -from .base import GraphExtractor - - -_spacy_models: dict[str, Any] = {} -_spacy_model_lock = threading.Lock() - - -def _load_spacy_model(model_name: str) -> Any: - cached = _spacy_models.get(model_name) - if cached is not None: - return cached - - try: - import spacy - except ImportError as exc: - raise ValueError("spaCy 未安装,无法使用 spacy 抽取器") from exc - - with _spacy_model_lock: - cached = _spacy_models.get(model_name) - if cached is not None: - return cached - model = spacy.load(model_name) - _spacy_models[model_name] = model - return model - - -class SpacyGraphExtractor(GraphExtractor): - extractor_type = "spacy" - - def _model_name(self) -> str: - return str(self.options.get("model") or self.options.get("model_name") or "").strip() - - def validate_options(self) -> None: - if not self._model_name(): - raise ValueError("spaCy 抽取器需要 model 或 model_name") - - async def extract(self, text: str, *, chunk_metadata: dict[str, Any] | None = None) -> dict[str, Any]: - self.validate_options() - model_name = self._model_name() - allowed_labels = set(self.options.get("entity_labels") or []) - doc = _load_spacy_model(model_name)(text) - entities = [] - seen = set() - for ent in doc.ents: - entity_text = ent.text.strip() - if not entity_text or entity_text in seen: - continue - if allowed_labels and ent.label_ not in allowed_labels: - continue - seen.add(entity_text) - entities.append( - { - "text": entity_text, - "label": ent.label_ or "Entity", - "attributes": [], - } - ) - - return {"entities": entities, "relations": [], "metadata": {"model": model_name}} diff --git a/backend/test/unit/graphs/test_milvus_graph_build.py b/backend/test/unit/graphs/test_milvus_graph_build.py index cc614bbd..378d5c56 100644 --- a/backend/test/unit/graphs/test_milvus_graph_build.py +++ b/backend/test/unit/graphs/test_milvus_graph_build.py @@ -5,7 +5,11 @@ from unittest.mock import AsyncMock, MagicMock import pytest -from yuxi.knowledge.graphs.extractors import LLMGraphExtractor, normalize_extraction_result +from yuxi.knowledge.graphs.extractors import ( + GraphExtractorFactory, + LLMGraphExtractor, + normalize_extraction_result, +) from yuxi.knowledge.graphs.milvus_graph_service import MilvusGraphService @@ -88,6 +92,37 @@ def test_llm_graph_extractor_appends_schema_to_fixed_prompt(): assert "文本:\n张三任职于公司" in prompt +def test_graph_extractor_factory_supports_only_llm(): + assert GraphExtractorFactory.supported_types() == ["llm"] + + +def test_graph_extractor_factory_rejects_spacy(): + with pytest.raises(ValueError, match="spacy"): + GraphExtractorFactory.create("spacy", {"model": "zh_core_web_sm"}) + + +@pytest.mark.asyncio +async def test_milvus_graph_service_configure_rejects_spacy(): + kb = SimpleNamespace(kb_type="milvus", additional_params={}) + + class Repo: + async def get_by_kb_id(self, kb_id): + return kb + + async def update(self, kb_id, data): + raise AssertionError("unsupported extractor should not be persisted") + + service = MilvusGraphService(kb_repo=Repo()) + + with pytest.raises(ValueError, match="不支持的图谱抽取器类型"): + await service.configure( + "kb_test", + extractor_type="spacy", + extractor_options={"model": "zh_core_web_sm"}, + created_by="user_1", + ) + + @pytest.mark.asyncio async def test_milvus_graph_service_configure_persists_updated_concurrency(): kb = SimpleNamespace( diff --git a/docs/intro/knowledge-base.md b/docs/intro/knowledge-base.md index fe6218ba..41860338 100644 --- a/docs/intro/knowledge-base.md +++ b/docs/intro/knowledge-base.md @@ -75,7 +75,7 @@ Milvus 知识库详情页提供「知识图谱」Tab。图谱构建流程会从 主要能力: -- 配置 LLM 或 spaCy 抽取器 +- 配置 LLM 抽取器,更多抽取方式拓展中 - 构建待索引 chunks 的图谱实体与关系 - 查看构建状态、标签和统计信息 - 在知识库详情页搜索和展示子图 diff --git a/web/src/components/KnowledgeGraphSection.vue b/web/src/components/KnowledgeGraphSection.vue index effea9b4..fc52d8fa 100644 --- a/web/src/components/KnowledgeGraphSection.vue +++ b/web/src/components/KnowledgeGraphSection.vue @@ -245,69 +245,65 @@ message="修改配置仅影响后续构建;已构建的图谱不会自动重算,如需一致请重置后重新抽取。抽取器类型创建后不可修改。" /> -
+
- - +
@@ -317,6 +313,7 @@ import { ref, computed, watch, nextTick, onUnmounted, reactive } from 'vue' import { useDatabaseStore } from '@/stores/database' import { useTaskerStore } from '@/stores/tasker' +import { useConfigStore } from '@/stores/config' import { RefreshCw, Settings, @@ -348,6 +345,7 @@ const props = defineProps({ const store = useDatabaseStore() const taskerStore = useTaskerStore() +const configStore = useConfigStore() const kbId = computed(() => store.kbId) const kbType = computed(() => store.database.kb_type) @@ -373,13 +371,17 @@ const extractorTypeOptions = [ value: 'llm', label: 'LLM', description: '使用大模型按 Schema 抽取实体和关系', - icon: BrainCircuit + helper: '当前唯一支持的图谱抽取方式', + icon: BrainCircuit, + disabled: false }, { - value: 'spacy', - label: 'spaCy', - description: '【Beta】使用本地 NER 模型抽取实体', - icon: ScanText + value: 'more', + label: '更多', + description: '更多抽取方式正在拓展中', + helper: '拓展中', + icon: ScanText, + disabled: true } ] @@ -466,9 +468,7 @@ const graphConfigForm = reactive({ model_spec: '', schema: '', concurrency_count: 50, - model_params_text: '', - spacy_model: 'zh_core_web_sm', - entity_labels_text: '' + model_params_text: '' }) const graph = reactive(useGraph(graphRef)) @@ -504,13 +504,6 @@ const loadGraphBuildStatus = async () => { } } -const parseCommaSeparatedValues = (value) => { - return value - .split(',') - .map((item) => item.trim()) - .filter(Boolean) -} - const parseModelParams = () => { const text = graphConfigForm.model_params_text.trim() if (!text) return {} @@ -528,19 +521,14 @@ const parseModelParams = () => { const fillGraphConfigForm = () => { const config = graphBuildStatus.value?.config - if (!config) return - const options = config.extractor_options || {} - graphConfigForm.extractor_type = config.extractor_type || 'llm' - graphConfigForm.model_spec = options.model_spec || '' + const options = config?.extractor_options || {} + graphConfigForm.extractor_type = 'llm' + graphConfigForm.model_spec = options.model_spec || configStore.config?.default_model || '' graphConfigForm.schema = options.schema || '' - graphConfigForm.concurrency_count = Number(options.concurrency_count || 5) + graphConfigForm.concurrency_count = Number(options.concurrency_count || 50) graphConfigForm.model_params_text = options.model_params ? JSON.stringify(options.model_params) : '' - graphConfigForm.spacy_model = options.model || 'zh_core_web_sm' - graphConfigForm.entity_labels_text = Array.isArray(options.entity_labels) - ? options.entity_labels.join(', ') - : '' } const openGraphConfig = () => { @@ -548,26 +536,18 @@ const openGraphConfig = () => { showGraphConfig.value = true } -const selectExtractorType = (type) => { - if (isEditingGraphConfig.value) return - graphConfigForm.extractor_type = type +const selectExtractorType = (option) => { + if (isEditingGraphConfig.value || option.disabled) return + graphConfigForm.extractor_type = option.value } const buildExtractorOptions = () => { - if (graphConfigForm.extractor_type === 'spacy') { - return { - model: graphConfigForm.spacy_model, - entity_labels: parseCommaSeparatedValues(graphConfigForm.entity_labels_text) - } - } - - const result = { + return { model_spec: graphConfigForm.model_spec, schema: graphConfigForm.schema.trim(), - concurrency_count: graphConfigForm.concurrency_count || 1, + concurrency_count: graphConfigForm.concurrency_count || 50, model_params: parseModelParams() } - return result } const configureGraphBuild = async () => { @@ -575,7 +555,7 @@ const configureGraphBuild = async () => { document.activeElement?.blur() await nextTick() await graphBuildApi.configure(kbId.value, { - extractor_type: graphConfigForm.extractor_type, + extractor_type: 'llm', extractor_options: buildExtractorOptions() }) message.success(isEditingGraphConfig.value ? '图谱抽取配置已更新' : '图谱抽取配置已保存') @@ -1055,7 +1035,12 @@ onUnmounted(() => { &.disabled { cursor: not-allowed; - opacity: 0.78; + opacity: 0.72; + background: var(--gray-50); + + &:hover { + border-color: var(--gray-150); + } } .card-header { @@ -1083,6 +1068,16 @@ onUnmounted(() => { color: var(--gray-600); line-height: 1.5; } + + .card-helper { + margin-top: 8px; + font-size: 12px; + color: var(--gray-500); + + &.warning { + color: var(--color-warning-500); + } + } } }