linxinhua commited on
Commit
1deeedd
·
verified ·
1 Parent(s): 29555e6

Update vectorize_knowledge_base.py from CIV3283/CIV3283_admin

Browse files
Files changed (1) hide show
  1. vectorize_knowledge_base.py +584 -0
vectorize_knowledge_base.py ADDED
@@ -0,0 +1,584 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import numpy as np
5
+ import pandas as pd
6
+ from typing import List, Dict, Tuple, Optional
7
+ from openai import OpenAI
8
+ from datetime import datetime
9
+ import csv
10
+
11
+ class KnowledgeBaseVectorizer:
12
+ def __init__(self, api_key: str, data_path: str = ""):
13
+ """
14
+ 初始化向量化器
15
+
16
+ Args:
17
+ api_key: OpenAI API密钥
18
+ data_path: knowledge_base.md文件的路径
19
+ """
20
+ self.client = OpenAI(api_key=api_key)
21
+ self.data_path = data_path
22
+ self.embedding_model = "text-embedding-3-small"
23
+ #self.vector_db_path = os.path.join(os.path.dirname(data_path), "vector_database.csv")
24
+ #self.metadata_path = os.path.join(os.path.dirname(data_path), "vector_metadata.json")
25
+ self.vector_db_path = "vector_database.csv"
26
+ self.metadata_path = "vector_metadata.json"
27
+
28
+ # 缓存相关属性
29
+ self._cached_df = None
30
+ self._cached_metadata = None
31
+ self._cached_embeddings = {} # 缓存不同类型的向量矩阵
32
+ self._last_load_time = None
33
+
34
+ def parse_knowledge_base(self) -> List[Dict]:
35
+ """
36
+ 解析knowledge_base.md文件,提取所有数据条目
37
+ 支持包含表格的完整内容提取
38
+
39
+ Returns:
40
+ 包含所有数据条目的列表,每个条目是一个字典
41
+ """
42
+ entries = []
43
+
44
+ try:
45
+ with open(self.data_path, 'r', encoding='utf-8') as f:
46
+ content = f.read()
47
+ except FileNotFoundError:
48
+ print(f"错误:找不到文件 {self.data_path}")
49
+ return entries
50
+
51
+ # 改进的匹配策略:使用更精确的正则表达式
52
+ # 匹配模式:# xx-xx-xx title **source** ... **content** ... (直到下一个 # 或文件结尾)
53
+ pattern = r'#\s+(\d{2}-\d{2}-\d{2})\s+([^\n]+)\s+\*\*source\*\*\s+([^\n]+)\s+\*\*content\*\*\s+(.*?)(?=\n#\s+\d{2}-\d{2}-\d{2}|$)'
54
+
55
+ matches = re.findall(pattern, content, re.DOTALL)
56
+
57
+ for match in matches:
58
+ # 清理内容:移除多余的空白行,但保留表格格式
59
+ content_text = match[3].strip()
60
+
61
+ # 保留表格的结构,但清理多余的空白
62
+ content_lines = content_text.split('\n')
63
+ cleaned_lines = []
64
+
65
+ for line in content_lines:
66
+ # 保留非空行和表格行
67
+ if line.strip() or (line.startswith('|') and line.endswith('|')):
68
+ cleaned_lines.append(line.rstrip())
69
+
70
+ # 重新组合内容
71
+ cleaned_content = '\n'.join(cleaned_lines)
72
+
73
+ entry = {
74
+ 'id': match[0].strip(),
75
+ 'title': match[1].strip(),
76
+ 'source': match[2].strip(),
77
+ 'content': cleaned_content,
78
+ 'full_text': f"{match[1].strip()} {cleaned_content}" # 用于向量化的完整文本
79
+ }
80
+ entries.append(entry)
81
+
82
+ print(f"成功解析 {len(entries)} 个数据条目")
83
+
84
+ # 打印一些调试信息
85
+ if entries:
86
+ print("前3个条目的内容长度:")
87
+ for i, entry in enumerate(entries[:3]):
88
+ content_lines = entry['content'].count('\n') + 1
89
+ has_table = '|' in entry['content']
90
+ print(f" 条目 {entry['id']}: {len(entry['content'])} 字符, {content_lines} 行, 包含表格: {has_table}")
91
+
92
+ return entries
93
+
94
+ def get_embedding(self, text: str) -> List[float]:
95
+ """
96
+ 使用OpenAI API获取文本的向量表示
97
+
98
+ Args:
99
+ text: 要向量化的文本
100
+
101
+ Returns:
102
+ 文本的向量表示
103
+ """
104
+ try:
105
+ response = self.client.embeddings.create(
106
+ input=text,
107
+ model=self.embedding_model
108
+ )
109
+ return response.data[0].embedding
110
+ except Exception as e:
111
+ print(f"获取向量时出错: {e}")
112
+ return []
113
+
114
+ def batch_get_embeddings(self, texts: List[str], batch_size: int = 10) -> List[List[float]]:
115
+ """
116
+ 批量获取文本的向量表示
117
+
118
+ Args:
119
+ texts: 要向量化的文本列表
120
+ batch_size: 批处理大小
121
+
122
+ Returns:
123
+ 向量列表
124
+ """
125
+ embeddings = []
126
+
127
+ for i in range(0, len(texts), batch_size):
128
+ batch = texts[i:i + batch_size]
129
+ print(f"处理批次 {i//batch_size + 1}/{(len(texts) + batch_size - 1)//batch_size}")
130
+
131
+ try:
132
+ response = self.client.embeddings.create(
133
+ input=batch,
134
+ model=self.embedding_model
135
+ )
136
+ batch_embeddings = [item.embedding for item in response.data]
137
+ embeddings.extend(batch_embeddings)
138
+ except Exception as e:
139
+ print(f"批次处理出错: {e}")
140
+ # 如果批处理失败,尝试单个处理
141
+ for text in batch:
142
+ embedding = self.get_embedding(text)
143
+ embeddings.append(embedding if embedding else [0] * 1536) # 默认维度
144
+
145
+ return embeddings
146
+
147
+ def create_vector_database(self):
148
+ """
149
+ 创建向量数据库并保存为CSV文件
150
+ 支持标题和内容的分别向量化
151
+ """
152
+ print("开始创建向量数据库...")
153
+
154
+ # 1. 解析知识库
155
+ entries = self.parse_knowledge_base()
156
+ if not entries:
157
+ print("没有找到任何数据条目")
158
+ return
159
+
160
+ # 2. 准备要向量化的文本
161
+ titles = [entry['title'] for entry in entries]
162
+ contents = [entry['content'] for entry in entries]
163
+ full_texts = [entry['full_text'] for entry in entries]
164
+
165
+ # 3. 批量获取向量
166
+ print("开始向量化标题...")
167
+ title_embeddings = self.batch_get_embeddings(titles)
168
+
169
+ print("开始向量化内容...")
170
+ content_embeddings = self.batch_get_embeddings(contents)
171
+
172
+ print("开始向量化完整文本...")
173
+ full_embeddings = self.batch_get_embeddings(full_texts)
174
+
175
+ # 4. 创建DataFrame来存储数据
176
+ print("创建向量数据库DataFrame...")
177
+
178
+ # 准备数据行
179
+ rows = []
180
+ for i, entry in enumerate(entries):
181
+ row = {
182
+ 'index': i,
183
+ 'id': entry['id'],
184
+ 'title': entry['title'],
185
+ 'source': entry['source'],
186
+ 'content': entry['content'],
187
+ 'full_text': entry['full_text']
188
+ }
189
+
190
+ # 添加标题向量维度
191
+ for j, val in enumerate(title_embeddings[i]):
192
+ row[f'title_dim_{j}'] = val
193
+
194
+ # 添加内容向量维度
195
+ for j, val in enumerate(content_embeddings[i]):
196
+ row[f'content_dim_{j}'] = val
197
+
198
+ # 添加完整文本向量维度
199
+ for j, val in enumerate(full_embeddings[i]):
200
+ row[f'full_dim_{j}'] = val
201
+
202
+ rows.append(row)
203
+
204
+ # 创建DataFrame
205
+ df = pd.DataFrame(rows)
206
+
207
+ # 5. 保存为CSV文件
208
+ print("保存向量数据库到CSV...")
209
+ df.to_csv(self.vector_db_path, index=False, encoding='utf-8')
210
+
211
+ # 6. 保存元数据(JSON格式,便于查看)
212
+ metadata = {
213
+ 'embedding_model': self.embedding_model,
214
+ 'created_at': datetime.now().isoformat(),
215
+ 'num_entries': len(entries),
216
+ 'embedding_dimensions': len(title_embeddings[0]) if title_embeddings else 0,
217
+ 'vector_types': ['title', 'content', 'full'],
218
+ 'columns': list(df.columns),
219
+ 'entries_summary': [
220
+ {
221
+ 'id': entry['id'],
222
+ 'title': entry['title'],
223
+ 'source': entry['source']
224
+ } for entry in entries
225
+ ]
226
+ }
227
+
228
+ with open(self.metadata_path, 'w', encoding='utf-8') as f:
229
+ json.dump(metadata, f, ensure_ascii=False, indent=2)
230
+
231
+ print(f"向量数据库创建完成!")
232
+ print(f"向量数据库保存在: {self.vector_db_path}")
233
+ print(f"元数据保存在: {self.metadata_path}")
234
+ print(f"总共处理了 {len(entries)} 个条目")
235
+ print(f"每个向量的维度: {len(title_embeddings[0]) if title_embeddings else 0}")
236
+
237
+ # 清除缓存以便重新加载
238
+ self.clear_cache()
239
+
240
+ def clear_cache(self):
241
+ """清除所有缓存"""
242
+ self._cached_df = None
243
+ self._cached_metadata = None
244
+ self._cached_embeddings = {}
245
+ self._last_load_time = None
246
+ print("向量数据库缓存已清除")
247
+
248
+ def load_vector_database(self, force_reload: bool = False) -> Tuple[Optional[pd.DataFrame], Optional[Dict]]:
249
+ """
250
+ 从CSV文件加载向量数据库(带缓存机制)
251
+
252
+ Args:
253
+ force_reload: 是否强制重新加载
254
+
255
+ Returns:
256
+ DataFrame和元数据字典的元组
257
+ """
258
+ # 检查是否需要重新加载
259
+ if not force_reload and self._cached_df is not None and self._cached_metadata is not None:
260
+ return self._cached_df, self._cached_metadata
261
+
262
+ try:
263
+ # 加载CSV文件
264
+ df = pd.read_csv(self.vector_db_path, encoding='utf-8')
265
+
266
+ # 加载元数据
267
+ with open(self.metadata_path, 'r', encoding='utf-8') as f:
268
+ metadata = json.load(f)
269
+
270
+ # 缓存结果
271
+ self._cached_df = df
272
+ self._cached_metadata = metadata
273
+ self._last_load_time = datetime.now()
274
+
275
+ # 预加载向量矩阵到缓存
276
+ self._preload_embeddings()
277
+
278
+ print(f"成功加载向量数据库,包含 {len(df)} 个条目")
279
+ return df, metadata
280
+ except FileNotFoundError as e:
281
+ print(f"错误:找不到文件 - {e}")
282
+ return None, None
283
+ except Exception as e:
284
+ print(f"加载向量数据库时出错: {e}")
285
+ return None, None
286
+
287
+ def _preload_embeddings(self):
288
+ """预加载所有类型的向量矩阵到缓存"""
289
+ if self._cached_df is None:
290
+ return
291
+
292
+ vector_types = ['title', 'content', 'full']
293
+ for vector_type in vector_types:
294
+ if vector_type not in self._cached_embeddings:
295
+ embeddings = self.get_embeddings_from_df(self._cached_df, vector_type)
296
+ # 预计算归一化向量
297
+ embeddings_norm = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
298
+ self._cached_embeddings[vector_type] = {
299
+ 'raw': embeddings,
300
+ 'normalized': embeddings_norm
301
+ }
302
+
303
+ print(f"预加载了 {len(vector_types)} 种类型的向量矩阵")
304
+
305
+ def get_embeddings_from_df(self, df: pd.DataFrame, vector_type: str = 'full') -> np.ndarray:
306
+ """
307
+ 从DataFrame中提取向量矩阵
308
+
309
+ Args:
310
+ df: 包含向量的DataFrame
311
+ vector_type: 向量类型 ('title', 'content', 'full')
312
+
313
+ Returns:
314
+ 向量矩阵
315
+ """
316
+ # 根据类型获取对应的列
317
+ if vector_type == 'title':
318
+ embedding_cols = [col for col in df.columns if col.startswith('title_dim_')]
319
+ elif vector_type == 'content':
320
+ embedding_cols = [col for col in df.columns if col.startswith('content_dim_')]
321
+ else: # 'full'
322
+ embedding_cols = [col for col in df.columns if col.startswith('full_dim_')]
323
+
324
+ embeddings = df[embedding_cols].values
325
+ return embeddings
326
+
327
+ def batch_search_similar(self, queries: List[str], top_k: int = 5,
328
+ title_weight: float = 0.4,
329
+ content_weight: float = 0.3,
330
+ full_weight: float = 0.3) -> List[List[Tuple[Dict, float, Dict]]]:
331
+ """
332
+ 批量搜索多个查询,只加载一次向量数据库
333
+
334
+ Args:
335
+ queries: 查询文本列表
336
+ top_k: 每个查询返回最相似的前k个结果
337
+ title_weight: 标题相似度的权重
338
+ content_weight: 内容相似度的权重
339
+ full_weight: 完整文本相似度的权重
340
+
341
+ Returns:
342
+ 每个查询对应的相似条目列表
343
+ """
344
+ # 确保权重之和为1
345
+ total_weight = title_weight + content_weight + full_weight
346
+ title_weight /= total_weight
347
+ content_weight /= total_weight
348
+ full_weight /= total_weight
349
+
350
+ # 加载向量数据库(只加载一次)
351
+ df, metadata = self.load_vector_database()
352
+ if df is None:
353
+ return [[] for _ in queries]
354
+
355
+ # 批量获取查询向量
356
+ print(f"批量生成 {len(queries)} 个查询的向量...")
357
+ query_embeddings = self.batch_get_embeddings(queries, batch_size=min(10, len(queries)))
358
+
359
+ if len(query_embeddings) != len(queries):
360
+ print("查询向量生成失败")
361
+ return [[] for _ in queries]
362
+
363
+ # 获取缓存的归一化向量矩阵
364
+ title_embeddings_norm = self._cached_embeddings['title']['normalized']
365
+ content_embeddings_norm = self._cached_embeddings['content']['normalized']
366
+ full_embeddings_norm = self._cached_embeddings['full']['normalized']
367
+
368
+ all_results = []
369
+
370
+ # 对每个查询进行相似度计算
371
+ for i, (query, query_embedding) in enumerate(zip(queries, query_embeddings)):
372
+ if not query_embedding:
373
+ all_results.append([])
374
+ continue
375
+
376
+ query_vec = np.array(query_embedding)
377
+ query_vec_norm = query_vec / np.linalg.norm(query_vec)
378
+
379
+ # 计算各部分的相似度
380
+ title_similarities = np.dot(title_embeddings_norm, query_vec_norm)
381
+ content_similarities = np.dot(content_embeddings_norm, query_vec_norm)
382
+ full_similarities = np.dot(full_embeddings_norm, query_vec_norm)
383
+
384
+ # 加权综合相似度
385
+ combined_similarities = (
386
+ title_weight * title_similarities +
387
+ content_weight * content_similarities +
388
+ full_weight * full_similarities
389
+ )
390
+
391
+ # 获取top-k
392
+ top_indices = np.argsort(combined_similarities)[::-1][:top_k]
393
+
394
+ query_results = []
395
+ for idx in top_indices:
396
+ # 从DataFrame中获取条目信息
397
+ row = df.iloc[idx]
398
+ entry = {
399
+ 'id': row['id'],
400
+ 'title': row['title'],
401
+ 'source': row['source'],
402
+ 'content': row['content']
403
+ }
404
+
405
+ # 添加各部分的相似度详情
406
+ similarity_details = {
407
+ 'combined': float(combined_similarities[idx]),
408
+ 'title': float(title_similarities[idx]),
409
+ 'content': float(content_similarities[idx]),
410
+ 'full': float(full_similarities[idx])
411
+ }
412
+
413
+ query_results.append((entry, float(combined_similarities[idx]), similarity_details))
414
+
415
+ all_results.append(query_results)
416
+ print(f"完成查询 {i+1}/{len(queries)}: '{query[:50]}...'")
417
+
418
+ return all_results
419
+
420
+ def search_similar(self, query: str, top_k: int = 5,
421
+ title_weight: float = 0.4,
422
+ content_weight: float = 0.3,
423
+ full_weight: float = 0.3) -> List[Tuple[Dict, float, Dict]]:
424
+ """
425
+ 搜索与查询最相似的条目,综合考虑标题和内容的相似度
426
+ 使用批量搜索的优化版本
427
+
428
+ Args:
429
+ query: 查询文本
430
+ top_k: 返回最相似的前k个结果
431
+ title_weight: 标题相似度的权重
432
+ content_weight: 内容相似度的权重
433
+ full_weight: 完整文本相似度的权重
434
+
435
+ Returns:
436
+ 相似条目和相似度分数的列表
437
+ """
438
+ # 使用批量搜索处理单个查询
439
+ results = self.batch_search_similar([query], top_k, title_weight, content_weight, full_weight)
440
+ return results[0] if results else []
441
+
442
+ def search_with_entities_optimized(self, entities: List[str], top_k: int = 3) -> List[Tuple[Dict, float, Dict]]:
443
+ """
444
+ 优化版本:使用实体列表搜索知识库,只加载一次向量数据库
445
+
446
+ Args:
447
+ entities: 实体列表
448
+ top_k: 每个实体返回的结果数
449
+
450
+ Returns:
451
+ 合并和去重后的搜索结果
452
+ """
453
+ if not entities:
454
+ return []
455
+
456
+ # 使用批量搜索
457
+ batch_results = self.batch_search_similar(
458
+ entities,
459
+ top_k=top_k,
460
+ title_weight=0.5, # 对于实体搜索,标题权重更高
461
+ content_weight=0.3,
462
+ full_weight=0.2
463
+ )
464
+
465
+ # 合并结果并去重
466
+ seen_ids = set()
467
+ all_results = []
468
+
469
+ for entity_results in batch_results:
470
+ for entry, score, details in entity_results:
471
+ entry_id = entry['id']
472
+ if entry_id not in seen_ids:
473
+ seen_ids.add(entry_id)
474
+ all_results.append((entry, score, details))
475
+
476
+ # 按分数排序
477
+ sorted_results = sorted(all_results, key=lambda x: x[1], reverse=True)
478
+ return sorted_results
479
+
480
+ def add_new_entry(self, id: str, title: str, source: str, content: str):
481
+ """
482
+ 添加新条目到向量数据库
483
+
484
+ Args:
485
+ id: 条目ID
486
+ title: 标题
487
+ source: 来源
488
+ content: 内容
489
+ """
490
+ # 加载现有数据库
491
+ df, metadata = self.load_vector_database()
492
+
493
+ if df is None:
494
+ print("向量数据库不存在,将创建新的数据库")
495
+ df = pd.DataFrame()
496
+
497
+ # 创建新条目
498
+ full_text = f"{title} {content}"
499
+
500
+ # 获取三种类型的向量
501
+ print(f"正在为新条目 {id} 生成向量...")
502
+ title_embedding = self.get_embedding(title)
503
+ content_embedding = self.get_embedding(content)
504
+ full_embedding = self.get_embedding(full_text)
505
+
506
+ if not all([title_embedding, content_embedding, full_embedding]):
507
+ print("无法生成向量")
508
+ return
509
+
510
+ # 创建新条目
511
+ new_entry = {
512
+ 'index': len(df),
513
+ 'id': id,
514
+ 'title': title,
515
+ 'source': source,
516
+ 'content': content,
517
+ 'full_text': full_text
518
+ }
519
+
520
+ # 添加向量维度
521
+ for j, val in enumerate(title_embedding):
522
+ new_entry[f'title_dim_{j}'] = val
523
+ for j, val in enumerate(content_embedding):
524
+ new_entry[f'content_dim_{j}'] = val
525
+ for j, val in enumerate(full_embedding):
526
+ new_entry[f'full_dim_{j}'] = val
527
+
528
+ # 添加到DataFrame
529
+ new_df = pd.DataFrame([new_entry])
530
+ df = pd.concat([df, new_df], ignore_index=True)
531
+
532
+ # 保存更新后的数据库
533
+ df.to_csv(self.vector_db_path, index=False, encoding='utf-8')
534
+
535
+ # 更新元数据
536
+ if metadata:
537
+ metadata['num_entries'] = len(df)
538
+ metadata['updated_at'] = datetime.now().isoformat()
539
+ with open(self.metadata_path, 'w', encoding='utf-8') as f:
540
+ json.dump(metadata, f, ensure_ascii=False, indent=2)
541
+
542
+ # 清除缓存以便重新加载
543
+ self.clear_cache()
544
+
545
+ print(f"成功添加新条目 {id}")
546
+
547
+ def export_to_readable_format(self, output_path: str = None):
548
+ """
549
+ 导出向量数据库到更易读的格式(不包含向量维度)
550
+
551
+ Args:
552
+ output_path: 输出文件路径
553
+ """
554
+ df, _ = self.load_vector_database()
555
+ if df is None:
556
+ return
557
+
558
+ if output_path is None:
559
+ output_path = os.path.join(
560
+ os.path.dirname(self.data_path),
561
+ "vector_database_readable.csv"
562
+ )
563
+
564
+ # 只保留非向量列
565
+ non_vector_cols = [col for col in df.columns if not any(col.startswith(prefix) for prefix in ['title_dim_', 'content_dim_', 'full_dim_'])]
566
+ readable_df = df[non_vector_cols]
567
+
568
+ # 保存
569
+ readable_df.to_csv(output_path, index=False, encoding='utf-8')
570
+ print(f"可读格式的数据库已保存到: {output_path}")
571
+
572
+ def get_cache_info(self) -> Dict:
573
+ """
574
+ 获取缓存状态信息
575
+
576
+ Returns:
577
+ 缓存状态字典
578
+ """
579
+ return {
580
+ 'is_cached': self._cached_df is not None,
581
+ 'cache_size': len(self._cached_df) if self._cached_df is not None else 0,
582
+ 'cached_embeddings': list(self._cached_embeddings.keys()),
583
+ 'last_load_time': self._last_load_time.isoformat() if self._last_load_time else None
584
+ }