项目实训第六篇博客 —— RAG知识库构建与实践
2026/6/8 3:21:53 网站建设 项目流程

项目实训第六篇博客 —— RAG知识库构建与实践

一、前言

在前几篇博客中,我们完成了患者端住院系统的核心功能开发,包括挂号、住院、支付等业务流程。然而,随着医疗信息化的发展,用户对智能问诊的需求日益增长。如何让系统"理解"医学知识并准确回答用户问题,成为了我们下一步的探索方向。

本篇博客将详细介绍RAG(Retrieval-Augmented Generation)医学知识库的完整构建过程。

图片位置1:在"前言"与"正文"之间插入一张RAG架构的总体流程图,展示用户提问→规则过滤→检索→Rerank→LLM生成的完整流程。


二、系统架构总览

我们的RAG知识库采用多层检索架构:

┌─────────────────────────────────────────────────────────────────┐ │ 用户输入 │ │ "糖尿病诊断标准" │ └─────────────────────────┬───────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 规则过滤器 │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ 非医学拦截 │ │ 医学得分 │ │ 科室分类识别 │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ └─────────────────────────┬───────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 混合检索层 │ │ ┌───────────────────┐ ┌───────────────────┐ │ │ │ FAISS 向量检索 │ │ BM25 关键词 │ │ │ │ (权重 0.6) │ │ (权重 0.4) │ │ │ └───────────────────┘ └───────────────────┘ │ │ ↓ │ │ 初步召回 Top-20 │ └─────────────────────────┬───────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ Rerank 重排 │ │ BGE-Reranker-v2-m3 二次排序 │ │ ↓ │ │ 最终 Top-5 结果 │ └─────────────────────────┬───────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ LLM 生成回答 │ └─────────────────────────────────────────────────────────────────┘

系统架构总览图


三、切块策略设计

3.1 切块配置文件

切块策略在chunk_config.yaml中配置:

# 切块配置chunking:# 子块配置(主检索块)child:max_chars:800# 最大中文字符min_chars:400# 最小中文字符overlap_chars:80# 重叠字符(10%)# 父块配置(上下文扩展)parent:max_chars:3000# 最大字符aggregation:"chapter"# 必须完整保留的元素preserve_completeness:-"table"# 表格-"list"# 有序/无序列表-"formula"# 公式块-"blockquote"# 引用块

3.2 核心切块逻辑

核心参数说明:

参数说明
max_chars800单个chunk最大字符数
min_chars400单个chunk最小字符数
overlap_chars80块与块之间的重叠字符
classDocumentChunker:"""文档切块器"""def__init__(self,config_path:str=None):# 从配置文件加载参数chunk_config=self.config.get('chunking',{})self.max_chars=self.child_config.get('max_chars',800)self.min_chars=self.child_config.get('min_chars',400)self.overlap_chars=self.child_config.get('overlap_chars',80)defcount_chars(self,text:str)->int:"""计算中文字符数"""returnlen(re.findall(r'[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef\u0020-\u007E]',text))defis_table(self,lines:List[str])->bool:"""判断是否为表格,表格完整保留不拆分"""table_lines=[lforlinlinesif'|'inlorre.match(r'\s{2,}',l)]returnlen(table_lines)>=2

3.3 切块策略特点

为什么要重叠?

重叠设计确保边界信息不丢失。例如:

  • Chunk1 结尾讲了一半的"糖尿病"
  • Chunk2 开头继续讲"糖尿病的诊断标准"
  • 重叠区域保留了这个连续性

四、模型调用实现

4.1 向量化模型配置

# 向量化配置embedding:model:"BAAI/bge-m3"dimension:1024batch_size:32device:"cpu"

4.2 向量化核心代码

