1. 探矿业务文档的特点与清洗的整体思路1.1 探矿资料的真实处境为什么检索总是翻车做探矿业务的同行应该都有过这种经历明明资料库里什么都存着可真到要用的时候什么都找不着。历史钻孔编录躺在一个个TXT里有的打开是乱码合作方发来的Word文档排版精美一转换段落全混在一起扫描版的PDF报告复制出来全是错字网页上找的矿政政策抓下来全是广告导航。这些资料共同构成了探矿业务的知识底座但它们的物理形态五花八门质量参差不齐。直接丢给RAG去做检索增强生成结果大差不差一是召回率低问“某个矿区ZK12钻孔的品位数据”模型给你返回一段页眉二是精度差明明问的是铜矿体厚度返回的却是区域地质背景介绍更麻烦的是很多关键表格数据在转换过程中就丢了行、丢了列检索系统压根看不见。这不是模型能力的问题而是上游数据质量的问题。RAG链条里有一条业内公认的规律检索效果的天花板由文档清洗的下限决定。Embedding模型再强、向量库再快喂进去的是乱码和结构崩坏的文本召回质量一定稀烂。所以在探矿业务里想做出一个真正能用的知识库第一场硬仗不是模型选型而是文档清洗和结构重建。1.2 清洗的目标不是“变干净”而是“让切块可检索”很多刚上手RAG的同学有个误区觉得清洗就是把乱码修好、把空格删掉、看起来整洁即可。但在探矿业务里文档清洗的本质目标有三个层次。第一层是可读性把编码错误、OCR错误、格式废料清掉让文本能被人和模型正常读取。第二层是结构性把钻孔编录里的深度区间、岩性描述、采样编号识别出来让切块切在语义边界上而不是切在段落中间。第三层是可追溯性保留住页码、来源文件、章节信息、坐标位置这些元数据让检索结果能溯源到原始报告的具体位置这对探矿报告这种需要严格查证的文档类型来说特别关键。换句话说清洗工作要服务的不是“阅读体验”而是后续的切块、向量化、召回和引用。每一处处理决策都应该问一句这个操作对检索精度有帮助吗如果没有就不值得做。2. 四大文档源的清洗实操TXT、Word、PDF、网页2.1 TXT文件先解决编码再对付半结构化文本TXT在探矿业务里的存在感极强很多老项目的野外记录、钻孔编录、化探数据都以TXT格式存放有些甚至是从DOS时代的软件里导出来的。这种文件最大的问题就是编码混乱。常见的就有GBK、GB18030、UTF-8、UTF-16、BIG5甚至还有带着BOM标记的UTF-8。你要是拿默认的UTF-8去读一个GBK编码的文件满屏都是“锟斤拷”和“烫烫烫”。处理时我习惯先做编码探测再按探测结果解码。Python里用chardet或者charset-normalizer可以粗暴判断但对短文件准确率不够。稳妥的做法是多重探测加人工兜底先用charset_normalizer判断如果置信度低就尝试GB18030和UTF-8两种最可能的编码分别解码用错误字符比例来打分选最优解。import charset_normalizer from pathlib import Path def decode_text_file(file_path: Path): raw file_path.read_bytes() # 先用detector检测 detected charset_normalizer.from_bytes(raw).best() if detected and detected.confidence 0.7: return str(detected) # 兜底策略GB18030/UTF-8轮询 for enc in [utf-8, gb18030]: try: text raw.decode(enc) # 用替换字符比例评估解码质量 bad_ratio text.count(\ufffd) / max(len(text), 1) if bad_ratio 0.01: return text except UnicodeDecodeError: continue return raw.decode(utf-8, errorsignore)编码搞定之后真正的难点在半结构化文本的归一化。探矿TXT里大量存在这种行ZK12 0.00-15.20m 第四系残坡积层 褐黄色含砾粉质粘土 ZK12 15.20-38.50m 全风化花岗岩 灰黄色风化强烈原岩结构基本破坏这种格式人类看得懂但对RAG来说很微妙。如果直接切块切出来的chunk可能一半是钻孔编录一半是后面的岩性描述检索时语义被割裂。我的做法是先做行级结构化用正则识别深度区间\d\.\d\s*[-~]\s*\d\.\d\s*m、岩性关键词、层位描述把这些行重组为“钻孔编号深度区间岩性描述”的结构化短句再交给后续的切块流程。这样每个切块都是完整的观测记录召回时相关性明显更高。2.2 Word文档格式噪声、表格与公式的取舍Word是探矿报告的主战场。资源储量估算报告、勘查实施方案、年度工作总结基本都是Word格式。这类文档清洗时有一个容易忽略的麻烦Word的排版结构在转换成纯文本时会“塌方”。目录、页眉页脚、批注、域代码、文本框里的内容都会以不可预期的位置混进正文。我用python-docx解析时会明确区分段落和表格对页眉页脚单独处理绝不直接合并。from docx import Document doc Document(2024年度勘查工作总结.docx) # 提取正文段落跳过目录和空段落 paragraphs [] in_toc False for para in doc.paragraphs: text para.text.strip() if not text: continue # 目录区检测连续多行仅含数字和点号 if text.endswith(目录) or set(text.replace(., ).replace( , )) set(0123456789): in_toc True continue if in_toc and text and not text[0].isdigit(): in_toc False if not in_toc: paragraphs.append(text) # 提取表格注意表格行不能丢失 for table in doc.tables: for row in table.rows: cells [cell.text.strip().replace(\n, ) for cell in row.cells] paragraphs.append( | .join(cells))这里特别要说的是表格处理。储量报告里的品位数据、钻孔采样分析结果、工程测量坐标表几乎全在表格里。把表格按“行”转成文本用分隔符隔开这样既能保留表格的对应关系也方便后续切块时把同一表格的行聚合在同一个chunk中。还有一个现实问题探矿Word里经常嵌公式和插图比如资源量估算公式、地质剖面图。公式如果只是当作普通文本提取会变成一堆乱码我处理时会把公式区域识别出来用Latex或Unicode数学符号重写图片则单独抽取出来配合OCR识别图内文字作为图片的替代文本存入元数据。这一步很费功夫但回报也大因为图件和公式恰恰是专家提问时最容易命中的内容。2.3 PDF文档扫描版和电子版得分开打PDF在探矿资料里是最麻烦的一类。老项目的纸质报告扫描件、历史图件的PDF化、公开地质资料的电子PDF混杂在一起。我处理PDF的第一件事就是判断它是电子版还是扫描版。方法不复杂用PyMuPDF提取前几页的文本如果每页提取出的字符数量接近零大概率是扫描版需要走OCR路线否则走文本提取路线。电子版PDF的处理重点有两个双栏布局和页眉页脚。很多地质期刊、国家发布的矿政文件都是双栏排版直接按阅读顺序提取文本会左右栏交错。我一般用pdfplumber的extract_words拿到每个词的坐标按x坐标分成左右两栏再分别按y坐标排序最后交叉拼接成正确的阅读顺序。页眉页脚的处理则要参考整份文档的统一性。比如每页顶部都有“XX矿区勘探报告”如果不滤除切块里会反复出现这段噪声检索时被高频词干扰。我做的是扫描每页顶部10%和底部8%区域的内容如果连续多页相同就在提取时剔除。扫描版PDF的清洗就更重了。需要先做倾斜校正再用OCR引擎识别最后还得处理识别结果的错字。探矿文档里有大量的专业术语“斜长角闪岩”“黄铜矿化”“绿泥石化”这些词通用OCR词典里根本没有识别错字率很高。解决办法是维护一个地质术语自定义词典OCR后对文本做一次词典回改把疑似错字替换成正确的专业术语。import fitz from paddleocr import PaddleOCR import re ocr PaddleOCR(use_angle_clsTrue, langch) def extract_scanned_pdf(pdf_path): doc fitz.open(pdf_path) texts [] for page in doc: # 先做倾斜校正 pix page.get_pixmap(dpi300) pix.save(page.png) result ocr.ocr(page.png, clsTrue) page_text \n.join(line[1][0] for line in result[0]) # 地质术语回改 page_text geology_dict_correct(page_text) texts.append(page_text) return \n.join(texts)这里还有个心得扫描件分辨率不够的时候强行OCR只会产出大量幻觉字符。与其用150dpi的模糊图硬怼不如用Acrobat或开源工具先把PDF转成300dpi的图片再OCR识别率会有肉眼可见的提升。2.4 网页资料正文抽取与动态加载处理探矿业务里网页资料的比重这两年明显上升尤其是矿政法规、区块出让公告、储量评审备案信息都是从网站抓下来的。网页清洗的核心是正文抽取和去重。正文抽取可以用现成的库比如trafilatura或readability-lxml做出来效果比较稳定。我实测下来trafilatura对中文网页的适配性还可以能把正文从导航、侧栏、评论区里分离出来。抓取时还需要保留几个关键元数据发布时间、来源站点、原文URL。探矿业务对政策文件的时效性要求极高同一条矿政文件不同年份的版本检索结果必须能区分清楚。动态加载的网页是另一个坑。不少地矿信息平台用JavaScript渲染内容直接爬HTML拿不到正文。我的解决办法是接Playwright无头浏览器渲染完再提取文本。虽然耗时增加但对检索质量帮助很大宁可多花几秒也不能让关键内容直接漏掉。网页文本还有一层清洗要做去重。不少网页转载内容会在末尾附上“相关推荐”“热门文章”这些内容会污染知识库的语义空间。我通常用URL结构和正文长度双重过滤把明显的推荐文章、列表页、登录页排除掉只保留有价值的信息页。3. 清洗之后的切块策略别让切块毁了清洗成果3.1 探矿文档的语义切块固定长度不是最优解清洗工作做得再好到了切块环节如果瞎切一通照样前功尽弃。RAG里切块的核心矛盾是块太大向量表示被稀释召回精度下降块太小语义不完整上下文断裂生成时信息不足。通用做法是固定长度切块比如按300~500个token去切。但在探矿文档上我强烈建议优先做结构感知切块。因为探矿文档天然有清晰的语义边界钻孔编录按钻孔编号划分储量估算按矿体编号划分采样分析按表格划分。用标题层级和段落边界做切块比纯长度切块的语义完整度高得多。我做切块时用的是分段策略先按文档结构树递归遍历遇到标题就把新的语义块标记为开始正文内容按段落累积超过阈值就断块表格内容按行聚合整表尽量放一个块。切块之后再做一轮超长块的二次拆分按照句子边界硬切但保证每个子块开头带上所属章节的标题信息作为上下文。def structure_aware_chunking(structured_doc, max_tokens400, overlap50): chunks [] current_chunk [] current_len 0 for section in structured_doc.sections: # 按语义单元遍历标题/段落/表格 content f【{section.title}】\n{section.content} content_len count_tokens(content) if current_len content_len max_tokens and current_chunk: chunks.append(\n.join(current_chunk)) # 重叠部分保留上一个段的尾部 overlap_text current_chunk[-overlap:] current_chunk overlap_text [content] current_len count_tokens(\n.join(current_chunk)) else: current_chunk.append(content) current_len content_len if current_chunk: chunks.append(\n.join(current_chunk)) return chunks3.2 元数据注入检索精度的隐形杠杆很多RAG项目忽略元数据的作用其实元数据是检索精度的隐形杠杆。在探矿业务里我会给每个chunk注入以下元数据元数据字段示例值用途来源类型PDF / Word / TXT / Web检索过滤限定查询范围文件名称某矿区储量核实报告2023.pdf引用溯源章节路径第三章/矿体地质特征/3.2 矿体形态语义上下文页码47精准定位项目编号KY2023-007项目维度过滤坐标点位42.3°N, 118.5°E地理位置检索发布时间2023-06-15时效性过滤这些元数据在检索时可以被当作过滤条件。比如工程师问“某矿区ZK05钻孔的累计见矿厚度”如果知识库里有多个矿区的资料元数据里的地理位置和项目编号能直接缩小召回范围避免跨矿区串线。这个操作对探矿业务检索精度的提升有时候比换一个更强的Embedding模型还明显。3.3 切块重叠与关键段落增强切块重叠overlap也是一个需要根据文档类型调整的参数。我一般取50~100个token的重叠保证段落边界处的语义连接不被切断。具体取值要看文档特征如果段落本身短小精悍比如钻孔编录的单行记录重叠可以少一些如果段落长且信息密度高比如储量估算的章节重叠需要多一些。还有一个进阶技巧是关键段落增强。我会先用一个轻量级模型或者规则识别出chunk里的关键信息片段——比如矿体品位、厚度、深度、资源量数字——把这些片段提取出来生成一个简短的摘要或关键词列表作为chunk的补充字段。查询时做两路召回一路走向量相似度一路走关键词匹配然后用RRFReciprocal Rank Fusion融合排序。实测下来这种“向量关键词元数据过滤”的三层策略比单靠向量召回的top-5准确率高出不少。特别是当探矿报告里大量出现数字和代号时向量模型对“ZK12孔铜品位0.68%”这种精确数值的语义理解并不稳定关键词匹配能补上这个短板。4. 清洗管线与效果实测从手工处理到半自动流程4.1 清洗管线搭建固定套路串起来把所有处理串成一条管线是我做探矿RAG项目最核心的工程步骤。完整流程长这样原始文件先进入“类型识别器”按扩展名和文件头区分TXT、Word、PDF、网页随后进入各自的抽取器统一输出结构化文档标题树段落列表表格列表再进入清洗器做编码修复、页眉页脚过滤、OCR纠错、术语回改清洗后的结构化文档送到切块器按语义边界切块并注入元数据最后经过Embedding模型向量化写入向量库。这个管线我建议按批处理的方式来跑。一次性灌入几千份历史报告如果中途出错最好能定位到具体文件而不是整个流程推倒重来。我会在管线里加一个处理日志记录每个文件的清洗状态成功、失败、还是部分成功。部分成功的文件人工复核一遍比重跑全量高效得多。4.2 检索效果评估用真实业务问题打分清洗和切块做得好不好不能靠感觉得靠结果说话。我给探矿RAG项目做评估时会先整理一份业务问题测试集。从矿区工程师、储量核算人员、项目负责人的真实提问中收集20到30个问题每个问题关联到原始文档里的标准答案段落。评估指标我主要看三个top-5命中率答案是否在召回的前5个chunk里、MRR正确答案的排序位置倒数均值、溯源准确率召回的chunk是否来自正确的那份报告和页码。这样评估的好处是能直接反映业务人员的实际使用感受。4.3 清洗前后对比数字不会骗人我在一个中型探矿知识库上做过一次清洗前与清洗后的对比测试。这个知识库包含127份PDF报告、43份Word报告、86份TXT钻孔编录、以及一批网页政策文件总共约1.2万个文档。清洗前只做了最基本的文本提取和固定长度切块清洗后按上述管线走了全流程。对比结果如下评估指标清洗前清洗后乱码/噪声字符占比18.4%0.9%表格数据完整保留比例62%95%top-5命中率41%78%MRR0.230.61溯源准确率页码定位无法溯源92%top-5命中率从41%提升到78%MRR从0.23到0.61。这个提升幅度在RAG项目里算是很大的一档而成本仅仅是把清洗流程做扎实、把切块改成结构感知、把元数据加上。我没有更换Embedding模型也没有调整向量检索算法纯粹是上游数据质量的杠杆效应。5. 常见问题与排查技巧实录5.1 编码探测不准确导致的乱码残留用chardet做编码探测短文本经常误判。一段40个字的钻孔描述放在GBK和UTF-8下解码都可能“不报错”但内容完全不同。我的排查方法是不要看置信度直接看解码结果里是否包含地质常见字符。如果一段话里出现了“矿”“岩”“孔”“深度”这些高频字大概率是正确编码。还可以做一个“地质词典覆盖率”的评分函数覆盖率高者胜出。注意GBK和GB18030是兼容关系但GB18030能解码更多生僻字。如果GBK解码报错一定要再试GB18030不要直接跳到UTF-8。5.2 Word表格提取丢行漏列这种情况常出在包含合并单元格或嵌套表格的复杂表格里。python-docx对合并单元格的表达是同一个cell对象被多处引用直接遍历行的时候会出现重复内容或者漏行。我的解决办法是先做表格结构重建按行坐标和列坐标建一个二维矩阵合并单元格在矩阵里铺开内容填充到所有占据的格子中再去重输出。这样既能保证行不漏也能对齐列关系。5.3 PDF扫描版OCR误识别专业术语地质报告里“黄铜矿”被OCR成“黄铜矿”是小事被识别成“百铜矿”就麻烦了。我遇到最多的是这类问题。对策是术语词典回改把OCR文本里那些“语义奇怪但出现在专业词典上下文附近”的词收集起来用编辑距离匹配到正确术语。像“角闪石”这类词长度短、变形空间小编辑距离阈值为1就能覆盖大量误识别。另一个容易忽略的点是地质报告里大量出现的数字和单位比如“0.68%”“m”“g/t”OCR之后经常变成“0.68%”和“glt”这类混排。我建议单独做一轮数字与单位规整把常见单位缩写和符号统一映射到标准形式。5.4 网页正文抓取不全很多矿政网站的正文是分成多页显示的或者内容通过Ajax加载直接爬只能拿到第一页和空壳。我排查时先看抓下来的文本里是否包含发布时间、正文结尾标志词比如“特此公告”如果没有就改用Playwright渲染再结合分页链接的识别把多页正文拼接。拼接时要用URL参数里的页码顺序不能按抓取顺序硬拼否则页面乱序会影响切块。5.5 过度清洗导致上下文断裂清洗不是越干净越好。有一回我把所有页眉页脚都滤除了结果发现一部分报告里“矿区名称”只在页眉里出现滤掉之后切块丢失了矿区上下文检索时同一批钻孔记录无法区分矿区归属。从那以后我做了一个原则过滤噪声之前先判断它是不是信息。页眉里连续出现的固定信息要滤除但只出现一次的“页眉异类”要保留因为它很可能是跨章节的上下文提示。5.6 切块之后才发现清洗遗漏这是一个工程上的教训。管线跑完之后我拿几个查询去验证检索结果发现某些chunk里还是混着乱码。追查下来是漏掉了一份PDF扫描件的倾斜校正步骤OCR结果全是歪斜识别。排查流程调了一晚上最后定位到是某台处理机的PaddleOCR版本不一致导致的预处理差异。所以现在我在管线里加了切块后的质量抽检环节随机抽取5%的chunk检查乱码率、异常字符率、空段落率超阈值就标记对应源文件重新处理。最后分享一点实战体会做探矿业务的RAG清洗体感上最强烈的一点是这件事没有一步到位的银弹功夫全在逐个文件、逐个格式的打磨里。刚开始推进的时候进度会慢得让人怀疑人生但清洗质量一旦过了某个临界点后面检索效果会有个明显的跃升。我个人建议先挑一批价值最高、使用最频繁的文档比如最新的储量核实报告和钻孔编录做精细清洗小范围验证检索效果再滚动扩大规模。这个路径投入产出比最高也最容易被业务方理解和支持。