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),