From bd3b2865021e5cb58dd98726c80f316c42d7306d Mon Sep 17 00:00:00 2001 From: EterUltimate <1831303476@qq.com> Date: Thu, 11 Jun 2026 18:05:23 +0800 Subject: [PATCH] fix: warn on high-cost lightrag livingmemory config --- _conf_schema.json | 6 +- config.py | 55 +++++++++++++++++++ docs/configuration.md | 5 ++ docs/integrations.md | 6 ++ pages/dashboard/app.js | 16 ++++++ pages/dashboard/index.html | 2 + pages/dashboard/styles.css | 35 ++++++++++++ tests/integration/test_config_blueprint.py | 1 + tests/integration/test_webui_static_assets.py | 16 ++++++ tests/unit/test_config.py | 55 ++++++++++++++++++- tests/unit/test_config_service.py | 41 ++++++++++++++ tests/unit/test_integration_service.py | 34 ++++++++++++ web_res/static/html/dashboard.html | 53 ++++++++++++++++++ webui/services/config_service.py | 3 + webui/services/integration_service.py | 6 ++ 15 files changed, 330 insertions(+), 4 deletions(-) diff --git a/_conf_schema.json b/_conf_schema.json index 7a4fee1f..dbb135b9 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -669,12 +669,12 @@ "Integration_Settings": { "description": "功能融合", "type": "object", - "hint": "与专门插件协同:记忆交给 LivingMemory,回复决策和生成交给 Group Chat Plus。本插件保留学习、审查、黑话、表达方式和上下文注入能力。", + "hint": "与专门插件协同:记忆交给 LivingMemory,回复决策和生成交给 Group Chat Plus。本插件保留学习、审查、黑话、表达方式和上下文注入能力。若同时使用 LightRAG hybrid/mix 查询与 LivingMemory 委托,可能显著增加 LLM 调用与 token 消耗。", "items": { "delegate_memory_to_livingmemory": { "description": "记忆委托给 LivingMemory", "type": "bool", - "hint": "开启后,检测到 LivingMemory 插件已加载时,本插件不再写入或注入本地长期记忆,避免双重记忆系统互相污染。", + "hint": "开启后,检测到 LivingMemory 插件已加载时,本插件不再写入或注入本地长期记忆,避免双重记忆系统互相污染。若 V2 知识引擎使用 LightRAG 且查询模式为 hybrid/mix,建议评估调用量与 token 成本。", "default": true }, "livingmemory_plugin_name": { @@ -755,7 +755,7 @@ "lightrag_query_mode": { "description": "LightRAG 查询模式", "type": "string", - "hint": "LightRAG检索模式。local=仅实体邻域检索(低延迟),hybrid=实体邻域+全局社区聚合(高质量但慢约4-5秒),naive=纯向量检索,global=仅全局社区,mix=混合模式", + "hint": "LightRAG检索模式。local=仅实体邻域检索(低延迟),hybrid=实体邻域+全局社区聚合(高质量但慢约4-5秒),naive=纯向量检索,global=仅全局社区,mix=混合模式。若同时委托 LivingMemory,hybrid/mix 会叠加记忆检索与融合上下文,可能显著增加 LLM 调用与 token 消耗,优先建议 local/naive。", "default": "local" }, "memory_engine": { diff --git a/config.py b/config.py index 14bd3474..7fb3b13a 100644 --- a/config.py +++ b/config.py @@ -19,6 +19,12 @@ DEFAULT_DB_TYPE = "postgresql" SUPPORTED_DB_TYPES = {"sqlite", "mysql", "postgresql"} POSTGRESQL_DB_TYPE_ALIASES = {"postgres", "pg", "pgsql"} +HIGH_COST_LIGHTRAG_QUERY_MODES = {"hybrid", "mix"} +LIGHTRAG_LIVINGMEMORY_COST_WARNING = ( + "当前配置选择 LightRAG 的 hybrid/mix 查询,并允许记忆委托给 LivingMemory;" + "当 LivingMemory 插件已加载时,会叠加 LightRAG 全局/混合检索与 LivingMemory 记忆检索," + "可能显著增加 LLM 调用与 token 消耗。建议优先改为 local/naive,或只保留一种记忆/检索策略。" +) def normalize_db_type(db_type: Any) -> Optional[str]: @@ -31,6 +37,53 @@ def normalize_db_type(db_type: Any) -> Optional[str]: return value +def _read_config_value(config_like: Any, key: str, default: Any = None) -> Any: + if isinstance(config_like, dict): + if key in config_like: + return config_like.get(key, default) + for group_key in ("V2_Architecture_Settings", "Integration_Settings"): + group = config_like.get(group_key) + if isinstance(group, dict) and key in group: + return group.get(key, default) + return default + return getattr(config_like, key, default) + + +def _read_config_bool(config_like: Any, key: str, default: bool = False) -> bool: + value = _read_config_value(config_like, key, default) + if isinstance(value, bool): + return value + if value is None: + return default + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "yes", "on"} + return bool(value) + + +def is_lightrag_livingmemory_high_cost_config(config_like: Any) -> bool: + """Return True for the known high-cost LightRAG + LivingMemory combination.""" + knowledge_engine = str( + _read_config_value(config_like, "knowledge_engine", "legacy") or "legacy" + ).strip().lower() + query_mode = str( + _read_config_value(config_like, "lightrag_query_mode", "local") or "local" + ).strip().lower() + delegate_memory = _read_config_bool(config_like, "delegate_memory_to_livingmemory") + + return ( + knowledge_engine == "lightrag" + and query_mode in HIGH_COST_LIGHTRAG_QUERY_MODES + and delegate_memory + ) + + +def get_config_cost_warnings(config_like: Any) -> List[str]: + """Return non-blocking warnings for expensive cross-feature config combinations.""" + if is_lightrag_livingmemory_high_cost_config(config_like): + return [LIGHTRAG_LIVINGMEMORY_COST_WARNING] + return [] + + def normalize_identifier_list(value: Any, *, full_learning_markers: bool = False) -> List[str]: """Normalize user/group identifier lists from AstrBot settings.""" if value is None: @@ -660,6 +713,8 @@ def validate_config(self) -> List[str]: errors.append("PostgreSQL schema 不能为空") # 提示性警告而非错误 + errors.extend(f" {warning}" for warning in get_config_cost_warnings(self)) + provider_warnings = [] if not self.filter_provider_id: provider_warnings.append("未配置筛选模型提供商ID,将尝试自动配置或使用备选模型") diff --git a/docs/configuration.md b/docs/configuration.md index 11d1ea20..e9a4e132 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -151,6 +151,11 @@ PostgreSQL 支持 `postgresql_schema`,非 `public` 时会自动创建 schema | `lightrag_query_mode` | `local` | LightRAG 查询模式 | | `memory_engine` | `legacy` | `legacy` 或 `mem0` | +成本提示: 当 `knowledge_engine="lightrag"` 且 `lightrag_query_mode` 为 +`hybrid` 或 `mix` 时,如果同时允许 `delegate_memory_to_livingmemory`, +LivingMemory 已加载后会叠加 LightRAG 全局/混合检索与记忆检索,可能明显增加 +LLM 调用和 token 消耗。优先建议使用 `local`/`naive`,或只保留一种记忆/检索策略。 + 只有 `knowledge_engine != "legacy"` 或 `memory_engine != "legacy"` 时才创建 `V2LearningIntegration`。 `embedding_provider_id` 只显示 Embedding Provider,`rerank_provider_id` 只显示 Reranker Provider。聊天模型不会混入这两个下拉框。 diff --git a/docs/integrations.md b/docs/integrations.md index 90e37db7..f4a30fd1 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -36,6 +36,12 @@ Self Learning 负责学习、审查、黑话、表达方式和 LLM 上下文注 委托只在目标插件已加载、激活且存在 `star_cls` 时生效。未检测到目标插件时,本插件自动回退到本地能力。 +成本提示: 如果 V2 架构同时选择 `knowledge_engine="lightrag"`、 +`lightrag_query_mode="hybrid"`/`"mix"`,并开启 LivingMemory 记忆委托, +一次上下文增强可能同时触发 LightRAG 全局/混合检索与 LivingMemory 记忆检索。 +设置页和功能融合页会显示非阻塞提醒。需要控制成本时,优先将 LightRAG 查询模式改为 +`local`/`naive`,或只保留一种记忆/检索策略。 + ## 运行路径 ### 记忆委托 diff --git a/pages/dashboard/app.js b/pages/dashboard/app.js index 3e02f082..1b8cef85 100644 --- a/pages/dashboard/app.js +++ b/pages/dashboard/app.js @@ -316,6 +316,20 @@ }, 3200); } + function warningListHtml(warnings) { + const items = Array.isArray(warnings) + ? warnings + .map((message) => String(message).trim()) + .filter(Boolean) + : []; + return items.map((message) => ` +
+ 成本提醒 + ${escapeHtml(message)} +
+ `).join(""); + } + function showErrors(errors) { const panel = $("error-panel"); if (!panel) return; @@ -947,6 +961,7 @@ function renderIntegrations(data) { setHtml("integration-cards", (data.dashboards || []).map(integrationCardHtml).join("") || empty("暂无融合状态")); + setHtml("integration-warnings", warningListHtml(data.warnings || [])); const settings = data.settings || {}; setHtml("integration-settings", Object.entries(settings).map(([key, value]) => `
@@ -1072,6 +1087,7 @@ function renderSettings(data) { const schema = data.schema || {}; + setHtml("settings-warnings", warningListHtml(schema.warnings || data.warnings || [])); const groups = schema.groups || []; if (!state.settingsGroup && groups.length) state.settingsGroup = groups[0].key; setHtml("settings-groups", groups.map((group) => ` diff --git a/pages/dashboard/index.html b/pages/dashboard/index.html index df61426b..c98ac401 100644 --- a/pages/dashboard/index.html +++ b/pages/dashboard/index.html @@ -423,6 +423,7 @@

MaiBot 学习数据迁移

融合设置

+
@@ -438,6 +439,7 @@

设置

+
diff --git a/pages/dashboard/styles.css b/pages/dashboard/styles.css index d9fe1a3e..9d5a301e 100644 --- a/pages/dashboard/styles.css +++ b/pages/dashboard/styles.css @@ -504,6 +504,41 @@ button.is-busy { margin-bottom: 14px; } +.settings-warning-list { + display: grid; + gap: 8px; + margin: 0 0 14px; +} + +.settings-warning { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 10px; + align-items: start; + padding: 10px 12px; + border: 1px solid rgba(217, 119, 6, 0.28); + border-radius: 8px; + color: #92400e; + background: rgba(217, 119, 6, 0.10); +} + +.settings-warning strong { + white-space: nowrap; + font-size: 12px; +} + +.settings-warning span { + min-width: 0; + overflow-wrap: anywhere; + font-size: 12px; + line-height: 1.55; +} + +html[data-theme="dark"] .settings-warning { + color: #fbbf24; + background: rgba(217, 119, 6, 0.16); +} + .panel { padding: 16px; } diff --git a/tests/integration/test_config_blueprint.py b/tests/integration/test_config_blueprint.py index 3a92d5e8..db8cdd42 100644 --- a/tests/integration/test_config_blueprint.py +++ b/tests/integration/test_config_blueprint.py @@ -91,6 +91,7 @@ async def test_config_schema_route_returns_groups(client): assert response.status_code == 200 data = await response.get_json() assert "groups" in data + assert "warnings" in data assert any(group["key"] == "Database_Settings" for group in data["groups"]) assert any( field["key"] == "relevance_threshold" diff --git a/tests/integration/test_webui_static_assets.py b/tests/integration/test_webui_static_assets.py index f7fd0be9..31c68855 100644 --- a/tests/integration/test_webui_static_assets.py +++ b/tests/integration/test_webui_static_assets.py @@ -273,6 +273,22 @@ def test_dashboard_exposes_companion_plugin_api_hub(): assert "GET /api/data/overview" in service +def test_dashboard_exposes_config_cost_warnings(): + text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8") + index = (PLUGIN_ROOT / "pages" / "dashboard" / "index.html").read_text(encoding="utf-8") + app = (PLUGIN_ROOT / "pages" / "dashboard" / "app.js").read_text(encoding="utf-8") + service = (PLUGIN_ROOT / "webui" / "services" / "config_service.py").read_text(encoding="utf-8") + + assert "configWarnings" in text + assert "settings-warning-list" in text + assert "成本提醒" in text + assert "settings-warnings" in index + assert "integration-warnings" in index + assert "warningListHtml" in app + assert "schema.warnings" in app + assert "get_config_cost_warnings" in service + + def test_dashboard_exposes_tiered_dependency_install_controls(): text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8") diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index ba1feddb..daf2bce8 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -14,7 +14,14 @@ import pytest from unittest.mock import patch, MagicMock -from config import DEFAULT_DATA_DIR, DEFAULT_DB_TYPE, PluginConfig, normalize_db_type +from config import ( + DEFAULT_DATA_DIR, + DEFAULT_DB_TYPE, + PluginConfig, + get_config_cost_warnings, + is_lightrag_livingmemory_high_cost_config, + normalize_db_type, +) @pytest.mark.unit @@ -461,6 +468,52 @@ def test_partial_providers_configured(self): blocking_errors = [e for e in errors if not e.startswith(" ")] assert len(blocking_errors) == 0 + def test_lightrag_hybrid_livingmemory_combo_warns_without_blocking(self): + """LightRAG hybrid plus LivingMemory delegation should surface cost warnings.""" + config = PluginConfig( + filter_provider_id="provider_1", + knowledge_engine="lightrag", + lightrag_query_mode="hybrid", + delegate_memory_to_livingmemory=True, + ) + + warnings = get_config_cost_warnings(config) + errors = config.validate_config() + blocking_errors = [e for e in errors if not e.startswith(" ")] + + assert is_lightrag_livingmemory_high_cost_config(config) is True + assert warnings + assert "LivingMemory" in warnings[0] + assert "token" in warnings[0] + assert any("LivingMemory" in error and error.startswith(" ") for error in errors) + assert blocking_errors == [] + + def test_lightrag_local_livingmemory_combo_does_not_warn(self): + """Low-latency LightRAG local mode should not raise the high-cost warning.""" + config = PluginConfig( + knowledge_engine="lightrag", + lightrag_query_mode="local", + delegate_memory_to_livingmemory=True, + ) + + assert is_lightrag_livingmemory_high_cost_config(config) is False + assert get_config_cost_warnings(config) == [] + + def test_lightrag_hybrid_string_false_delegation_does_not_warn(self): + """Raw grouped config values should parse string false as disabled.""" + raw_config = { + "V2_Architecture_Settings": { + "knowledge_engine": "lightrag", + "lightrag_query_mode": "hybrid", + }, + "Integration_Settings": { + "delegate_memory_to_livingmemory": "false", + }, + } + + assert is_lightrag_livingmemory_high_cost_config(raw_config) is False + assert get_config_cost_warnings(raw_config) == [] + @pytest.mark.parametrize( "raw_db_type", ["postgres", "pg", "pgsql", "postgresql", "", None], diff --git a/tests/unit/test_config_service.py b/tests/unit/test_config_service.py index 48b8d8d6..e6d04a03 100644 --- a/tests/unit/test_config_service.py +++ b/tests/unit/test_config_service.py @@ -203,6 +203,24 @@ async def test_get_config_schema_includes_full_settings(self, tmp_path): assert schema["provider_options_by_type"]["embedding"][0]["value"] == "embed-a" assert schema["provider_options_by_type"]["rerank"][0]["value"] == "rerank-a" + @pytest.mark.asyncio + async def test_config_schema_exposes_lightrag_livingmemory_cost_warning(self, tmp_path): + container = build_container(tmp_path) + container.plugin_config.knowledge_engine = "lightrag" + container.plugin_config.lightrag_query_mode = "hybrid" + container.plugin_config.delegate_memory_to_livingmemory = True + + schema = await ConfigService(container).get_config_schema() + groups = {group["key"]: group for group in schema["groups"]} + v2_fields = {field["key"]: field for field in groups["V2_Architecture_Settings"]["fields"]} + integration_fields = {field["key"]: field for field in groups["Integration_Settings"]["fields"]} + + assert schema["warnings"] + assert "LivingMemory" in schema["warnings"][0] + assert "token" in schema["warnings"][0] + assert "LivingMemory" in v2_fields["lightrag_query_mode"]["hint"] + assert "LightRAG" in integration_fields["delegate_memory_to_livingmemory"]["hint"] + @pytest.mark.asyncio async def test_provider_schema_uses_astrbot_provider_config_classification(self, tmp_path): plugin_config = PluginConfig.create_default() @@ -491,6 +509,29 @@ async def test_update_config_persists_and_syncs_paths(self, tmp_path): assert saved["relevance_threshold"] == 0.75 assert saved["log_level"] == "debug" + @pytest.mark.asyncio + async def test_update_config_returns_cost_warning_for_high_cost_combo(self, tmp_path): + container = build_container(tmp_path) + service = ConfigService(container) + + success, message, updated = await service.update_config( + { + "V2_Architecture_Settings": { + "knowledge_engine": "lightrag", + "lightrag_query_mode": "hybrid", + }, + "Integration_Settings": { + "delegate_memory_to_livingmemory": True, + }, + } + ) + + assert success is True + assert updated["knowledge_engine"] == "lightrag" + assert updated["lightrag_query_mode"] == "hybrid" + assert "LivingMemory" in message + assert "token" in message + @pytest.mark.asyncio async def test_update_config_syncs_webui_changes_to_plugin_page_config_and_runtime(self, tmp_path): container = build_container(tmp_path) diff --git a/tests/unit/test_integration_service.py b/tests/unit/test_integration_service.py index 7689e925..4f9c43fb 100644 --- a/tests/unit/test_integration_service.py +++ b/tests/unit/test_integration_service.py @@ -62,6 +62,8 @@ def test_integration_service_reports_companion_dashboards_and_dev_apis(): delegate_reply_to_group_chat_plus=True, group_chat_plus_plugin_name="astrbot_plugin_group_chat_plus", disable_local_reply_when_delegated=True, + knowledge_engine="legacy", + lightrag_query_mode="local", ), webui_config=SimpleNamespace(host="127.0.0.1", port=8989), feature_delegation=delegation, @@ -88,3 +90,35 @@ def test_integration_service_reports_companion_dashboards_and_dev_apis(): group_chat_plus_embed = IntegrationService(container).get_embed_target("reply-strategy") assert livingmemory_embed["target_url"] == "http://127.0.0.1:8888" assert group_chat_plus_embed["target_url"] == "http://127.0.0.1:8787/panel?embed=1" + + +def test_integration_service_reports_high_cost_v2_warning(): + container = SimpleNamespace( + plugin_config=SimpleNamespace( + delegate_memory_to_livingmemory=True, + livingmemory_plugin_name="LivingMemory", + disable_local_memory_when_delegated=True, + delegate_reply_to_group_chat_plus=True, + group_chat_plus_plugin_name="astrbot_plugin_group_chat_plus", + disable_local_reply_when_delegated=True, + knowledge_engine="lightrag", + lightrag_query_mode="mix", + ), + webui_config=SimpleNamespace(host="127.0.0.1", port=8989), + feature_delegation=SimpleNamespace( + status=lambda: { + "memory_delegated": True, + "memory_plugin": "LivingMemory", + "reply_delegated": False, + "reply_plugin": None, + }, + memory_plugin=lambda: None, + reply_plugin=lambda: None, + ), + ) + + payload = IntegrationService(container).get_status() + + assert payload["warnings"] + assert "LivingMemory" in payload["warnings"][0] + assert "token" in payload["warnings"][0] diff --git a/web_res/static/html/dashboard.html b/web_res/static/html/dashboard.html index 6b499036..e81f585e 100644 --- a/web_res/static/html/dashboard.html +++ b/web_res/static/html/dashboard.html @@ -2336,6 +2336,36 @@ color: var(--danger); } + .settings-warning-list { + display: grid; + gap: 8px; + margin: 0 0 12px; + } + + .settings-warning { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 10px; + align-items: start; + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--warning), transparent 68%); + border-radius: var(--radius-md); + color: var(--warning); + background: var(--warning-soft); + font-size: var(--text-sm); + line-height: 1.55; + } + + .settings-warning strong { + white-space: nowrap; + color: var(--text); + } + + .settings-warning span { + min-width: 0; + overflow-wrap: anywhere; + } + .settings-empty { padding: 24px 0 8px; color: var(--muted); @@ -3678,6 +3708,7 @@

手动安装依赖

依赖安装需手动确认,不会在插件安装或启动时自动执行。
+
正在加载配置中。
@@ -6199,6 +6230,27 @@

手动安装依赖

return state.config.schema || { groups: [], config: {} }; } + function getConfigWarnings() { + const payload = getConfigSchemaPayload(); + return safeArray(payload.warnings || payload.config_warnings) + .map((warning) => String(warning || '').trim()) + .filter(Boolean); + } + + function renderConfigWarnings() { + const warningsEl = $('configWarnings'); + if (!warningsEl) { + return; + } + const warnings = getConfigWarnings(); + warningsEl.innerHTML = warnings.map((warning) => ` +
+ 成本提醒 + ${escapeHtml(warning)} +
+ `).join(''); + } + function getConfigEditableKeys() { const payload = getConfigSchemaPayload(); return safeArray(payload.groups) @@ -6633,6 +6685,7 @@

手动安装依赖

: '
暂无配置项。
'; } + renderConfigWarnings(); state.config.dirtyKeys = new Set(state.config.dirtyKeys); updateConfigSummary(); applyConfigSearch(); diff --git a/webui/services/config_service.py b/webui/services/config_service.py index 7d05b465..cc007d3b 100644 --- a/webui/services/config_service.py +++ b/webui/services/config_service.py @@ -19,9 +19,11 @@ ) try: + from ...config import get_config_cost_warnings from ...statics.messages import FileNames from ...utils.logging_utils import apply_astrbot_log_level except ImportError: + from config import get_config_cost_warnings from statics.messages import FileNames from utils.logging_utils import apply_astrbot_log_level @@ -1045,6 +1047,7 @@ async def get_config_schema(self) -> Dict[str, Any]: return { "config": self.plugin_config.to_dict(), "groups": self._build_group_schema(merged_schema), + "warnings": get_config_cost_warnings(self.plugin_config), "provider_options": self._provider_options(), "provider_options_by_type": { "chat_completion": self._provider_options("chat_completion"), diff --git a/webui/services/integration_service.py b/webui/services/integration_service.py index 1048fcf0..8e9d01b1 100644 --- a/webui/services/integration_service.py +++ b/webui/services/integration_service.py @@ -4,6 +4,11 @@ from typing import Any, Dict, Optional +try: + from ...config import get_config_cost_warnings +except ImportError: + from config import get_config_cost_warnings + LIVINGMEMORY_EMBED_URL = "/api/integrations/embed/livingmemory" GROUP_CHAT_PLUS_EMBED_URL = "/api/integrations/embed/group_chat_plus" @@ -95,6 +100,7 @@ def get_status(self) -> Dict[str, Any]: return { "delegation": status, "settings": self._settings(config), + "warnings": get_config_cost_warnings(config), "dashboards": [ self._self_learning_dashboard(), self._livingmemory_dashboard(memory_star, status),