classEmbeddingGenerator:"""向量生成器"""def__init__(self,config_path:str=None,device:str="cpu"):self.model_name=embed_config.get('model','BAAI/bge-m3')self.dimension=embed_config.get('dimension',1024)self.batch_size=embed_config.get('batch_size',32)self.device=device self.model=Nonedefload_model(self):"""加载向量化模型"""print(f"正在加载模型:{self.model_name}")print(f"设备:{self.device}")self.model=SentenceTransformer(self.model_name,device=self.device)print(f"模型加载成功! 维度:{self.dimension}")returnTruedefgenerate_embedding(self,texts:List[str])->np.ndarray:"""生成文本向量"""embeddings=self.model.encode(texts,batch_size=self.batch_size,show_progress_bar=False,convert_to_numpy=True,normalize_embeddings=True# L2 归一化)returnembeddings

4.3 模型选型理由

模型维度特点选择理由
BGE-M31024多语言支持✅ 中文效果好,推理速度快
text2vec768中文专用中文场景也不错
paraphrase-multilingual768多语言 paraphrase语义理解好

BGE-M3 优势

  1. 多语言支持:同时支持中英文医学术语
  2. 效果好:在 MTEB 榜单上表现优异
  3. 速度快:CPU 推理效率高

五、规则过滤器与Rerank部署

5.1 规则过滤器核心代码

classRuleFilter:"""规则过滤器"""def__init__(self):# 非医学关键词(直接拦截)self.non_medical_keywords=['天气','新闻','股票','电影','音乐','游戏','足球','篮球','明星','娱乐','购物','外卖','打车','旅游','酒店','餐厅','今天','明天','后天','日期','时间','几点','星期几','你好','在吗','帮我','请问','叫什么','你是谁','介绍','笑话','故事','八卦','娱乐','做饭','做菜','减肥',]# 疾病名称(高分)self.disease_keywords=['糖尿病','高血压','冠心病','心肌梗死','心衰','心律失常','肺炎','支气管炎','哮喘','COPD','肺癌','肺结核','胃炎','胃溃疡','肠炎','肝炎','肝硬化','胃癌','肠癌','肾炎','肾病','尿毒症','肾结石','脑梗','脑出血','脑卒中','癫痫','帕金森','老年痴呆',# ... 更多疾病]# 症状关键词(中高分)self.symptom_keywords=['症状','表现','体征','临床表现','阳性体征','疼痛','发热','发烧','咳嗽','呕吐','腹泻','便秘','头痛','胸痛','腹痛','腰痛','关节痛','背痛','麻木','无力','疲劳','乏力','消瘦','体重下降',# ... 更多症状]# 科室关键词(中分)self.department_keywords={'心内科':['心血管','心脏','冠心病','心梗','心律失常','心衰','高血压'],'呼吸内科':['呼吸','肺','支气管','肺炎','哮喘','咳嗽','咯血'],'消化内科':['消化','胃肠','胃','肠','肝','胆','胰腺','腹泻','便秘'],'内分泌科':['内分泌','甲状腺','糖尿病','甲亢','甲减','激素'],'神经内科':['神经','脑','脑血管','癫痫','帕金森','头痛','头晕','麻木'],# ... 更多科室}deffilter(self,query:str):"""过滤用户输入"""query=query.strip()ifnotquery:return{'pass':False,'reason':'empty','type':'empty','department':None,'suggestion':'请输入您的问题','score':0}iflen(query)<2:return{'pass':False,'reason':'too_short','type':'invalid','department':None,'suggestion':'问题太短,请详细描述','score':0}# 检查非医学关键词forkeywordinself.non_medical_keywords:ifkeywordinquery:has_medical=any(kwinqueryforkwinself.symptom_keywords+self.disease_keywords)ifnothas_medical:return{'pass':False,'reason':'non_medical','type':'non_medical','department':None,'suggestion':'这是医学知识库,请问您有医学相关的问题吗?','score':0}# 计算得分score=self._calculate_score(query)ifscore>=2:department=self._identify_department(query)return{'pass':True,'reason':'medical','type':'medical','department':department,'suggestion':None,'score':score}else:return{'pass':False,'reason':'low_score','type':'unclear','department':None,'suggestion':'您的问题可能与医学无关,请描述具体的症状或疾病','score':score}def_calculate_score(self,query:str)->int:"""计算医学得分"""score=0forkeywordinself.disease_keywords:ifkeywordinquery:score+=3# 疾病名称 3分forkeywordinself.symptom_keywords:ifkeywordinquery:score+=2# 症状关键词 2分fordept,keywordsinself.department_keywords.items():forkeywordinkeywords:ifkeywordinquery:score+=1# 科室关键词 1分breakreturnscoredef_identify_department(self,query:str):"""识别科室"""scores={}fordept,keywordsinself.department_keywords.items():score=sum(1forkwinkeywordsifkwinquery)ifscore>0:scores[dept]=scorereturnmax(scores,key=scores.get)ifscoreselseNone

得分机制

关键词类型得分示例
疾病名称3分糖尿病、高血压、冠心病
症状关键词2分胸痛、发热、咳嗽
科室关键词1分心内科、呼吸内科

5.2 过滤流程图

用户输入 │ ├─→ 空? ──→ 拦截 "请输入您的问题" │ ├─→ 包含非医学词? ──→ 无医学词? ──→ 拦截 "这是医学知识库..." │ └─→ 计算医学得分 │ ├─→ 得分 >= 2 ──→ 通过 └─→ 得分 < 2 ──→ 拦截 "您的问题可能与医学无关..."


无关知识会被过滤

5.3 Rerank模型部署

classReranker:"""Rerank 重排器"""def__init__(self):self.model=Noneself.model_name='BAAI/bge-reranker-v2-m3'defload(self):"""加载 Rerank 模型"""print(f"加载 Rerank 模型:{self.model_name}")try:self.model=CrossEncoder(self.model_name,max_length=512,device='cpu')print("Rerank 模型加载成功!")returnTrueexceptExceptionase:print(f"Rerank 模型加载失败:{e}")self.model=NonereturnFalsedefrerank(self,query,chunks,top_k=5):"""对 chunks 进行重排"""ifnotself.modelornotchunks:returnchunks# 构建 query-document 对pairs=[(query,chunk.get('text',''))forchunkinchunks]# 计算相关性分数scores=self.model.predict(pairs)# 添加分数到 chunksfori,chunkinenumerate(chunks):chunk['rerank_score']=float(scores[i])# 按 rerank 分数排序reranked=sorted(chunks,key=lambdax:x['rerank_score'],reverse=True)returnreranked[:top_k]

5.4 Rerank 原理

初步检索结果(按向量相似度) ┌─────────────────────────────────────┐ │ 1. 糖尿病的诊断标准 [0.92] │ │ 2. 血糖测定方法 [0.91] │ │ 3. 高血压诊断 [0.89] │ │ 4. 糖尿病治疗方案 [0.88] │ │ 5. 今天天气怎么样 [0.85] ← 误召 │ └─────────────────────────────────────┘ ↓ Rerank 重排 ┌─────────────────────────────────────┐ │ 1. 糖尿病的诊断标准 [0.98] ↑ │ │ 2. 糖尿病治疗方案 [0.95] ↑ │ │ 3. 血糖测定方法 [0.91] ↓ │ │ 4. 高血压诊断 [0.78] ↓ │ │ 5. 今天天气怎么样 [0.12] ↓ 被降 │ └─────────────────────────────────────┘

六、检索策略实现

6.1 索引构建核心代码

classIndexBuilder:"""索引构建器"""def__init__(self,config_path:str=None):self.method=self.retrieval_config.get('method','hybrid')self.dense_weight=self.retrieval_config.get('dense_weight',0.6)self.bm25_weight=self.retrieval_config.get('bm25_weight',0.4)self.top_k=self.retrieval_config.get('top_k',20)defbuild_faiss_index(self,vectors:np.ndarray)->faiss.Index:"""构建 FAISS 向量索引"""dimension=vectors.shape[1]print(f"构建 FAISS 索引,维度:{dimension}")# 使用内积索引(需要归一化向量)index=faiss.IndexFlatIP(dimension)index.add(vectors.astype('float32'))print(f"FAISS 索引构建完成,向量数:{index.ntotal}")returnindexdefbuild_bm25_index(self,texts:List[str])->BM25Okapi:"""构建 BM25 索引"""# 中文分词(使用简单空格分词)tokenized_texts=[text.split()fortextintexts]bm25=BM25Okapi(tokenized_texts)returnbm25

6.2 检索配置

# 检索配置retrieval:method:"hybrid"# hybrid / dense / bm25dense_weight:0.6# 向量检索权重bm25_weight:0.4# BM25 权重top_k:20# 初步召回数量# Rerank 配置rerank:enabled:truemodel:"BAAI/bge-reranker-v2-m3"top_k:5# Rerank 后保留数量

6.3 检索流程详解

classRecallTester:"""召回测试器"""defsearch(self,query,top_k=5,use_rerank=True):"""搜索相关 chunks"""results=[]# 1. 向量检索 (FAISS)ifself.modelandself.index:query_vector=self.model.encode([query],normalize_embeddings=True)distances,indices=self.index.search(query_vector.astype('float32'),top_k*4# 初步召回更多结果)foridx,distinzip(indices[0],distances[0]):ifidx>=0andidx<len(self.chunks):chunk=self.chunks[idx].copy()chunk['vector_score']=float(dist)results.append(chunk)# 2. Rerank 重排ifuse_rerankandresults:results=self.reranker.rerank(query,results,top_k=top_k)returnresults[:top_k]

检索三步曲

1. 向量检索 → 召回 Top-20 2. Rerank重排 → 精排 Top-5 3. 返回结果

6.4 混合检索融合

``
`


七、完整调用流程

defmain():tester=RecallTester()# 加载知识库tester.load()# 用户输入query=input("请输入问题: ")# 规则过滤result=tester.filter.filter(query)ifnotresult['pass']:print(f"拦截:{result['suggestion']}")return# 知识库检索results=tester.search(query,top_k=5,use_rerank=True)# 打印结果tester.print_results(results)

九、总结与展望

  • 🔄 LLM 接入与 Prompt 优化
  • 🔄 上下文窗口管理
  • 🔄 知识库自我更新
  • 🔄 对话历史管理(短期/长期记忆)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询