基于 FastAPI + Qdrant 构建可解释、可约束的教育 Agent(三):知识清洗与结构感知切片
基于 FastAPI Qdrant 构建可解释、可约束的教育 Agent三知识清洗与结构感知切片本文内容系列第三篇承接文档解析介绍 RAG 入库前的两个环节——知识清洗与结构感知切片。清洗阶段逐行剔除重复、样板、纯链接与孤立符号并以可复算的启发式规则计算质量分切片阶段按块类型区别处理标题不单独成块表格与图片整块保留正文按句子边界切分并保留重叠。技术栈Python 3.11、正则规则、结构化 dataclass默认参数 target_size1100、hard_max1800、overlap160。文章目录基于 FastAPI Qdrant 构建可解释、可约束的教育 Agent三知识清洗与结构感知切片1. 清洗与切片在 RAG 中的位置2. 知识清洗管线3. 质量分计算4. 结构感知切片5. 正文切分与 overlap6. 参数与工程处理7. 系列规划结语1. 清洗与切片在 RAG 中的位置文档解析产出结构化的 ParsedBlock 后若直接按固定长度切分入库会引入两类问题解析结果中仍包含每页重复的页眉、版权声明、纯页码、乱码符号等低信息内容固定长度切分会在句子中间、表格内部断开破坏语义与表格结构。项目将处理顺序固定为解析 → 清洗过滤 质量打分→ 结构感知切片。在切块前剔除脏内容、在完整块上计算质量分成本与准确性都更优。2. 知识清洗管线KnowledgeCleaningService.clean逐块、逐行处理整体管线如下逐行过滤规则包括重复行长度在 4–80 之间、全篇出现不少于 3 次的行样板行页码、纯数字、分隔线、版权声明、机密标识等纯链接行整行匹配http(s)://孤立符号长度 ≤2 且不含中英文。样板行通过预编译正则匹配BOILERPLATE_REre.compile(r^(?:第\s*\d\s*页|page\s*\d|\d|[-–—]|课程资料\s*版权所有|版权所有|copyright|confidential|机密)$,re.I,)逐行过滤实现def_clean_block_text(self,text,repeated):lines[]forrawinstr(textor).splitlines():line_normalize_text(raw)ifnotline:continueiflineinrepeated:continueifself.BOILERPLATE_RE.match(line):continueifre.fullmatch(rhttps?://\S,line,re.I):continueiflen(line)2andnotre.search(r[\u4e00-\u9fffA-Za-z],line):continuelines.append(line)return_normalize_text(\n.join(lines))重复行统计限定长度 4–80避免短词在正常内容中重复出现被误删def_repeated_lines(self,blocks):countsdefaultdict(int)forblockinblocks:forrawinblock.text.splitlines():line_normalize_text(raw)if4len(line)80:counts[line]1return{lineforline,countincounts.items()ifcount3}3. 质量分计算质量分不使用模型而采用可复算的启发式规则定义在knowledge_quality.pydefscore_knowledge_text(text,block_typetext):contentstr(textor).strip()ifnotcontent:return0.0ifblock_typein{heading,table,visual_summary}:return0.9signallen(re.findall(r[\u4e00-\u9fffA-Za-z0-9],content))noiselen(re.findall(r|□|■|▯|[^\s\u4e00-\u9fffA-Za-z0-9。、,.!?;:()\[\]\-*/|#],content))densitysignal/max(1,len(content))length_scoremin(1.0,signal/80)noise_penaltymin(0.6,noise/max(1,len(content)))returnround(max(0.0,density*0.55length_score*0.45-noise_penalty),3)清洗时文本为空或质量分低于 0.2 的块被丢弃并计入 low_quality保留块将分数写入 metadata。文档质量分取保留块分数的均值报告记录 total_blocks、kept_blocks、dropped_blocks、low_quality_blocks。4. 结构感知切片StructureAwareChunker.chunk按块类型区别处理关键规则heading 块跳过不单独成块标题信息通过 section_path 保留block_type 归一为 table、image含 image、visual_summary、texttable / image 在长度不超过 hard_max 时整块保留避免 Markdown 表格被切断text 块进入句子级切分检索文本由章节路径与正文拼接补充上下文。forblockindoc.blocks:block_qualityfloat(block.metadata.get(quality_score,doc.quality_scoreor0.6))ifblock.block_typein{heading}:continueblock_typetableifblock.block_typetableelseimageifblock.block_typein{image,visual_summary}elsetexttexts[block.text]ifblock_typein{table,image}andlen(block.text)self.hard_maxelseself._split_block(block.text)forpartintexts:sectionblock.section_pathorchapterordoc.document_name retrieval_text_normalize_text(\n.join([section,part]))# 组装 StructuredChunk 与 metadata每个 chunk 的 metadata 包含 document_id、chunk_id、chapter、section_path、chunk_index、chunk_type、quality_score、page、file_type、resource_type、retrieval_text用于引用溯源、类型过滤与质量降权。5. 正文切分与 overlap正文块先按句末标点或换行切出句子单元再贪心装箱def_split_block(self,text):unitsre.split(r(?[。.!?])\s*|\n,_normalize_text(text))units[u.strip()foruinunitsifu.strip()]ifnotunits:units[text]chunks,buf[],forunitinunits:iflen(buf)len(unit)self.target_size:buff{buf}\n{unit}.strip()ifbufelseunitcontinueifbuf:chunks.append(buf)prefixbuf[-self.overlap:]ifself.overlapandbufelsebuff{prefix}\n{unit}.strip()ifprefixelseunitwhilelen(buf)self.hard_max:chunks.append(buf[:self.hard_max])bufbuf[self.hard_max-self.overlap:]ifself.overlapself.hard_maxelsebuf[self.hard_max:]ifbuf:chunks.append(buf)returnchunks相邻块保留 overlap避免完整答案横跨切块边界时无法被完整召回hard_max 用于处理缺少标点的超长文本。6. 参数与工程处理默认切片参数如下参数默认值含义target_size1100单块目标长度hard_max1800单块硬上限overlap160相邻块重叠长度工程处理要点清洗先于切片避免脏内容在切块后扩散质量分规则确定、可复算不引入额外推理依赖表格与图片整块保留正文只在句子边界断开章节路径前置到检索文本提升短文本与表格行的召回语义。7. 系列规划知识工程部分后续将介绍查询分析与多路召回、多路结果融合、精排与动态 TopK、证据包构建。下一篇聚焦查询分析与多路召回。结语本文给出了知识清洗与结构感知切片的实现思路核心是在入库前去除低信息内容、保留文档结构并让每个 chunk 携带足够的上下文与元数据。相关实现仍在持续迭代欢迎在评论区讨论改进方案。