版本: v1.0
日期: 2026-07-17
范围: data-pipeline/ 全部解析模块 + backend-python/app/rag/ 检索链路
主线版本: 2025 年版药典(主要解析目标)
补充版本: 2020 年版药典(历史数据兼容)
药典 PDF (2020/2025) -> pharmacopoeia.py -> ingest.py 药典 DOCX (2025) -> docx_ingest.py -> docx_ingest.py (内置入库) 维基百科 -> wiki_spider.py -> import_from_openclaw.py NMPA 公开数据 -> nmpa_spider.py -> ingest.py 考试大纲 -> exam_outline.py -> knowledge_chunker.py
当前状态: 仅 DOCX 解析链路可用,PDF 解析器为空壳,考试模块未实现。
文件位置: data-pipeline/docx_ingest.py
当前状态: 可用,但存在多处健壮性问题
DOCX 文件 | 文件名 -> 药品名(display_name) | 段落遍历(doc.paragraphs) | Heading 1 -> 跳过(与文件名重复) 拼音行 -> 提取 pinyin 短行(<=10字符)-> section 标题匹配 【xxx】格式 -> 行内 section 标题 其他 -> 归入当前 section | 凡例/纲要 -> 按编号拆分(一、二、三...) | 输出结构化 dict
| 编号 | 问题 | 严重程度 | 说明 |
|---|---|---|---|
| D-01 | 表格内容完全丢失 | 严重 | 只遍历 doc.paragraphs,未处理 doc.tables。药典中含量测定限度表、鉴别反应表等关键信息以表格形式存在 |
| D-02 | section 标题长度硬编码 | 中等 | len(text) <= 10 阈值过小,较长标题会被漏掉 |
| D-03 | 拼音识别可能误判 | 中等 | is_pinyin_line() 可能将英文药品名或化学分子式误判为拼音 |
| D-04 | 凡例/纲要拆分逻辑脆弱 | 中等 | 中文数字正则 {1,2} 无法匹配二十一等复合数字;模式优先级可能导致错误匹配 |
| D-05 | 文件名依赖 | 轻微 | 药品名直接取文件名,不规范的文件名(含空格、编号前缀)会导致解析错误 |
| D-06 | 旧版去重逻辑风险 | 中等 | dedup_old_versions() 将所有非 2025 版同名药品标记为 inactive,可能误删 2020 版独有的药品信息 |
SECTION_HEADERS = OrderedDict([
(处方, 处方), (制法, 制法), (性状, 性状),
(鉴别, 鉴别), (检查, 检查), (浸出物, 浸出物),
(含量测定, 含量测定), (含量, 含量测定),
(功能与主治, 功能主治), (功能, 功能主治), (主治, 功能主治),
(用法与用量, 用法用量), (用法, 用法用量), (用量, 用法用量),
(注意, 注意事项), (注意事项, 注意事项),
(规格, 规格), (贮藏, 贮藏), (类别, 类别),
(制剂, 制剂), (附注, 附注),
])
缺失的映射:
根据 extract_catalog.py 提取的目录:
中国药典 2025 年版 四部 |-- 凡例 |-- 通则 | |-- 制剂通则 | |-- 通用检测方法 | +-- 指导原则 |-- 药用辅料 +-- 通用技术要求
文件位置: data-pipeline/crawlers/pharmacopoeia.py
当前状态: 完全未实现
| 方法 | 预期功能 | 当前返回 |
|---|---|---|
| parse() | 完整解析流程 | [] |
| _extract_text_with_position() | 提取文本块及坐标 | [] |
| _detect_entry_boundaries() | 检测条目边界 | [] |
| _parse_entry_sections() | 解析各栏目 | 空字典 |
| 挑战 | 说明 |
|---|---|
| 排版差异 | 2020 版 PDF 与 2025 版排版规则不同,字体大小、缩进量需要分别适配 |
| 双栏布局 | 部分页面采用双栏布局,文本提取时可能出现跨栏混排 |
| 化学结构式 | 含量测定等章节包含化学结构式图片,纯文本提取会丢失 |
| 页码不连续 | 前言、目录、凡例的页码与正文不连续,需要分别处理 |
文件位置: data-pipeline/crawlers/extract_catalog.py
当前状态: 部分可用
| 编号 | 问题 | 说明 |
|---|---|---|
| C-01 | 层级判断靠坐标硬编码 | x < 50 为部级、40 <= x < 100 为章级、x >= 80 为节级,不同 PDF 版本排版不同时会失效 |
| C-02 | 层级范围重叠 | 章级 40-100 与节级 >=80 重叠,x=90 同时满足两个条件 |
| C-03 | 目录页定位不稳定 | 依赖目录关键词匹配,如果目录页格式不同会定位错误 |
| 来源 | 格式 | 状态 |
|---|---|---|
| data/pharmacopoeia_2020_volume1_toc.pdf | 无法解析(解析器未实现) | |
| data-pipeline/data/catalog_volume1.json | JSON | 已提取目录结构 |
| data-pipeline/data/catalog_volume2.json | JSON | 已提取目录结构 |
| data-pipeline/data/catalog_volume3.json | JSON | 已提取目录结构 |
| data-pipeline/data/catalog_volume4.json | JSON | 已提取目录结构 |
| 差异项 | 2020 版 | 2025 版 |
|---|---|---|
| 主要数据格式 | DOCX | |
| 药品品种数量 | 约 5911 种 | 新增 + 修订 |
| 凡例内容 | 2020 版凡例 | 2025 版凡例(有更新) |
| 通则编号 | 旧编号体系 | 可能有调整 |
| 检测方法 | 2020 版方法 | 新增/修订方法 |
当前 docx_ingest.py 中的去重逻辑:
UPDATE drugs SET is_active = FALSE WHERE is_active = TRUE AND source_version != 2025年版 AND name IN (
SELECT name FROM drugs
WHERE source_version = 2025年版 AND is_active = TRUE
)
问题:
文件位置: data-pipeline/processors/cleaner.py
当前状态: 功能薄弱
self.replacements = [
(r(?<!\n)\n(?!\n), ), # 规则1: 移除单换行
(r\x00, ), # 规则2: 移除空字符
(r([a-z])\n([a-z]), r\1\2), # 规则3: 英文断行连接
(r(\d)\n(\d), r\1\2), # 规则4: 数字断行连接
(r\n{3,}, \n\n), # 规则5: 多换行压缩
(r\t, ), # 规则6: 制表符替换
(r {2,}, ), # 规则7: 多空格压缩
]
冲突分析:
ocr_fixes = {
鍜?: 鍟?,
鍞?: 鍞?,
鑼剁⒈: 鑼剁⒈,
}
问题:
| 缺失项 | 说明 |
|---|---|
| 全角/半角规范化 | 中文逗号 vs 英文逗号、中文句号 vs 英文句号、中文括号 vs 英文括号 |
| 化学分子式处理 | 上下标丢失问题(如 H2O 应为 H 下标 2 O) |
| 中药拉丁名规范化 | 拉丁学名斜体标记丢失 |
| 特殊符号处理 | 摄氏度、微克、毫升等单位符号的规范化 |
| 段落合并逻辑 | 跨页段落的智能合并 |
文件位置: data-pipeline/processors/chunker.py
当前状态: 部分实现
def chunk_drug_entry(self, drug_entry: dict) -> list[Chunk]:
# 按 section 切分,保持每个 section 完整
# 若单个 section 过长,再按段落细分
问题:
def chunk_regulation(self, text: str, metadata: dict) -> list[Chunk]:
return [] # 空实现
def chunk_exam_knowledge(self, knowledge_point: dict) -> list[Chunk]:
return [] # 空实现
content=f銆恵drug_name} - {section_key}銆慭n{section_text}
文件位置: data-pipeline/processors/knowledge_chunker.py
当前状态: 完全未实现
class KnowledgeChunker:
def process(self, knowledge_point: dict) -> list[dict]:
return []
def build_chapter_tree(self, knowledge_points: list[dict]) -> dict:
return {}
影响: 考试模块的数据管道完全断裂,无法支持:
文件位置: backend-python/app/rag/retriever.py
当前状态: 部分实现
def classify_intent(query: str) -> str:
# 基于关键词匹配的简单分类
| 编号 | 问题 | 说明 |
|---|---|---|
| I-01 | 关键词冲突 | 过敏同时出现在 safety_sections 和 symptom_keywords 中,优先级判断导致错误分类 |
| I-02 | 无否定语义处理 | 这不是感冒会匹配到感冒关键词 |
| I-03 | 无置信度判断 | 所有查询都被强制分到某个类别,没有不确定的兜底 |
| I-04 | 无上下文理解 | 无法理解多轮对话中的指代关系 |
注释写着混合检索器:pgvector 向量检索 + BM25 关键词检索,但实际只有向量检索。
影响:
engine = create_async_engine(DB_URL) try:
# ...
finally:
await engine.dispose()
每次查询都创建新的 engine 并销毁,应该使用连接池复用。
文件位置: backend-python/app/rag/reranker.py
当前状态: 形同虚设
class Reranker:
def _load_model(self):
pass # 空实现
def rerank(self, query, documents, top_k=5):
sorted_docs = sorted(documents, key=lambda d: d.get(score, 0), reverse=True)
return sorted_docs[:top_k] # 只是按原始分数排序
设计目标: BGE-Reranker-v2-m3 Cross-Encoder 精排
实际效果: 仅按向量相似度分数排序取 Top-K,无精排能力
文件位置: data-pipeline/processors/embedder.py
当前状态: 本地模型未实现
def encode(self, texts: list[str], batch_size: int = 32) -> list[list[float]]:
return [[0.0] * settings.embedding_dim] * len(texts) # 返回全零向量
实际情况: 入库时走 DashScope API(ingest.py 中),本地 BGE-M3 模型未加载。
文件位置: data-pipeline/crawlers/wiki_spider.py
当前状态: 部分可用
sections[matched] = .join(current_text)[:3000]
每个 section 最多 3000 字符,超出部分直接截断,可能丢失关键的不良反应、禁忌等信息。
def infer_category(name, sections):
if any(k in name for k in [草, 花, 叶, 根, 皮, 中, 散, 汤, 丸]):
return 中药
仅靠药名中是否含特定汉字来判断,误判率高(如花旗参匹配花)。
{ drug_id: string, name: string, name_en: string, name_pinyin: string, category: 化学药 | 中药 | 生物制品 | 辅料, subcategory: string, sections: {
性状: string,
鉴别: string,
检查: string,
含量测定: string,
类别: string,
贮藏: string,
制剂: string,
用法与用量: string,
禁忌: string,
不良反应: string,
注意事项: string
}, source: {
version: 2020年版 | 2025年版,
volume: 一部 | 二部 | 三部 | 四部,
page: string
} }
{ point_id: KP-{subject}-{chapter}-{seq}, subject: yao1 | yao2 | fagui | zonghe, chapter_id: string, title: string, content: string, difficulty: 1-5, frequency: 高频 | 中频 | 低频 | 未考, related_drugs: [drug_id], key_points: [string] }
{ question_id: Q{yyyy}{mm}{seq}, question_type: A | B | X, subject: yao1 | yao2 | fagui | zonghe, content: string, options: [A. xxx, B. xxx, C. xxx, D. xxx], answer: A | AB | ABC, explanation: string, knowledge_point_ids: [point_id], frequency: 高频 | 中频 | 低频 | 未考 }
| 编号 | 问题 | 模块 | 影响 |
|---|---|---|---|
| F-01 | PDF 解析器完全未实现 | pharmacopoeia.py | 2020 版药典 PDF 无法解析 |
| F-02 | 考试知识点切片为空 | knowledge_chunker.py | 考试模块数据管道断裂 |
| F-03 | Reranker 未加载模型 | reranker.py | 检索结果无精排能力 |
| F-04 | BM25 关键词检索未实现 | retriever.py | 精确查询效果差 |
| 编号 | 问题 | 模块 | 影响 |
|---|---|---|---|
| S-01 | DOCX 解析不处理表格 | docx_ingest.py | 含量测定限度表等丢失 |
| S-02 | 文本清洗规则冲突 | cleaner.py | 英文/数字断行连接失效 |
| S-03 | OCR 纠错字典不足 | cleaner.py | PDF 提取文本错误多 |
| S-04 | 切片无 overlap | chunker.py | 上下文断裂 |
| S-05 | 凡例/通则切片为空 | chunker.py | 通则类内容无法检索 |
| 编号 | 问题 | 模块 | 影响 |
|---|---|---|---|
| M-01 | 意图分类关键词冲突 | retriever.py | 用户意图误判 |
| M-02 | section 标题长度硬编码 | docx_ingest.py | 部分标题漏识别 |
| M-03 | 凡例/纲要拆分逻辑脆弱 | docx_ingest.py | 编号拆分错误 |
| M-04 | PDF 目录层级坐标硬编码 | extract_catalog.py | 不同 PDF 版本失效 |
| M-05 | 旧版去重可能误删 | docx_ingest.py | 2020 版独有信息丢失 |
| 编号 | 问题 | 模块 | 影响 |
|---|---|---|---|
| L-01 | Wiki 内容截断 | wiki_spider.py | 长内容不完整 |
| L-02 | Wiki 分类推断粗糙 | wiki_spider.py | 分类误判 |
| L-03 | 文件名依赖 | docx_ingest.py | 不规范文件名出错 |
| L-04 | 数据库连接不复用 | retriever.py | 性能浪费 |
实现 PDF 解析器
修复文本清洗规则顺序
self.replacements = [ (r([a-z])\n([a-z]), r\1\2), # 先处理断行 (r(\d)\n(\d), r\1\2), (r(?<!\n)\n(?!\n), ), # 再移除不当换行 ... ]
实现表格解析
实现 Reranker 模型加载
实现 BM25 混合检索
优化意图分类
实现考试知识点切片
多版本数据共存
增量更新机制
质量评估体系
| 版本 | 部数 | 主要内容 | 数据格式 |
|---|---|---|---|
| 2020 年版 | 四部 | 中药、化学药、生物制品、通则 | |
| 2025 年版 | 四部 | 新增 + 修订品种 | DOCX |
| 代码 | 科目名称 |
|---|---|
| yao1 | 药学专业知识(一) |
| yao2 | 药学专业知识(二) |
| fagui | 药事管理与法规 |
| zonghe | 药学综合知识与技能 |
| 题型 | 说明 |
|---|---|
| A 型题 | 单项选择题 |
| B 型题 | 配伍选择题 |
| X 型题 | 多项选择题 |
文档结束