chunker.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. """
  2. 智能切片器:按药品条目为单位切片
  3. 确保每个 chunk 是完整的药品信息,不截断关键内容
  4. """
  5. import logging
  6. from typing import Optional
  7. from dataclasses import dataclass, field
  8. logger = logging.getLogger(__name__)
  9. @dataclass
  10. class Chunk:
  11. content: str
  12. metadata: dict = field(default_factory=dict)
  13. class DrugChunker:
  14. def __init__(
  15. self,
  16. chunk_size: int = 1500,
  17. chunk_overlap: int = 200,
  18. min_chunk_size: int = 200,
  19. ):
  20. self.chunk_size = chunk_size
  21. self.chunk_overlap = chunk_overlap
  22. self.min_chunk_size = min_chunk_size
  23. def chunk_drug_entry(self, drug_entry: dict) -> list[Chunk]:
  24. """
  25. 将结构化药品条目切成 chunk。
  26. 策略:按 section 切分,保持每个 section 完整。
  27. 若单个 section 过长,再按段落细分。
  28. """
  29. chunks = []
  30. drug_name = drug_entry.get("name", "")
  31. source = drug_entry.get("source", {})
  32. for section_key, section_text in drug_entry.get("sections", {}).items():
  33. if not section_text or len(section_text.strip()) < 10:
  34. continue
  35. if len(section_text) <= self.chunk_size:
  36. chunks.append(Chunk(
  37. content=f"【{drug_name} - {section_key}】\n{section_text}",
  38. metadata={
  39. "drug_name": drug_name,
  40. "section": section_key,
  41. "source": source.get("version", "") + " "
  42. + source.get("volume", "") + " "
  43. + source.get("page", ""),
  44. "category": drug_entry.get("category", ""),
  45. }
  46. ))
  47. else:
  48. sub_chunks = self._split_long_section(
  49. section_text, drug_name, section_key, source
  50. )
  51. chunks.extend(sub_chunks)
  52. return chunks
  53. def _split_long_section(
  54. self, text: str, drug_name: str, section: str, source: dict
  55. ) -> list[Chunk]:
  56. """对过长 section 按段落切分,相邻 chunk 之间保留 overlap 以避免上下文断裂"""
  57. paragraphs = text.split("\n")
  58. chunks = []
  59. buffer = ""
  60. for para in paragraphs:
  61. if len(buffer) + len(para) > self.chunk_size and len(buffer) >= self.min_chunk_size:
  62. chunks.append(Chunk(
  63. content=f"【{drug_name} - {section}】\n{buffer}",
  64. metadata={
  65. "drug_name": drug_name,
  66. "section": section,
  67. "source": f"{source.get('version', '')} {source.get('volume', '')} {source.get('page', '')}",
  68. "category": "",
  69. }
  70. ))
  71. # overlap: 从上一个 buffer 尾部保留 chunk_overlap 字符作为下一段的上下文
  72. if self.chunk_overlap > 0 and len(buffer) > self.chunk_overlap:
  73. buffer = buffer[-self.chunk_overlap:] + "\n" + para
  74. else:
  75. buffer = para
  76. else:
  77. buffer += ("\n" if buffer else "") + para
  78. if buffer:
  79. chunks.append(Chunk(
  80. content=f"【{drug_name} - {section}】\n{buffer}",
  81. metadata={
  82. "drug_name": drug_name,
  83. "section": section,
  84. "source": f"{source.get('version', '')} {source.get('volume', '')} {source.get('page', '')}",
  85. "category": "",
  86. }
  87. ))
  88. return chunks
  89. def chunk_regulation(self, text: str, metadata: dict) -> list[Chunk]:
  90. """凡例/通则按编号或段落切分,生成可供检索的 chunk。"""
  91. if not text or len(text.strip()) < 10:
  92. return []
  93. chunks = []
  94. source_info = (
  95. f"{metadata.get('source', '')} {metadata.get('version', '')} "
  96. f"{metadata.get('volume', '')}".strip()
  97. )
  98. reg_name = metadata.get("name", "通则")
  99. # 尝试按编号拆分(一、二、三... 或 1. 2. 3.)
  100. import re
  101. cn_nums = "一二三四五六七八九十百千"
  102. patterns = [
  103. re.compile(r'(?:^|\n)([' + cn_nums + r']{1,3})[、,,.]'),
  104. re.compile(r'(?:^|\n)(([' + cn_nums + r']{1,3}))'),
  105. re.compile(r'(?:^|\n)(\d{1,2})[\.\))]'),
  106. ]
  107. sections = None
  108. for pattern in patterns:
  109. matches = list(pattern.finditer(text))
  110. if len(matches) >= 2:
  111. sections = []
  112. for i, match in enumerate(matches):
  113. start = match.start()
  114. end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
  115. section_text = text[start:end].strip()
  116. if len(section_text) > 20:
  117. sections.append(section_text)
  118. break
  119. if sections:
  120. for sec_text in sections:
  121. if len(sec_text) <= self.chunk_size:
  122. chunks.append(Chunk(
  123. content=f"【{reg_name}】\n{sec_text}",
  124. metadata={
  125. "regulation": reg_name,
  126. "source": source_info,
  127. **{k: v for k, v in metadata.items() if k not in ("name", "source")},
  128. }
  129. ))
  130. else:
  131. # 长段落实用 _split_long_section 逻辑
  132. sub = self._split_long_section(sec_text, reg_name, "通则", {
  133. "version": metadata.get("version", ""),
  134. "volume": metadata.get("volume", ""),
  135. "page": metadata.get("page", ""),
  136. })
  137. chunks.extend(sub)
  138. else:
  139. # 无编号结构,按段落切分
  140. paras = [p.strip() for p in text.split("\n") if p.strip()]
  141. buffer = ""
  142. for para in paras:
  143. if len(buffer) + len(para) > self.chunk_size and len(buffer) >= self.min_chunk_size:
  144. chunks.append(Chunk(
  145. content=f"【{reg_name}】\n{buffer}",
  146. metadata={
  147. "regulation": reg_name,
  148. "source": source_info,
  149. **{k: v for k, v in metadata.items() if k not in ("name", "source")},
  150. }
  151. ))
  152. # overlap
  153. if self.chunk_overlap > 0 and len(buffer) > self.chunk_overlap:
  154. buffer = buffer[-self.chunk_overlap:] + "\n" + para
  155. else:
  156. buffer = para
  157. else:
  158. buffer += ("\n" if buffer else "") + para
  159. if buffer:
  160. chunks.append(Chunk(
  161. content=f"【{reg_name}】\n{buffer}",
  162. metadata={
  163. "regulation": reg_name,
  164. "source": source_info,
  165. **{k: v for k, v in metadata.items() if k not in ("name", "source")},
  166. }
  167. ))
  168. return chunks
  169. def chunk_exam_knowledge(self, knowledge_point: dict) -> list[Chunk]:
  170. """考试知识点按大纲章节切分,生成可向量化的 chunk。"""
  171. if not knowledge_point:
  172. return []
  173. title = knowledge_point.get("title", "")
  174. content = knowledge_point.get("content", "")
  175. subject = knowledge_point.get("subject", "")
  176. chapter_id = knowledge_point.get("chapter_id", "")
  177. difficulty = knowledge_point.get("difficulty", "")
  178. frequency = knowledge_point.get("frequency", "")
  179. key_points = knowledge_point.get("key_points", [])
  180. if not content or len(content.strip()) < 10:
  181. return []
  182. chunks = []
  183. header = f"【{subject or '考试'} - {chapter_id or '未知章节'} - {title or '知识点'}】"
  184. if len(content) <= self.chunk_size:
  185. chunks.append(Chunk(
  186. content=f"{header}\n{content}",
  187. metadata={
  188. "subject": subject,
  189. "chapter_id": chapter_id,
  190. "title": title,
  191. "difficulty": difficulty,
  192. "frequency": frequency,
  193. "key_points": ",".join(key_points) if key_points else "",
  194. "content_type": "exam_knowledge",
  195. }
  196. ))
  197. else:
  198. paras = content.split("\n")
  199. buffer = ""
  200. for para in paras:
  201. if len(buffer) + len(para) > self.chunk_size and len(buffer) >= self.min_chunk_size:
  202. chunks.append(Chunk(
  203. content=f"{header}\n{buffer}",
  204. metadata={
  205. "subject": subject,
  206. "chapter_id": chapter_id,
  207. "title": title,
  208. "difficulty": difficulty,
  209. "frequency": frequency,
  210. "content_type": "exam_knowledge",
  211. }
  212. ))
  213. if self.chunk_overlap > 0 and len(buffer) > self.chunk_overlap:
  214. buffer = buffer[-self.chunk_overlap:] + "\n" + para
  215. else:
  216. buffer = para
  217. else:
  218. buffer += ("\n" if buffer else "") + para
  219. if buffer:
  220. chunks.append(Chunk(
  221. content=f"{header}\n{buffer}",
  222. metadata={
  223. "subject": subject,
  224. "chapter_id": chapter_id,
  225. "title": title,
  226. "difficulty": difficulty,
  227. "frequency": frequency,
  228. "content_type": "exam_knowledge",
  229. }
  230. ))
  231. return chunks