1. 探矿业务里的数据泥潭:为什么通用RAG方案一上来就翻车
做过矿业数字化项目的人都有一个共同体会:这个行业的数据,脏得很有层次感。地质报告是扫描件PDF,钻孔编录是上世纪九十年代Word 97格式,化探数据是TXT但编码是GBK,矿区公示信息散落在十几个网页里,还有一堆SHP转出来的坐标文本。你把这些东西一股脑丢给通用RAG框架,检索出来的结果基本没法看——要么是乱码切片,要么是表格结构全丢,要么是同一份报告被切成几百个语义碎片,问它“XX矿区铜品位多少”,它给你返回一段讲地层年代的段落。
这不是RAG本身的问题,是清洗环节被严重低估了。通用方案默认你的文档是干净的、结构化的、编码统一的,但探矿业务恰恰相反。我前后经手过三个地质资料知识库项目,踩过的坑足够写一本避雷手册。这篇文章就把TXT、Word、PDF、网页这四类数据在探矿场景下的RAG清洗思路完整拆一遍,从编码识别到版面还原,从表格抽取到切片策略,每个环节都给出可复现的操作方案。
核心关键词RAG、TXT、Word、PDF、网页会贯穿全文,但我不打算写成工具说明书,而是按“为什么这么选—具体怎么做—踩过什么坑”的逻辑展开。适合正在做地质、矿业、勘探类知识库的工程师,也适合任何被非结构化文档折磨过的RAG开发者。读完你至少能拿到一套可直接抄作业的清洗流水线,以及一份价值不菲的避坑清单。
2. 整体清洗架构设计:四类数据源的分治策略
2.1 为什么不能用一个解析器打天下
很多人上来就想找一个“万能文档解析库”,Python里试一圈pdfplumber、python-docx、BeautifulSoup,然后写一个统一入口。这个思路在探矿业务里必然失败,原因有三。
第一,编码问题在TXT里是致命的。地质行业大量老数据是GBK甚至GB2312编码,有些化探数据文件还混着全角字符和特殊符号。你直接用UTF-8读,轻则乱码,重则抛异常中断整个流水线。而PDF和Word的编码是封装在文件格式内部的,处理逻辑完全不同。
第二,版面信息的价值差异巨大。PDF里的钻孔柱状图、Word里的品位表格、网页里的矿权公示表格,这些结构化信息是探矿RAG的核心检索目标。通用文本抽取会把表格拍扁成一行字符串,坐标和品位数值混在一起,检索精度直接归零。
第三,切片策略必须跟着数据源走。TXT化探数据适合按行或按块切,Word地质报告适合按章节切,PDF扫描件需要先做版面分析再切,网页需要先做正文抽取再切。用同一套切片参数,等于让所有数据都受最差那类数据的拖累。
所以我的方案是分治架构:四类数据源各自走独立的清洗管道,最终汇入统一的向量化与索引层。下面这张表是我在实际项目中沉淀下来的分治策略对照。
| 数据源 | 核心难点 | 首选解析方案 | 切片粒度 | 元数据重点 |
|---|---|---|---|---|
| TXT | 编码混乱、分隔符不统一 | chardet检测+多编码回退 | 按行/按记录块 | 文件来源、坐标系统 |
| Word | 表格嵌套、公式对象、版本差异 | python-docx+自定义表格抽取 | 按章节+表格独立 | 报告编号、编写单位 |
| 扫描件无文本层、双栏排版 | PyMuPDF+OCR回退+版面分析 | 按段落+表格区域 | 页码、图件编号 | |
| 网页 | 正文噪声、动态渲染 | 正文抽取算法+结构化解析 | 按语义块 | URL、发布时间 |
2.2 清洗流水线的五个阶段
不管哪类数据源,清洗流水线都遵循同样的五阶段模型,只是每个阶段的具体实现不同。
阶段一:格式识别与预处理。判断文件真实类型(不能只看扩展名),检测编码,识别是否加密,评估文本层质量。这一步的输出是一份“体检报告”,决定后续走哪条分支。
阶段二:内容抽取。从原始格式中提取文本、表格、图片说明等可用信息。关键是保留结构信息,不能只拿纯文本。
阶段三:噪声清洗。去掉页眉页脚、水印文字、重复的免责声明、网页导航栏残留。探矿报告里常见的“内部资料注意保密”这类页眉,如果不清掉,会在每个切片里出现,严重干扰检索。
阶段四:结构化重组。把抽取出来的碎片按逻辑关系重新组织,表格还原成Markdown或JSON,章节建立层级关系,坐标数据关联到对应矿区。
阶段五:切片与元数据注入。按数据源特性选择切片策略,同时把来源、时间、矿区、坐标系统等元数据注入每个切片,供后续过滤检索使用。
注意:阶段三和阶段四的顺序不能颠倒。我见过先重组再清洗的方案,结果页眉文字被当成章节标题嵌进了层级结构里,后面怎么改都别扭。
2.3 元数据设计:被大多数人忽略的检索加速器
探矿RAG和通用RAG最大的区别在于,用户查询几乎总是带条件的。“XX矿区”“XX年份”“铜品位”“钻孔编号”这些条件,如果只靠向量相似度去匹配,召回率会很难看。正确做法是把这些条件做成元数据字段,检索时先做元数据过滤,再做向量检索。
我在项目里定义的元数据字段包括:矿区名称、报告类型(普查/详查/勘探)、编写年份、坐标系统(西安80/北京54/CGCS2000)、矿种、数据来源类型。这些字段在清洗阶段就要从文档内容或文件路径中抽取出来,注入每个切片。
抽取方式分两种:规则抽取和模型抽取。规则抽取适合格式固定的数据,比如从文件路径/data/XX矿区/2023/详查报告.docx里直接解析出矿区和年份。模型抽取适合从正文里识别矿种和坐标系统,用一个轻量NER模型就能搞定,不需要上大模型。
3. TXT数据清洗:从乱码到规整记录
3.1 编码检测的坑与多级回退方案
TXT文件在探矿业务里主要承载化探数据、钻孔编录简表、坐标点文件。这些文件的编码之混乱,超出一般开发者的想象。我遇到过同一个目录下,有的文件是UTF-8,有的是GBK,还有几个是GB2312和UTF-8混着来的——前半部分GBK,后半部分UTF-8,因为文件被不同工具先后编辑过。
chardet库是起点,但不能全信。它的检测准确率在短文本上很差,一个只有几十行的坐标文件,它可能给出三四种可能编码,置信度都不到0.7。我的做法是多级回退:
import chardet def detect_and_read(filepath): with open(filepath, 'rb') as f: raw = f.read() # 第一级:chardet检测 result = chardet.detect(raw) encoding = result['encoding'] confidence = result['confidence'] # 第二级:置信度不足时,按优先级尝试 if confidence < 0.8 or encoding is None: for enc in ['utf-8', 'gbk', 'gb2312', 'gb18030', 'latin-1']: try: text = raw.decode(enc) # 第三级:用中文字符占比验证 chinese_ratio = sum(1 for c in text if '\u4e00' <= c <= '\u9fff') / max(len(text), 1) if chinese_ratio > 0.1 or enc == 'utf-8': return text, enc except UnicodeDecodeError: continue return raw.decode(encoding, errors='replace'), encoding这里的关键是第三级验证:用中文字符占比来判断解码是否正确。GBK编码的中文文件用UTF-8解,会出现大量乱码字符,中文字符占比会异常低。反过来,UTF-8文件用GBK解,也会产生乱码。这个方法比单纯看chardet置信度可靠得多。
实操心得:
gb18030是GBK的超集,兼容性最好,放在回退链里能兜住绝大多数国标编码文件。但不要把它放在第一位,因为gb18030解码几乎不会报错,会把真正的UTF-8文件也“成功”解码成乱码。
3.2 分隔符统一与记录规整
编码解决之后,下一个问题是分隔符。化探数据常见的分隔符有逗号、制表符、空格、分号,甚至还有用竖线的。更麻烦的是,有些文件用多个空格对齐列,有些用制表符,有些混用。
我的处理策略是先探测再统一。统计每行中各种候选分隔符的出现次数,选择出现次数最稳定(方差最小)的那个作为主分隔符。然后按主分隔符切分,对每列做去空格处理。
def normalize_delimiter(lines): candidates = [',', '\t', ';', '|', ' '] scores = {} for sep in candidates: counts = [line.count(sep) for line in lines if line.strip()] if not counts: continue # 用众数出现频率和方差评估稳定性 from statistics import mode, variance try: m = mode(counts) v = variance(counts) if len(counts) > 1 else 0 scores[sep] = (counts.count(m) / len(counts), -v) except: continue best_sep = max(scores, key=lambda k: scores[k]) if scores else ',' return best_sep选好分隔符后,还要处理列数不一致的问题。探矿数据里经常出现某几行列数多一个或少一个,原因是数据录入时多打了分隔符或者漏了字段。我的做法是:以众数列数为准,多出的列合并到最后一列,缺少的列补空值,同时在元数据里标记“该文件存在列数异常”,供后续人工复核。
3.3 坐标与数值数据的特殊处理
探矿TXT里大量出现坐标和品位数值,这些数据的清洗有特殊要求。
坐标数据常见格式有度分秒(113°45'30")、十进制(113.7583)、带带号的平面坐标(38500000, 2850000)。RAG检索时,用户可能用任意一种格式查询,所以清洗阶段要统一转成十进制并保留原始格式作为元数据。度分秒转十进制的公式是度 + 分/60 + 秒/3600,注意西经和南纬要取负值。
品位数值要注意单位和有效数字。有的报告用百分比(0.85%),有的用ppm(8500),有的用g/t。清洗时要统一转成ppm或百分比,并在切片里同时保留原始表述。比如“铜品位0.85%”这个切片,元数据里存cu_grade_ppm: 8500,文本里保留“0.85%”,这样无论用户怎么查都能命中。
注意:数值转换一定要做范围校验。我遇到过把“0.85”当成85%的bug,原因是代码里漏了除以100。探矿数据的数值范围有行业常识,铜品位超过10%就极其罕见,超过这个阈值要触发告警而不是静默通过。
4. Word文档清洗:表格、公式与版本兼容
4.1 python-docx的能力边界与补充方案
python-docx是处理Word文档的首选库,但它有几个明确的边界:不能处理.doc老格式,对文本框和艺术字支持有限,对公式对象只能拿到OMML原始XML。探矿报告里这三种情况都很常见。
.doc格式的解决方案是先用LibreOffice做批量转换,命令行一行搞定:
libreoffice --headless --convert-to docx --outdir ./converted ./raw_docs/*.doc这个转换在Linux服务器上跑很稳,但要注意中文字体问题。如果服务器没装中文字体,转换出来的文档可能丢字。装一个fonts-noto-cjk包就能解决。
文本框里的内容python-docx默认读不到,需要遍历document.element.body下的所有w:txbxContent节点。这个操作稍微底层一些,但能捞回不少被遗漏的文字——探矿报告里经常用文本框放图例说明和注意事项。
4.2 表格抽取:探矿报告的核心资产
Word表格是探矿报告里信息密度最高的部分。钻孔编录表、样品分析结果表、储量估算表,这些表格如果抽取不好,整个RAG系统的价值就丢了一半。
python-docx的表格API能拿到行列结构,但合并单元格是最大的坑。一个跨三行的表头单元格,在API里会在三行里各出现一次,直接遍历会导致数据重复。我的处理方式是检测单元格的_tc元素是否有vMerge或gridSpan属性,据此重建真实的表格结构。
def extract_table(table): rows = [] for row in table.rows: cells = [] for cell in row.cells: # 检测合并单元格 tc = cell._tc tcPr = tc.tcPr is_merge = False if tcPr is not None: if tcPr.find(qn('w:vMerge')) is not None: is_merge = True if tcPr.find(qn('w:gridSpan')) is not None: is_merge = True cells.append({ 'text': cell.text.strip(), 'is_merge': is_merge }) rows.append(cells) return rows抽取出来的表格,我建议同时保留两种形式:一种是Markdown表格,供人阅读和向量化;一种是JSON结构,供精确查询。Markdown表格在切片时作为一个整体,不要拆散,否则“铜品位”和“0.85”可能被切到不同切片里,检索时匹配不上。
4.3 公式与特殊对象的处理
探矿报告里的公式主要是品位计算公式、储量估算公式、坐标转换公式。Word里的公式对象是OMML格式,python-docx只能拿到XML。直接把这个XML转成文本会得到一堆标签,没有检索价值。
我的做法是用XSLT把OMML转成LaTeX,然后保留LaTeX文本。LibreOffice自带这个转换能力,也可以用mathml2latex这类库。转换后的LaTeX文本虽然用户不会直接搜,但可以作为元数据存储,当用户搜索“储量计算公式”时能命中。
实操心得:公式转换不要追求100%准确,能达到80%的可用性就够了。关键是不要因为公式转换失败而中断整个文档的清洗。用try-except包住,失败就跳过,记录日志,后续人工处理。
4.4 版本兼容与批量处理
探矿单位的Word文档版本跨度极大,从Word 97到Office 365都有。除了.doc转.docx,还要注意.docx内部格式的差异。有些老版本生成的.docx,python-docx打开会报错,原因是XML命名空间不标准。
解决方案是用lxml做一次预处理,修复命名空间声明。或者更简单粗暴:用LibreOffice把所有文档统一转一遍,不管原来是.doc还是.docx,都转成标准.docx。这个转换会损失一些格式细节,但对文本和表格内容没有影响,反而能规避大量兼容性问题。
批量处理的流水线我建议用concurrent.futures做并行,但要注意LibreOffice的headless模式不能并发太多实例,一般控制在CPU核数的一半左右。我试过开16个并发,结果LibreOffice进程互相抢锁,反而比串行还慢。
5. PDF解析:扫描件、双栏与表格的三重挑战
5.1 文本层质量评估:决定走哪条路
PDF在探矿业务里分两类:原生PDF和扫描PDF。原生PDF有文本层,可以直接抽取;扫描PDF是图片,必须走OCR。但实际情况比这复杂——有些PDF是混合的,前几页是原生文本,后面附的图件是扫描的;有些PDF有文本层但质量极差,是OCR软件生成的,错字连篇。
所以第一步是文本层质量评估。我用PyMuPDF打开PDF,抽取前几页的文本,计算三个指标:字符总数、中文字符占比、乱码字符占比。如果字符总数太少(比如每页不到50个字符),说明是扫描件;如果乱码字符占比超过10%,说明文本层质量差,也要走OCR。
import fitz def assess_pdf_quality(pdf_path, sample_pages=5): doc = fitz.open(pdf_path) total_chars = 0 chinese_chars = 0 garbage_chars = 0 for i in range(min(sample_pages, len(doc))): text = doc[i].get_text() total_chars += len(text) chinese_chars += sum(1 for c in text if '\u4e00' <= c <= '\u9fff') garbage_chars += sum(1 for c in text if c in '\ufffd\u25a1\u25a0') doc.close() if total_chars < 50 * sample_pages: return 'scanned' if garbage_chars / max(total_chars, 1) > 0.1: return 'low_quality' return 'native'5.2 扫描件OCR:精度与速度的平衡
扫描件必须走OCR,但OCR的精度和速度是一对矛盾。我试过Tesseract、PaddleOCR和几个商业API,最终在探矿场景下选择了PaddleOCR,原因是它对中文和表格的识别效果明显好于Tesseract,而且可以本地部署,数据不出内网。
OCR之前要做图像预处理:灰度化、二值化、去噪、纠偏。探矿报告的扫描件经常有倾斜,不纠偏的话OCR精度会下降20%以上。PyMuPDF可以把PDF页面渲染成图片,然后用OpenCV做预处理。
import cv2 import numpy as np def preprocess_image(img): gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 自适应二值化 binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) # 去噪 denoised = cv2.medianBlur(binary, 3) # 纠偏 coords = np.column_stack(np.where(denoised > 0)) angle = cv2.minAreaRect(coords)[-1] if angle < -45: angle = 90 + angle if abs(angle) > 0.5: h, w = denoised.shape M = cv2.getRotationMatrix2D((w//2, h//2), angle, 1.0) denoised = cv2.warpAffine(denoised, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) return denoisedOCR的精度还取决于分辨率。PyMuPDF渲染时用zoom=3(约216 DPI)效果最好,再高收益递减,再低精度明显下降。这个参数是我对比了十几种组合后确定的,可以直接用。
5.3 双栏排版与阅读顺序还原
地质报告大量采用双栏排版,PyMuPDF默认按坐标顺序抽取文本,会把左右两栏交错在一起,读起来是“左栏第一行、右栏第一行、左栏第二行、右栏第二行”这种混乱顺序。
解决方案是基于坐标的栏检测。先获取页面上所有文本块的边界框,然后用聚类算法把文本块分成左右两组,分别按纵坐标排序,最后左栏全部在前、右栏全部在后拼接。
def detect_columns(page): blocks = page.get_text('blocks') if not blocks: return [] # 按x坐标聚类 x_coords = [b[0] for b in blocks] mid_x = page.rect.width / 2 left_blocks = [b for b in blocks if b[0] < mid_x] right_blocks = [b for b in blocks if b[0] >= mid_x] # 如果右栏内容太少,可能是单栏 if len(right_blocks) < len(blocks) * 0.2: return sorted(blocks, key=lambda b: (b[1], b[0])) left_sorted = sorted(left_blocks, key=lambda b: b[1]) right_sorted = sorted(right_blocks, key=lambda b: b[1]) return left_sorted + right_sorted这个算法对标准双栏有效,对三栏或不规则排版需要调整。探矿报告里偶尔有跨栏的表格或图件,这些块的宽度会横跨中线,需要单独识别并放在正确位置。
5.4 PDF表格抽取:从线条到结构
PDF表格抽取比Word表格难得多,因为PDF没有“表格”这个概念,只有线条和文字。表格抽取的本质是从线条和文字位置反推表格结构。
我用的方案是pdfplumber的表格抽取功能,它基于线条检测。但探矿报告里很多表格没有完整线条,只有横线没有竖线,或者干脆没有线条只有对齐的文字。这种情况pdfplumber会失效。
回退方案是基于文字对齐的表格重建。获取所有文字的坐标,按y坐标分组(同一行的文字y坐标接近),然后在每组内按x坐标排序,根据x坐标的间隔判断列边界。这个方法对规整的表格有效,对合并单元格和错位表格需要额外处理。
注意:PDF表格抽取的结果一定要人工抽检。我见过把两列数据合并成一列的bug,原因是两列之间的间距刚好和列内文字间距一样。抽检比例建议不低于5%,重点检查数值列。
6. 网页数据清洗:从噪声中提取矿权信息
6.1 正文抽取:干掉导航栏和广告
探矿业务需要的网页数据主要是矿权公示、地质资料目录、行业标准页面。这些页面的正文占比往往不到30%,其余是导航栏、侧边栏、页脚、相关推荐。如果不清洗,检索“XX矿权出让”会命中一堆导航链接。
正文抽取算法我用过三种:基于密度的(如readability)、基于文本比例的、基于机器学习的。在探矿场景下,基于密度的readability-lxml效果最稳定,因为它对中文页面的适配经过大量验证。
from readability import Document import requests def extract_main_content(url): resp = requests.get(url, timeout=10) resp.encoding = resp.apparent_encoding doc = Document(resp.text) content = doc.summary() # 进一步清洗HTML标签 from lxml.html import fromstring tree = fromstring(content) text = tree.text_content() return text.strip()readability的局限是它对表格的处理不好,会把表格拍扁。矿权公示页面里的表格恰恰是核心信息,所以需要先抽取表格再抽取正文,两者合并。
6.2 动态渲染页面的处理
越来越多的矿权公示页面用JavaScript动态渲染,requests拿到的HTML里没有数据。这种情况需要上无头浏览器。playwright是目前最稳的选择,比selenium快,API也更现代。
from playwright.sync_api import sync_playwright def fetch_dynamic_page(url): with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto(url, wait_until='networkidle') # 等待表格加载 page.wait_for_selector('table', timeout=10000) content = page.content() browser.close() return contentwait_until='networkidle'是关键参数,它等待网络请求静默后再返回,能兜住大多数异步加载。但有些页面有轮询请求,永远不会networkidle,这时候要改用wait_for_selector加超时。
实操心得:无头浏览器很吃内存,批量抓取时一定要控制并发数,并且每个页面处理完就关闭context。我试过不关context,跑了几百个页面后内存爆了,进程被系统杀掉。
6.3 网页表格与结构化数据抽取
矿权公示表格通常是标准HTML表格,用pandas.read_html就能抽取。但要注意表头合并和多级表头的情况,pandas默认会把多级表头拍成一行,需要手动处理。
import pandas as pd def extract_html_tables(html): tables = pd.read_html(html) results = [] for df in tables: # 清洗:去掉全空行全空列 df = df.dropna(how='all').dropna(axis=1, how='all') if df.empty: continue # 转成Markdown md = df.to_markdown(index=False) results.append(md) return results抽取出来的表格同样要保留Markdown和JSON两种形式。Markdown用于向量化,JSON用于精确查询。矿权公示里的坐标、面积、矿种这些字段,做成JSON后可以直接做结构化过滤。
7. 切片策略与向量化:让检索精度上一个台阶
7.1 分源切片参数设计
切片是RAG里最容易被忽视但影响最大的环节。通用方案建议512 token固定切片,但在探矿业务里这个参数会让表格和章节结构全丢。
我的分源切片策略如下表:
| 数据源 | 切片方式 | 切片大小 | 重叠 | 特殊处理 |
|---|---|---|---|---|
| TXT化探数据 | 按记录块 | 10-20行 | 2行 | 表头注入每个切片 |
| Word报告 | 按章节 | 300-500字 | 50字 | 表格独立成片 |
| PDF正文 | 按段落 | 400-600字 | 80字 | 跨页段落合并 |
| PDF表格 | 整表 | 不限 | 无 | 超长表格按行切 |
| 网页正文 | 按语义块 | 300-500字 | 50字 | 标题注入切片 |
关键原则是:表格不拆散,章节不跨级,数值不孤立。一个包含“铜品位0.85%”的表格行,必须和表头“样品编号、Cu(%)、品位”在同一个切片里,否则检索时匹配不上。
7.2 元数据注入与混合检索
每个切片除了文本内容,还要注入元数据。元数据的来源有三个:文件路径解析、文档内容抽取、人工标注。我建议在清洗阶段就把元数据准备好,切片时直接带上。
检索时用混合检索:先用元数据做过滤(比如限定矿区为“XX矿区”、年份为“2020-2023”),再做向量相似度检索,最后用BM25做关键词补充。这个组合在探矿场景下的召回率比纯向量检索高30%以上。
def hybrid_search(query, filters, top_k=10): # 元数据过滤 candidate_ids = metadata_filter(filters) # 向量检索 vector_results = vector_search(query, candidate_ids, top_k * 2) # BM25补充 bm25_results = bm25_search(query, candidate_ids, top_k * 2) # 融合排序 merged = reciprocal_rank_fusion(vector_results, bm25_results) return merged[:top_k]7.3 常见检索失败模式与修复
即使清洗做得再好,检索也会出问题。我整理了探矿RAG里最常见的四种失败模式和对策。
| 失败模式 | 典型表现 | 根因 | 修复方案 |
|---|---|---|---|
| 数值匹配不上 | 搜“0.85%”找不到 | 切片里是“0.85”没有百分号 | 数值归一化+多形式存储 |
| 表格信息丢失 | 搜“钻孔编号ZK001”无结果 | 表格被拆散 | 表格整片+表头注入 |
| 跨页段落断裂 | 搜完整句子无结果 | 段落被页边界切断 | 跨页合并+重叠切片 |
| 同义词不匹配 | 搜“铜矿”找不到“Cu矿床” | 向量模型对专业术语不敏感 | 同义词词典+查询扩展 |
实操心得:数值匹配问题最隐蔽,因为开发时用文本查询测试发现不了。一定要用真实业务查询做回归测试,比如“XX矿区铜品位大于0.5%的样品有哪些”,这种查询会同时触发数值匹配和元数据过滤。
8. 常见问题与排查技巧实录
8.1 编码类问题速查
编码问题是TXT清洗里最高频的故障。下面这张表是我从实际故障记录里整理出来的,覆盖了90%以上的场景。
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 中文全是问号 | 用latin-1解码了GBK | 检查chardet结果 | 改用gb18030 |
| 部分中文乱码 | 混合编码 | 分段检测编码 | 分段解码后拼接 |
| 文件读取报错 | 含BOM头 | 检查文件头字节 | 用utf-8-sig |
| 特殊符号丢失 | 编码不支持 | 对比原始字节 | 用errors='replace' |
BOM头是个经典坑。UTF-8 BOM是EF BB BF三个字节,用utf-8解码会在文本开头多一个\ufeff字符。这个字符如果进了切片,会影响检索匹配。解决方案是用utf-8-sig编码打开,Python会自动去掉BOM。
8.2 PDF解析类问题速查
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 抽取文本为空 | 扫描件无文本层 | 检查字符数 | 走OCR流程 |
| 文字顺序混乱 | 双栏排版 | 检查文本块坐标 | 栏检测+重排序 |
| 表格串列 | 列间距过小 | 可视化检查 | 调整列检测阈值 |
| OCR错字多 | 图像质量差 | 检查渲染DPI | 提高zoom到3 |
OCR错字在探矿场景下特别危险,因为矿种名称和化学元素符号错一个字,检索就完全失效。我的做法是建立矿种和元素的纠错词典,OCR后做一次规则纠错。比如“铜”被识别成“同”,“锌”被识别成“辛”,这些高频错误直接替换。
8.3 切片与检索类问题速查
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 检索结果不相关 | 切片过大 | 检查切片长度 | 缩小切片+增加重叠 |
| 同一内容重复召回 | 切片重叠过多 | 检查重叠比例 | 降低重叠到10% |
| 表格检索不到 | 表格被拆散 | 检查切片边界 | 表格独立成片 |
| 元数据过滤失效 | 元数据缺失 | 检查切片元数据 | 清洗阶段补全 |
切片过大是新手最常犯的错误。512 token的切片在通用场景下没问题,但在探矿报告里,一个切片可能包含三个不同主题的段落,向量化后语义被平均,检索时哪个主题都匹配不好。我的经验是探矿类文本切片控制在300-400字,比通用建议小一些。
8.4 独家避坑清单
最后分享几条从实际项目里用血泪换来的经验,每一条都对应过真实故障。
第一条:永远不要相信文件扩展名。我遇到过.txt文件实际是PDF,.docx文件实际是RTF。清洗流水线的第一步必须是魔数检测,读文件头字节判断真实类型。
第二条:清洗日志比清洗结果更重要。每个文件的清洗过程都要记录:编码检测结果、文本层质量评分、表格抽取数量、切片数量。出问题时,日志是唯一的排查线索。
第三条:小批量验证再全量跑。清洗流水线写完,先拿20个文件跑一遍,人工检查结果。确认无误再全量跑。我见过全量跑了8小时才发现切片参数写错,全部重来的惨剧。
第四条:保留原始文件。清洗后的数据可以重新生成,原始文件丢了就真丢了。所有原始文件做只读备份,清洗输出写到独立目录。
第五条:数值字段做范围校验。探矿数据的数值有行业常识范围,超出范围的数值大概率是解析错误。铜品位0-10%、铅锌品位0-20%、坐标经纬度范围,这些校验规则能拦住大部分解析bug。
第六条:表格抽取结果必须人工抽检。自动化表格抽取的准确率在80-90%之间,剩下10-20%的错误如果进了知识库,会持续产生错误检索结果。抽检比例不低于5%,重点检查数值列和合并单元格。
第七条:向量模型选型要实测。通用中文向量模型在探矿专业术语上的表现差异很大。我实测下来,在矿种、地层、构造这些术语上,领域微调过的模型比通用模型召回率高15-20%。如果没有微调条件,至少要用业务查询做一轮对比测试。
第八条:检索效果要持续监控。知识库上线不是终点。用户查询日志要定期分析,找出零结果查询和高频误匹配,反推清洗和切片的问题。我负责的一个项目,上线三个月后通过日志分析发现“品位”和“品味”的混淆问题,加了一个同义词映射后,相关查询的召回率提升了12%。
这套清洗方案在三个探矿知识库项目里验证过,最大的一个库有40多万个切片,覆盖2000多份报告和10万条化探数据。检索准确率从最初的不到50%提升到85%以上,核心就是清洗环节做扎实了。RAG的上限由数据质量决定,清洗就是拉高这个上限的唯一途径。