knowledge_sync.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  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. """删除指定 source 的所有旧块。
  27. 修复: 原实现 vectorstore.get() 会拉取整个 collection (数百万条 embedding)
  28. 到内存, 导致 gunicorn worker OOM 反复重启。改为按 metadata where 过滤
  29. 只取目标 source 的 ids 再删除, 内存占用与数据总量无关。
  30. """
  31. try:
  32. collection = vectorstore._collection
  33. existing = collection.get(where={"source": prefix}, include=[])
  34. ids = existing.get("ids", []) or []
  35. if ids:
  36. collection.delete(ids=ids)
  37. logger.info("已删除 source=%s 的 %d 个旧块", prefix, len(ids))
  38. else:
  39. logger.info("无 source=%s 旧块可删除", prefix)
  40. except Exception as e:
  41. logger.warning("删除 source=%s 旧块失败: %s", prefix, e)
  42. def _build_chunks(splitter, items: list[dict], source: str) -> list[Document]:
  43. chunks = []
  44. for item in items:
  45. item_id = item.get("id")
  46. title = item.get("title", "")
  47. content = item.get("content", "")
  48. if item_id is None or not content:
  49. continue
  50. full_text = f"{title}\n\n{content}"
  51. split_texts = splitter.split_text(full_text)
  52. for i, text in enumerate(split_texts):
  53. metadata = {
  54. "id": f"{source}_{item_id}_{i}",
  55. "source": source,
  56. "post_id": str(item_id),
  57. "title": title,
  58. "category": item.get("category", ""),
  59. "tags": item.get("tags", ""),
  60. "source_url": item.get("sourceUrl", ""),
  61. "pub_date": item.get("pubDate", ""),
  62. "chunk_index": i,
  63. "chunk_total": len(split_texts),
  64. }
  65. chunks.append(Document(page_content=text, metadata=metadata))
  66. return chunks
  67. async def sync_knowledge_base():
  68. """同步三个数据源: articles 增量, microbiome/dan_knowledge 全量重建到 cfc_knowledge"""
  69. loader = KnowledgeLoader()
  70. splitter = get_knowledge_splitter()
  71. embeddings = get_embeddings()
  72. vectorstore = Chroma(
  73. collection_name="cfc_knowledge",
  74. embedding_function=embeddings,
  75. persist_directory=settings.chroma_db_path,
  76. )
  77. state = _load_sync_state()
  78. BATCH_SIZE = 100
  79. stats = {}
  80. articles = await loader.load_updated_since(state["last_sync"])
  81. article_chunks = []
  82. if articles:
  83. docs = loader.format_for_indexing(articles)
  84. for doc in docs:
  85. if not doc.get("content"):
  86. continue
  87. split_texts = splitter.split_text(doc["content"])
  88. for i, text in enumerate(split_texts):
  89. metadata = dict(doc["metadata"])
  90. metadata["chunk_index"] = i
  91. article_chunks.append(Document(page_content=text, metadata=metadata))
  92. stats["article"] = len(article_chunks)
  93. logger.info("文章增量: %d 篇 -> %d 块", len(articles), len(article_chunks))
  94. microbiome = await loader.java.get_microbiome_articles()
  95. _delete_by_prefix(vectorstore, "microbiome")
  96. microbiome_chunks = _build_chunks(splitter, microbiome, "microbiome")
  97. stats["microbiome"] = len(microbiome_chunks)
  98. logger.info("菌群知识库: %d 篇 -> %d 块", len(microbiome), len(microbiome_chunks))
  99. dan_kb = await loader.java.get_dan_knowledge_base()
  100. _delete_by_prefix(vectorstore, "dan_knowledge")
  101. dan_chunks = _build_chunks(splitter, dan_kb, "dan_knowledge")
  102. stats["dan_knowledge"] = len(dan_chunks)
  103. logger.info("知识库(dan): %d 篇 -> %d 块", len(dan_kb), len(dan_chunks))
  104. all_chunks = article_chunks + microbiome_chunks + dan_chunks
  105. if not all_chunks:
  106. logger.info("知识库同步: 无新增内容")
  107. return
  108. for i in range(0, len(all_chunks), BATCH_SIZE):
  109. batch = all_chunks[i:i + BATCH_SIZE]
  110. await vectorstore.aadd_documents(batch)
  111. logger.debug("知识库同步进度: %d/%d", i + len(batch), len(all_chunks))
  112. import datetime
  113. state["last_sync"] = datetime.datetime.now().isoformat()
  114. _save_sync_state(state)
  115. logger.info("知识库同步完成: 各源块数 %s", stats)