Skip to content

Commit 9d4472c

Browse files
piexianRC-CHN
andauthored
fix: 改进知识库的初始化错误处理 (#7243)
* fix: 改进 KnowledgeBaseManager 和 KBHelper 中的初始化错误处理 * fix: 改进知识库初始化和重排序错误处理,增强日志记录 * fix: 改进知识库模块初始化和检索错误处理 * fix(ui): handle kb init errors in list cards display a dedicated error state for knowledge base cards that fail initialization, including a visible badge and error details prevent navigation and edit actions for failed cards while keeping delete available, and hide normal stats/description for error items add list.initError locale strings for en-US, ru-RU, and zh-CN * fix(kb): avoid replacing helper on init failure Initialize a new KB helper before swapping instances so a failed re-init does not break the active knowledge base service. If initialization fails, restore in-memory KB settings and keep the existing helper and previous init error state. Also clear stale init_error after successful vector DB initialization to prevent outdated error reporting. * test(kb): add kb manager resilience tests cover initialization failure and recovery scenarios to guard against regressions in kb error handling include reference assets under refs for test validation --------- Co-authored-by: RC-CHN <1051989940@qq.com>
1 parent e8d6938 commit 9d4472c

File tree

9 files changed

+498
-31
lines changed

9 files changed

+498
-31
lines changed

astrbot/core/knowledge_base/kb_helper.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ async def _repair_and_translate_chunk_with_retry(
108108
class KBHelper:
109109
vec_db: BaseVecDB
110110
kb: KnowledgeBase
111+
init_error: str | None
111112

112113
def __init__(
113114
self,
@@ -122,6 +123,7 @@ def __init__(
122123
self.prov_mgr = provider_manager
123124
self.kb_root_dir = kb_root_dir
124125
self.chunker = chunker
126+
self.init_error = None
125127

126128
self.kb_dir = Path(self.kb_root_dir) / self.kb.kb_id
127129
self.kb_medias_dir = Path(self.kb_dir) / "medias" / self.kb.kb_id
@@ -148,21 +150,28 @@ async def get_ep(self) -> EmbeddingProvider:
148150
async def get_rp(self) -> RerankProvider | None:
149151
if not self.kb.rerank_provider_id:
150152
return None
151-
rp: RerankProvider = await self.prov_mgr.get_provider_by_id(
153+
rp: RerankProvider | None = await self.prov_mgr.get_provider_by_id(
152154
self.kb.rerank_provider_id,
153155
) # type: ignore
154156
if not rp:
155-
raise ValueError(
156-
f"无法找到 ID 为 {self.kb.rerank_provider_id} 的 Rerank Provider",
157+
logger.warning(
158+
f"知识库 {self.kb.kb_name}({self.kb.kb_id}) 的 Rerank Provider({self.kb.rerank_provider_id}) 不可用,将跳过重排序。",
157159
)
160+
return None
158161
return rp
159162

160163
async def _ensure_vec_db(self) -> FaissVecDB:
161164
if not self.kb.embedding_provider_id:
162165
raise ValueError(f"知识库 {self.kb.kb_name} 未配置 Embedding Provider")
163166

164167
ep = await self.get_ep()
165-
rp = await self.get_rp()
168+
rp: RerankProvider | None = None
169+
try:
170+
rp = await self.get_rp()
171+
except Exception as e:
172+
logger.warning(
173+
f"知识库 {self.kb.kb_name}({self.kb.kb_id}) 初始化重排序能力失败,将跳过重排序: {e}",
174+
)
166175

167176
vec_db = FaissVecDB(
168177
doc_store_path=str(self.kb_dir / "doc.db"),
@@ -172,6 +181,8 @@ async def _ensure_vec_db(self) -> FaissVecDB:
172181
)
173182
await vec_db.initialize()
174183
self.vec_db = vec_db
184+
# Clear stale init_error once initialization succeeds.
185+
self.init_error = None
175186
return vec_db
176187

177188
async def delete_vec_db(self) -> None:
@@ -183,7 +194,7 @@ async def delete_vec_db(self) -> None:
183194
shutil.rmtree(self.kb_dir)
184195

185196
async def terminate(self) -> None:
186-
if self.vec_db:
197+
if hasattr(self, "vec_db") and self.vec_db:
187198
await self.vec_db.close()
188199

189200
async def upload_document(

astrbot/core/knowledge_base/kb_mgr.py

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import traceback
21
from pathlib import Path
32

43
from astrbot.core import logger
@@ -56,8 +55,7 @@ async def initialize(self) -> None:
5655
logger.error(f"知识库模块导入失败: {e}")
5756
logger.warning("请确保已安装所需依赖: pypdf, aiofiles, Pillow, rank-bm25")
5857
except Exception as e:
59-
logger.error(f"知识库模块初始化失败: {e}")
60-
logger.error(traceback.format_exc())
58+
logger.error(f"知识库模块初始化失败: {e}", exc_info=True)
6159

6260
async def _init_kb_database(self) -> None:
6361
self.kb_db = KBSQLiteDatabase(DB_PATH.as_posix())
@@ -76,7 +74,14 @@ async def load_kbs(self) -> None:
7674
kb_root_dir=FILES_PATH,
7775
chunker=CHUNKER,
7876
)
79-
await kb_helper.initialize()
77+
try:
78+
await kb_helper.initialize()
79+
except Exception as e:
80+
kb_helper.init_error = str(e)
81+
logger.error(
82+
f"知识库 {record.kb_name}({record.kb_id}) 初始化失败: {e}",
83+
exc_info=True,
84+
)
8085
self.kb_insts[record.kb_id] = kb_helper
8186

8287
async def create_kb(
@@ -179,6 +184,20 @@ async def update_kb(
179184
return None
180185

181186
kb = kb_helper.kb
187+
previous_state = {
188+
"kb_name": kb.kb_name,
189+
"description": kb.description,
190+
"emoji": kb.emoji,
191+
"embedding_provider_id": kb.embedding_provider_id,
192+
"rerank_provider_id": kb.rerank_provider_id,
193+
"chunk_size": kb.chunk_size,
194+
"chunk_overlap": kb.chunk_overlap,
195+
"top_k_dense": kb.top_k_dense,
196+
"top_k_sparse": kb.top_k_sparse,
197+
"top_m_final": kb.top_m_final,
198+
}
199+
previous_init_error = kb_helper.init_error
200+
182201
if kb_name is not None:
183202
kb.kb_name = kb_name
184203
if description is not None:
@@ -198,12 +217,47 @@ async def update_kb(
198217
kb.top_k_sparse = top_k_sparse
199218
if top_m_final is not None:
200219
kb.top_m_final = top_m_final
220+
221+
# Build a new helper first. Keep current vec_db alive until new init succeeds.
222+
new_helper = KBHelper(
223+
kb_db=self.kb_db,
224+
kb=kb,
225+
provider_manager=self.provider_manager,
226+
kb_root_dir=FILES_PATH,
227+
chunker=CHUNKER,
228+
)
229+
230+
try:
231+
await new_helper.initialize()
232+
except Exception as e:
233+
# Roll back in-memory settings and keep current helper available.
234+
kb.kb_name = previous_state["kb_name"]
235+
kb.description = previous_state["description"]
236+
kb.emoji = previous_state["emoji"]
237+
kb.embedding_provider_id = previous_state["embedding_provider_id"]
238+
kb.rerank_provider_id = previous_state["rerank_provider_id"]
239+
kb.chunk_size = previous_state["chunk_size"]
240+
kb.chunk_overlap = previous_state["chunk_overlap"]
241+
kb.top_k_dense = previous_state["top_k_dense"]
242+
kb.top_k_sparse = previous_state["top_k_sparse"]
243+
kb.top_m_final = previous_state["top_m_final"]
244+
kb_helper.init_error = previous_init_error
245+
logger.error(
246+
f"知识库 {kb.kb_name}({kb.kb_id}) 重新初始化失败,继续使用旧实例: {e}",
247+
exc_info=True,
248+
)
249+
return kb_helper
250+
201251
async with self.kb_db.get_db() as session:
202252
session.add(kb)
203253
await session.commit()
204254
await session.refresh(kb)
205255

206-
return kb_helper
256+
old_helper = kb_helper
257+
self.kb_insts[kb_id] = new_helper
258+
await old_helper.terminate()
259+
new_helper.init_error = None
260+
return new_helper
207261

208262
async def retrieve(
209263
self,
@@ -215,11 +269,21 @@ async def retrieve(
215269
"""从指定知识库中检索相关内容"""
216270
kb_ids = []
217271
kb_id_helper_map = {}
272+
unavailable_kbs = []
218273
for kb_name in kb_names:
219274
if kb_helper := await self.get_kb_by_name(kb_name):
275+
if kb_helper.init_error:
276+
unavailable_kbs.append((kb_name, kb_helper.init_error))
277+
logger.warning(f"知识库 {kb_name} 不可用: {kb_helper.init_error}")
278+
continue
220279
kb_ids.append(kb_helper.kb.kb_id)
221280
kb_id_helper_map[kb_helper.kb.kb_id] = kb_helper
222281

282+
# all requested KBs are unavailable
283+
if not kb_ids and unavailable_kbs:
284+
errors = "; ".join(f"{n}: {e}" for n, e in unavailable_kbs)
285+
raise ValueError(f"所有请求的知识库均不可用: {errors}")
286+
223287
if not kb_ids:
224288
return {}
225289

astrbot/core/knowledge_base/retrieval/manager.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,15 @@ async def retrieve(
184184
first_rerank = vec_db.rerank_provider
185185
break
186186
if first_rerank and retrieval_results:
187-
retrieval_results = await self._rerank(
188-
query=query,
189-
results=retrieval_results,
190-
top_k=top_m_final,
191-
rerank_provider=first_rerank,
192-
)
187+
try:
188+
retrieval_results = await self._rerank(
189+
query=query,
190+
results=retrieval_results,
191+
top_k=top_m_final,
192+
rerank_provider=first_rerank,
193+
)
194+
except Exception as e:
195+
logger.warning(f"Rerank 执行失败,已跳过重排序并使用融合结果: {e}")
193196

194197
return retrieval_results[:top_m_final]
195198

@@ -229,10 +232,10 @@ async def _dense_retrieve(
229232

230233
all_results.extend(vec_results)
231234
except Exception as e:
232-
from astrbot.core import logger
233-
234-
logger.warning(f"知识库 {kb_id} 稠密检索失败: {e}")
235-
continue
235+
logger.error(f"知识库 {kb_id} 稠密检索失败: {e}", exc_info=True)
236+
if len(kb_ids) == 1:
237+
raise RuntimeError(f"知识库 {kb_id} 稠密检索失败: {e}") from e
238+
# multi-KB: skip the faulty KB and continue
236239

237240
# 按相似度排序并返回 top_k
238241
all_results.sort(key=lambda x: x.similarity, reverse=True)

astrbot/dashboard/routes/knowledge_base.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,12 @@ async def list_kbs(self):
314314
# 转换为字典列表
315315
kb_list = []
316316
for kb in kbs:
317-
kb_list.append(kb.model_dump())
317+
kb_dict = kb.model_dump()
318+
# include init_error from KBHelper if present
319+
kb_helper = await kb_manager.get_kb(kb.kb_id)
320+
if kb_helper and kb_helper.init_error:
321+
kb_dict["init_error"] = kb_helper.init_error
322+
kb_list.append(kb_dict)
318323

319324
return (
320325
Response()

dashboard/src/i18n/locales/en-US/features/knowledge-base/index.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"loading": "Loading...",
1111
"documents": "Documents",
1212
"chunks": "Chunks",
13-
"sessionConfig": "Session Config"
13+
"sessionConfig": "Session Config",
14+
"initError": "Initialization Failed"
1415
},
1516
"card": {
1617
"edit": "Edit",

dashboard/src/i18n/locales/ru-RU/features/knowledge-base/index.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"loading": "Загрузка...",
1111
"documents": "док.",
1212
"chunks": "фрагм.",
13-
"sessionConfig": "Профиль"
13+
"sessionConfig": "Профиль",
14+
"initError": "Ошибка инициализации"
1415
},
1516
"card": {
1617
"edit": "Изменить",

dashboard/src/i18n/locales/zh-CN/features/knowledge-base/index.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"loading": "正在加载...",
1111
"documents": "文档",
1212
"chunks": "分块",
13-
"sessionConfig": "会话配置"
13+
"sessionConfig": "会话配置",
14+
"initError": "初始化失败"
1415
},
1516
"card": {
1617
"edit": "编辑",

dashboard/src/views/knowledge-base/KBList.vue

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,27 @@
2727
</div>
2828

2929
<div v-else-if="kbList.length > 0" class="kb-grid">
30-
<v-card v-for="kb in kbList" :key="kb.kb_id" class="kb-card" elevation="2" hover
31-
@click="navigateToDetail(kb.kb_id)">
32-
<div class="kb-card-content">
30+
<v-card v-for="kb in kbList" :key="kb.kb_id" class="kb-card" elevation="2" :hover="!kb.init_error"
31+
:class="{ 'kb-card-error': kb.init_error }"
32+
@click="!kb.init_error && navigateToDetail(kb.kb_id)">
33+
<!-- Error badge -->
34+
<v-badge v-if="kb.init_error" color="error" icon="mdi-alert-circle"
35+
class="kb-error-badge position-absolute" style="top: 0; right: 0; transform: translate(34%, -34%);" />
36+
<div class="kb-card-content" :class="{ 'kb-card-content-error': kb.init_error }">
3337
<div class="kb-emoji">{{ kb.emoji || '📚' }}</div>
3438
<h3 class="kb-name">{{ kb.kb_name }}</h3>
35-
<p class="kb-description text-medium-emphasis">{{ kb.description || '暂无描述' }}</p>
39+
<p v-if="!kb.init_error" class="kb-description text-medium-emphasis">{{ kb.description || '暂无描述' }}</p>
3640

37-
<div class="kb-stats mt-4">
41+
<!-- Error message display -->
42+
<div v-if="kb.init_error" class="kb-error-panel mt-3 mb-2">
43+
<div class="kb-error-title">
44+
<v-icon size="16" color="error">mdi-close-circle</v-icon>
45+
<span>{{ t('list.initError') }}</span>
46+
</div>
47+
<div class="kb-error-detail" :title="kb.init_error">{{ kb.init_error }}</div>
48+
</div>
49+
50+
<div class="kb-stats mt-4" v-if="!kb.init_error">
3851
<div class="stat-item">
3952
<v-icon size="small" color="primary">mdi-file-document</v-icon>
4053
<span>{{ kb.doc_count || 0 }} {{ t('list.documents') }}</span>
@@ -45,8 +58,8 @@
4558
</div>
4659
</div>
4760

48-
<div class="kb-actions">
49-
<v-btn icon="mdi-pencil" size="small" variant="text" color="info" @click.stop="editKB(kb)" />
61+
<div class="kb-actions" :class="{ 'error-actions': kb.init_error }">
62+
<v-btn v-if="!kb.init_error" icon="mdi-pencil" size="small" variant="text" color="info" @click.stop="editKB(kb)" />
5063
<v-btn icon="mdi-delete" size="small" variant="text" color="error" @click.stop="confirmDelete(kb)" />
5164
</div>
5265
</div>
@@ -478,6 +491,34 @@ onMounted(() => {
478491
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important;
479492
}
480493
494+
/* Error state card styles */
495+
.kb-card-error {
496+
cursor: not-allowed;
497+
border: 1px solid rgba(var(--v-theme-error), 0.3);
498+
background-color: rgba(var(--v-theme-error), 0.02) !important;
499+
overflow: visible; /* Allow badge to overflow */
500+
}
501+
502+
.kb-card-error:hover {
503+
transform: none;
504+
box-shadow: 0 4px 12px rgba(var(--v-theme-error), 0.1) !important;
505+
border-color: rgba(var(--v-theme-error), 0.5);
506+
}
507+
508+
.kb-card-error .kb-emoji {
509+
opacity: 0.7;
510+
filter: grayscale(0.5);
511+
}
512+
513+
.kb-card-error .kb-name {
514+
color: rgba(var(--v-theme-on-surface), 0.7);
515+
}
516+
517+
.kb-error-badge {
518+
z-index: 10;
519+
opacity: 0.9;
520+
}
521+
481522
.kb-card-content {
482523
padding: 24px;
483524
display: flex;
@@ -488,6 +529,11 @@ onMounted(() => {
488529
position: relative;
489530
}
490531
532+
.kb-card-content-error {
533+
justify-content: center;
534+
gap: 8px;
535+
}
536+
491537
.kb-emoji {
492538
font-size: 56px;
493539
margin-bottom: 8px;
@@ -518,6 +564,36 @@ onMounted(() => {
518564
justify-content: center;
519565
}
520566
567+
.kb-error-panel {
568+
width: 100%;
569+
text-align: left;
570+
background: rgba(var(--v-theme-error), 0.08);
571+
border: 1px solid rgba(var(--v-theme-error), 0.18);
572+
border-radius: 10px;
573+
padding: 10px 12px;
574+
}
575+
576+
.kb-error-title {
577+
display: flex;
578+
align-items: center;
579+
gap: 8px;
580+
font-size: 0.8rem;
581+
font-weight: 600;
582+
color: rgb(var(--v-theme-error));
583+
margin-bottom: 4px;
584+
}
585+
586+
.kb-error-detail {
587+
font-size: 0.78rem;
588+
line-height: 1.35;
589+
color: rgba(var(--v-theme-on-surface), 0.82);
590+
word-break: break-word;
591+
display: -webkit-box;
592+
-webkit-line-clamp: 3;
593+
-webkit-box-orient: vertical;
594+
overflow: hidden;
595+
}
596+
521597
.stat-item {
522598
display: flex;
523599
align-items: center;

0 commit comments

Comments
 (0)