Pharma RAG:企业知识库的架构革命
一、为什么制药行业的知识库问题比你想的严重一名医学写作(Medical Writer)在准备 CTD 5.3.5.1(临床研究报告摘要)时,需要交叉引用:3 份 Phase III CSR(临床研究报告),每份 800–2000 页协议书修正案 5 个版本统计分析报告(SAP)+ 列表表格(TLF)共 1200 张竞品公开文献 40+ 篇内部 SOP 和监管对应文件若干在没有 AI 的情况下,熟练的 MW 完成这个任务需要3–5 天。用传统企业搜索(Elasticsearch / SharePoint 全文索引)?省下来的顶多是找文件的时间,文档本身还是得一页页读。这不是效率问题,是认知负荷问题。而 RAG 真正解决的,正是这个。二、RAG 的本质:不是搜索引擎升级,是知识架构重建很多人把 RAG 理解为"更聪明的搜索",这个理解差了一个维度。传统搜索的逻辑是:匹配词 → 返回文档。用户还是要自己读文档,自己综合,自己判断。RAG 的逻辑是:理解意图 → 精准定位段落 → 合成答案 → 提供引用。用户得到的是结论,而不是文档列表。制药场景的特殊性在于三点:1. 文档密度极高,同义词体系复杂一个药物在不同文件里可能叫:通用名 / INN / 商品名 / 化合物编号 / CAS号 / 研发代号。传统关键词检索在这里基本失效——你搜 “gefitinib”,搜不到写着 “ZD1839” 的段落。2. 跨文档推理是刚需“这个化合物在 Phase II 的 ORR 是多少,Phase III 里有没有变化,原因是什么?”——这个问题的答案分散在两份 CSR 的不同章节里,没有跨文档推理能力的系统无法回答。3. 可追溯性是监管要求,不是产品特性FDA 的电子提交规范要求所有数据来源可追溯。AI 生成的任何结论,必须能定位到原始文档的具体段落。这不是可选项。三、全链路架构设计:从文档入库到答案生成经过两年在多个药企项目的迭代,我们沉淀出一套三层架构:离线摄入 Pipeline → 在线查询 Pipeline → 持续评估与监控。离线摄入的几个关键决策:解析层:PDF 不等于文本。制药 CSR 里充满了多列表格、脚注、图表标注。我们用 Unstructured.io + 自研后处理来处理结构,纯 PyPDF2 提出来的文本在 CSR 场景误差率高达 30%+。分块层:后面单独讲,这里是最容易被低估的环节。向量化层:通用 Embedding 模型在生物医药领域效果有明显 Gap。PubMedBERT 或经过领域微调的模型,在制药文档的检索召回率上比 text-embedding-ada-002 高出 15–20 个百分点。混合索引:只用向量检索会丢失精确匹配(药物编号、p 值、剂量数值)。向量 + BM25 的 RRF 融合是目前最稳健的组合。在线查询的关键设计:P99 延迟控制在 2s 以内,这是用户可接受的临界值。我们的拆解是:查询改写: 300ms(小模型或规则)向量检索: 200ms(ANN 近似搜索)Rerank: 400ms(Cross-Encoder,Top-K=20 → Top-K=5)LLM 生成: 1000ms(Streaming 输出,体感更快)四、最被低估的环节:Pharma 文档的分块策略90% 的 RAG 项目在这里踩坑,然后把问题归因到"模型不行"或"数据不好"。我们在制药场景的最终方案是父子层级分块(Parent-Child Chunking):核心思路:用小块做检索(256–512 tokens,精准),命中后返回父块(1024–2048 tokens,上下文完整)。这解决了 RAG 的核心张力:检索需要小粒度(精准),生成需要大粒度(上下文)。对于 CSR 这类高度结构化的文档,我们额外加了章节元数据:每个 Chunk 携带section_path(如 “5.3.5.1 §3.2 药效学 主要终点”),检索时可以按章节过滤,也可以在引用里精确标注来源位置。五、代码实战:核心模块完整实现5.1 文档摄入 Pipeline# pharma_ingestion.py — 制药文档摄入 Pipeline(已脱敏)# 依赖:unstructured, langchain, weaviate-client, sentence-transformersimporthashlibimportloggingfromdataclassesimportdataclass,fieldfrompathlibimportPathfromtypingimportAnyimportweaviatefromlangchain.text_splitterimportRecursiveCharacterTextSplitterfromsentence_transformersimportSentenceTransformerfromunstructured.partition.pdfimportpartition_pdf logger=logging.getLogger("pharma_ingestion")@dataclassclassPharmaDocMeta:"""制药文档元数据——驱动权限过滤和引用生成"""doc_id:strdoc_type:str# CSR | SCS | Protocol | IB | SOP | Labeltitle:strversion:streffective_date:strindication:list[str]# 适应症列表molecule:str# 分子/化合物study_phase:str# Phase I/II/III/IVclassification:str# CONFIDENTIAL | INTERNAL | PUBLICowner_dept:str@dataclassclassChunk:"""单个文本块,携带完整溯源信息"""chunk_id:strparent_id:str# 父块 ID(父子分块用)text:strsection_path:str# e.g. "§3.2 Primary Endpoint"page_range:tuple[int,int]meta:PharmaDocMeta embedding:list[float]=field(default_factory=list)classPharmaDocumentParser:""" 制药文档解析器 处理 CSR/SCS/Protocol 的特殊结构:多列表格、脚注、图表引用 """def__init__(self,strategy:str="hi_res"):# hi_res 模式:调用 OCR + 版面分析,适合扫描件和复杂表格# fast 模式:纯文本提取,适合已结构化的 DOCXself.strategy=strategydefparse(self,file_path:Path)-list[dict]:"""返回结构化元素列表(文本段落 / 表格 / 标题)"""elements=partition_pdf(filename=str(file_path),strategy=self.strategy,infer_table_structure=True,# 表格结构识别include_page_breaks=True,)structured=[]foreleminelements:elem_type=type(elem).__name__# Title / NarrativeText / Table etc.structured.append({"type":elem_type,"text":str(elem),"page":getattr(elem.metadata,"page_number",None),"is_table":elem_type=="Table",})returnstructuredclassParentChildChunker:""" 父子层级分块器 - 父块(2048 tok):存储,用于生成时的上下文 - 子块(256 tok):用于精准向量检索 制药场景强烈推荐此模式 """def__init__(self,parent_chunk_size:int=2048,child_chunk_size:int=256,child_overlap:int=32,):self.parent_splitter=RecursiveCharacterTextSplitter(chunk_size=parent_chunk_size,chunk_overlap=128,separators=["\n\n\n","\n\n","\n","。",". "],)self.child_splitter=RecursiveCharacterTextSplitter(chunk_size=child_chunk_size,chunk_overlap=child_overlap,separators=["\n\n","\n","。",". "," "],)defsplit(self,elements:list[dict],meta:PharmaDocMeta)-list[Chunk]:""" 输入解析后的元素列表,输出父子 Chunk 列表 表格元素:整体作为一个 Chunk 不拆分 """chunks:list[Chunk]=[]current_section="§ Unknown"text_buffer=[]page_start=1foreleminelements:# 更新章节路径ifelem["type"]=="Title":current_section=elem["text"].strip()continue# 表格:整体保留,不拆分ifelem["is_table"]:table_chunk=self._make_chunk(text=elem["text"],section_path=current_section,page_range=(elem.get("page",0),elem.get("page",0)),meta=meta,parent_id="",# 表格块本身就是原子单元)chunks.append(table_chunk)continuetext_buffer.append(elem["text"])# 处理剩余文本缓冲full_text="\n\n".join(text_buffer)parent_texts=self.parent_splitter.split_text(full_text)forp_textinparent_texts:parent_chunk=self._make_chunk(text=p_text,section_path=current_section,page_range=(page_start,page_start),meta=meta,parent_id="",)chunks.append(parent_chunk)# 为每个父块生成子块(用于检索)child_texts=self.child_splitter.split_text(p_text)forc_textinchild_texts:child_chunk=self._make_chunk(text=c_text,section_path=current_section,page_range=(page_start,page_start),meta=meta,parent_id=parent_chunk.chunk_id,)chunks.append(child_chunk)returnchunksdef_make_chunk(self,text:str,section_path:str,page_range:tuple,meta:PharmaDocMeta,parent_id:str,)-Chunk:chunk_id=hashlib.sha256(f"{meta.doc_id}:{text[:128]}".encode()).hexdigest()[:16]returnChunk(chunk_id=chunk_id,parent_id=parent_id,text=text,section_path=section_path,page_range=page_range,meta=meta,)classPharmaEmbedder:""" 制药领域向量化 强烈建议使用领域预训练模型,而非通用 OpenAI Embedding 实测:PubMedBERT-fine-tuned 在 CSR 检索上比 ada-002 高 18pp recall """def__init__(self,model_name:str="pritamdeka/PubMedBERT-mnli-snli-scinli"):self.model=SentenceTransformer(model_name)logger.info(f"Embedder loaded:{model_name}")defembed_batch(self,texts:list[str],batch_size:int=64)-list[list[float]]:"""批量向量化,自动分批处理"""all_embeddings=[]foriinrange(0,len(texts),batch_size):batch=texts[i:i+batch_size]embeddings=self.model.encode(batch,normalize_embeddings=True,# 余弦相似度需要归一化show_progress_bar=False,)all_em
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2508128.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!