knowledge_sync.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. """知识库同步定时任务: 定期从 Java 拉取文章, 更新 ChromaDB"""
  2. from app.rag.loader import KnowledgeLoader
  3. from app.rag.splitter import get_knowledge_splitter
  4. from app.rag.embeddings import get_embeddings
  5. from app.config import settings
  6. from langchain_chroma import Chroma
  7. from langchain_core.documents import Document
  8. import logging
  9. import os
  10. import json
  11. logger = logging.getLogger(__name__)
  12. SYNC_STATE_FILE = os.path.join(settings.chroma_db_path, ".sync_state")
  13. def _load_sync_state() -> dict:
  14. try:
  15. if os.path.exists(SYNC_STATE_FILE):
  16. with open(SYNC_STATE_FILE) as f:
  17. return json.load(f)
  18. except Exception:
  19. pass
  20. return {"last_sync": "2000-01-01T00:00:00"}
  21. def _save_sync_state(state: dict):
  22. os.makedirs(os.path.dirname(SYNC_STATE_FILE), exist_ok=True)
  23. with open(SYNC_STATE_FILE, "w") as f:
  24. json.dump(state, f)
  25. def _delete_by_prefix(vectorstore, prefix: str):
  26. try:
  27. existing = vectorstore.get()
  28. ids = [i for i in existing.get("ids", []) if i.startswith(prefix)]
  29. if ids:
  30. vectorstore.delete(ids)
  31. logger.info("已删除前缀 %s 的 %d 个旧块", prefix, len(ids))
  32. except Exception as e:
  33. logger.warning("删除前缀 %s 旧块失败: %s", prefix, e)
  34. def _build_chunks(splitter, items: list[dict], source: str) -> list[Document]:
  35. chunks = []
  36. for item in items:
  37. item_id = item.get("id")
  38. title = item.get("title", "")
  39. content = item.get("content", "")
  40. if item_id is None or not content:
  41. continue
  42. full_text = f"{title}\n\n{content}"
  43. split_texts = splitter.split_text(full_text)
  44. for i, text in enumerate(split_texts):
  45. metadata = {
  46. "id": f"{source}_{item_id}_{i}",
  47. "source": source,
  48. "post_id": str(item_id),
  49. "title": title,
  50. "category": item.get("category", ""),
  51. "tags": item.get("tags", ""),
  52. "source_url": item.get("sourceUrl", ""),
  53. "pub_date": item.get("pubDate", ""),
  54. "chunk_index": i,
  55. "chunk_total": len(split_texts),
  56. }
  57. chunks.append(Document(page_content=text, metadata=metadata))
  58. return chunks
  59. async def sync_knowledge_base():
  60. """同步三个数据源: articles 增量, microbiome/dan_knowledge 全量重建到 cfc_knowledge"""
  61. loader = KnowledgeLoader()
  62. splitter = get_knowledge_splitter()
  63. embeddings = get_embeddings()
  64. vectorstore = Chroma(
  65. collection_name="cfc_knowledge",
  66. embedding_function=embeddings,
  67. persist_directory=settings.chroma_db_path,
  68. )
  69. state = _load_sync_state()
  70. BATCH_SIZE = 100
  71. stats = {}
  72. articles = await loader.load_updated_since(state["last_sync"])
  73. article_chunks = []
  74. if articles:
  75. docs = loader.format_for_indexing(articles)
  76. for doc in docs:
  77. if not doc.get("content"):
  78. continue
  79. split_texts = splitter.split_text(doc["content"])
  80. for i, text in enumerate(split_texts):
  81. metadata = dict(doc["metadata"])
  82. metadata["chunk_index"] = i
  83. article_chunks.append(Document(page_content=text, metadata=metadata))
  84. stats["article"] = len(article_chunks)
  85. logger.info("文章增量: %d 篇 -> %d 块", len(articles), len(article_chunks))
  86. microbiome = await loader.java.get_microbiome_articles()
  87. _delete_by_prefix(vectorstore, "microbiome_")
  88. microbiome_chunks = _build_chunks(splitter, microbiome, "microbiome")
  89. stats["microbiome"] = len(microbiome_chunks)
  90. logger.info("菌群知识库: %d 篇 -> %d 块", len(microbiome), len(microbiome_chunks))
  91. dan_kb = await loader.java.get_dan_knowledge_base()
  92. _delete_by_prefix(vectorstore, "dan_knowledge_")
  93. dan_chunks = _build_chunks(splitter, dan_kb, "dan_knowledge")
  94. stats["dan_knowledge"] = len(dan_chunks)
  95. logger.info("知识库(dan): %d 篇 -> %d 块", len(dan_kb), len(dan_chunks))
  96. all_chunks = article_chunks + microbiome_chunks + dan_chunks
  97. if not all_chunks:
  98. logger.info("知识库同步: 无新增内容")
  99. return
  100. for i in range(0, len(all_chunks), BATCH_SIZE):
  101. batch = all_chunks[i:i + BATCH_SIZE]
  102. await vectorstore.aadd_documents(batch)
  103. vectorstore.persist()
  104. logger.debug("知识库同步进度: %d/%d", i + len(batch), len(all_chunks))
  105. import datetime
  106. state["last_sync"] = datetime.datetime.now().isoformat()
  107. _save_sync_state(state)
  108. logger.info("知识库同步完成: 各源块数 %s", stats)