前阵子有个刚接触智能体开发的同事问我一个特别朴素的需求:他想把一堆合同 PDF 里的甲方、乙方、金额抓出来,整理成表格。很多人第一反应是“这不就是读文件再调一下模型嘛”,但真正把流程跑通之后才知道,文档解析几乎是所有智能体工作流里最容易被低估的一块地基。我在这个方向上踩过不少坑,也沉淀了一套自己的处理套路,这篇就围绕“agent入门之文档解析”展开,把需求拆解、链路设计、工具选型、问题排查的完整做法都聊一遍。内容以 PDF 为主,兼顾扫描件和 Word,但整体思路是通用的。无论你是正在写第一个知识库问答、自动归档工单,还是想把合同、简历、报销单批量结构化,这篇都值得花十分钟读完,因为它解决的问题不是“能不能听懂”,而是“能不能稳定地拿出可用结果”。
1. 为什么文档解析是智能体入门第一课
1.1 解析定义了智能体看到的“世界”
智能体之所以能代替人做事情,前提是它得先“看懂”材料。这个“看懂”并不是简单地把 PDF 转成 txt,而是要让模型在关键字段、页面关系、表格结构上都拥有清晰上下文。我常打一个比方:如果智能体是一台机器,文档解析就是它的瞳孔,瞳孔要是模糊的,后面编排再漂亮都白搭。
初学智能体的人容易把精力放在 Prompt 编排、工具调度、多轮对话上,但这些环节都是建立在输入质量基础上的。你让智能体判断一份合同是否合规,它连“甲方”和“乙方”在表格中的具体位置都分辨不出来,那后面所有推理都是沙上建塔。所以把文档解析作为入门的第一个练习项目,不是因为它最简单,而是因为它最能暴露问题,并且这些问题都是后面做复杂任务时同样会遇到的。
1.2 入门阶段最容易混淆的:提取字符不等于提取信息
市面上很多 PDF 库都能把文字提取出来,但提取出的纯文本常常是碎的,表格被拍扁、页脚混进正文、多栏 PDF 的阅读顺序完全错乱。这类输出给模型后,模型确实能“读到字”,但很难“读到位”。
我自己测过一个典型场景:一份两页的采购合同,第一页是供应商信息,第二页是物料明细表。纯文本提取后,模型把第一页底部的公司地址和页眉拼在了一起,导致地址解析错误。这就是典型的“有字符、无信息”。
真正的文档解析,应该至少做到三件事:保留版式关系、识别表格结构、输出带来源标注的字段。等到后面你开始做合同审查、简历筛选、票据归档,会发现在字符之上还有关系层,比如合并单元格、跨页表头、条款嵌套。这些结构一旦丢,模型就只能靠猜,结果自然不会稳定。
1.3 模型再强,也扛不住输入和输出的双重不确定
很多人觉得“大模型什么都能读”,这话只说对了一半。大模型确实能处理语义模糊的内容,但它对输入格式非常敏感。同样一段文字,排版整齐、换行明确的版本和完全拍扁的版本,抽取准确率能差出二三十个百分点。
更关键的是输出侧。你用同一个模型解析一百份结构相同的发票,可能每份返回的 JSON 字段都不一样,有时字段名缩写,有时金额带币种符号,有时多出一个你没定义的备注项。如果不给模型强约束,它就会在格式稳定性上拖你的后腿。
所以我在入门阶段就养成了一个习惯:把文档解析当成“有输入规范、有输出规范、有校验机制”的接口来设计,而不是当成本能任务去裸调。下面几部分,就是我在这个接口里总结出来的关键设计。
2. 设计阶段就拆清楚:解析不只是“读文件”
2.1 输入标准化:不同的文档走不同的解析路线
我把常见文档分成三类,每一类都有独立的解析策略,很少用一套代码通吃所有格式。
第一类是文本型 PDF,也就是可以直接选中文字的电子版。这类文档优先用保留版式信息的解析方式,按页读取文字块的坐标和行号,把表格区域单独标记出来。第二类是扫描型 PDF,本质上是图片,必须走 OCR 路线,先做图像预处理,再做文字识别,必要时还要做版面分析。第三类是 Word 或 Excel,这两类反而好处理,Word 可以保留段落和表格结构,Excel 可以直接按行列读二维数组,尽量不要把 Excel 打印成 PDF 再去解析,等于自己给自己制造障碍。
把这个“按类型分桶”的动作搞定,后面所有步骤才有意义。否则你把一张图片丢给文本解析器,得到一堆乱码,还以为是模型出了问题。
2.2 Schema 先行:先定义好“交付物长什么样”
做文档解析之前,我强烈建议先把输出数据结构写死,就像写接口协议一样。比如合同解析,我需要三个字段:甲方全称、乙方全称、含税总金额。每个字段还要写清楚类型、格式、缺失时怎么处理。
这样做的好处非常直接:模型不是靠感觉决定输出什么,而是围绕一个明确的目标去组织推理。以 JSON Schema 为例,我会在 Prompt 里给模型这样一个硬模板:
{ "contract_no": "编号,字符串类型,形如 HT-2025-0001", "party_a": "甲方全称,字符串", "party_b": "乙方全称,字符串", "total_amount": "含税总金额,数字,小数点后两位", "sign_date": "签订日期,格式为 YYYY-MM-DD" }注意,这里不只是给字段名,还要给字段语义和格式约束。模型看到“total_amount 是数字”和看到“total_amount 是含税总金额,数字,小数点后两位”的效果完全不同。后者明显减少输出抖动的频率。
2.3 数据要有“来源”:让每个字段都能回查
这是我做了几个月之后才补上的关键设计。解析结果里的每个字段,最好都附带来源信息,至少包括页码和片段。例如输出里不只有“2025-03-14”,还要有“page 2, line 7”。这样一旦发现某个字段抽取错误,你可以直接定位到原文去核对,而不是对着整份 PDF 从头翻。
这个设计在初期会觉得麻烦,但项目一复杂就非常值钱。我曾处理过一批合同,其中有十份把“签订日期”识别成了“盖章日期”,正是因为输出里保留了页码和行号,才快速定位出模型混淆的规律,然后用 Prompt 修正了它。没有来源信息的情况下,这种排查几乎是盲人摸象。
3. 实操链路:从一份 PDF 到结构化 JSON
3.1 搭建一个“轻流程”而不是“单步骤”
我在实际项目里把解析拆成八个步骤:文件判定、自动分桶、文本提取、OCR 补充、表格修正、组装语义块、模型抽取、结果校验。
这八个步骤合起来,是一条比较稳定的流水线。很多人上来只想写成一行调用,觉得中间过程全部交给模型就行,但实测下来,中间任何一步退化,最终准确率都会明显下降。下面我按实际执行顺序展开讲。
3.2 文件判定与分桶
第一步先判断文件类型。拿到一个文件,我用扩展名加“文件头探测”双重确认。PDF 要区分版式文件和扫描件,判断方法是用库尝试直接提取文本,如果提取出来的字符过少,基本可以认定是扫描版,需要进 OCR 分支。
Word 和 Excel 则直接进原生解析分支。分桶之后,我给每一类文件设置不同的默认参数,比如扫描件统一先做“灰度化 + 放大处理”,避免图像分辨率过低而影响识别。
3.3 文本提取与表格修正
以文本型 PDF 为例,我通常会这样处理:
def read_pdf_with_layout(path): pages = [] with open(path, "rb") as f: # 按页解析,保留文本块坐标与行号 for page_index, page in enumerate(extract_pages(f)): raw_lines = extract_lines(page) # 过滤页眉页脚等重复噪音 cleaned_lines = filter_noise(raw_lines, page_index) # 对表格区域做结构化标记 table_blocks = detect_table_blocks(page) pages.append({ "page": page_index + 1, "lines": cleaned_lines, "tables": table_blocks }) return pages真正把表格结构保持住,是解析里比较重要的部分。表格判断不能只靠“看到竖线”,而是要根据文本块的坐标聚类。如果某几个文本块在垂直和水平方向都有对齐关系,就倾向把它们归为一个表格。有了这个标记,后面给模型做数据准备时,可以把表格以二维数组的形式输入。
表格区域(第2页): [["物料名称", "规格", "数量", "单价"], ["钢板", "3mm", "1200", "15.50"], ["螺栓", "M8", "5000", "0.12"]]这类结构化输入,比把表格拍成一行流水文字可靠得多。
3.4 组装“语义块”而不是“整篇文本”
我一直强调不要直接把整本 PDF 塞给模型,原因有两个:一是上下文窗口有限,长文档会稀释关键信息;二是模型对长文本里的位置关系感知很弱,跨页内容容易混淆。
我的做法是按“语义块”组织输入。每一块包含:页码信息、所属章节、文本内容、表格内容。在组装时,把一个表格及其前后的注释合并成一个块。例如:
来源:第2页,采购明细区域 内容:物料名称、规格、数量、单价这样做的好处是,模型可以清楚地知道“这段文字在文档的哪个位置”,并且能很自然地把表格和对应的标题关联起来。
3.5 模型抽取环节:把“任务说明”写得像接口文档
接下来是最重要的一步:构造抽取指令。我会把抽取任务写成下面这种风格:
你是一个文档结构化抽取器,你的任务是从给定的文档语义块中抽取合同关键信息。 要求: 1. 只输出 JSON,不要包含任何解释。 2. 字段定义: contract_no: string,合同编号; party_a: string,甲方全称; party_b: string,乙方全称; total_amount: number,含税总金额,仅保留两位小数; sign_date: string,格式 YYYY-MM-DD。 3. 如果某个字段在文本中不存在,输出 null,不要推测。 4. 每个字段必须引用页码作为证据,格式为 "evidence_page": 数字。 5. 除非原文明确出现,否则不要使用当前日期作为默认值。这里有一条我每次都要写的规则:“不要推测缺失字段”。模型天生有补全倾向,它会把没出现的信息脑补出来。这条规则能大幅减少误报。
抽取结果示例:
{ "contract_no": "HT-2025-001", "party_a": "某科技有限公司", "party_b": "某供应链管理有限公司", "total_amount": 158000.50, "sign_date": "2025-03-14", "evidence_page": 1 }3.6 结果校验:把“看起来对”变成“可验证”
拿到模型的 JSON 后,不要直接入库。我通常做三层校验:
第一层,格式校验,确认每个字段的类型是否正确,金额字段不能出现字符串;第二层,业务规则校验,比如总金额是否等于单价乘以数量,日期是否在合理时间范围内;第三层,证据链校验,用返回的页码去原文里回查,看对应位置是否真的包含该字段。
第三层需要额外编写逻辑,却是最有效的手段。如果模型给了“evidence_page: 1”,但第 1 页根本没有“HT-2025-001”这个字符串,就可以判定这条结果不可信,从而触发重新解析。这套机制虽然朴素,却能把错误率再压下去不少。
4. 高频翻车点与排查心得
4.1 扫描件乱码率居高不下
扫描件解析的翻车率通常远高于文本型 PDF。最常见的原因是图像质量差,倾斜、暗角、低分辨率都会让 OCR 识别率直线下降。
我的解决思路是先放大再识别。把扫描图统一缩放到 200 DPI 以上,做指定角度的预矫正。这个步骤看起来无关紧要,实际上能让中文识别的准确率提升很多。遇到印章压在文字上的情况,还可以尝试把背景色分离出来,减少噪声。这里有个很容易忽略的点:OCR 的字典要尽量贴近业务场景,让它更适应合同、发票里出现的常见词汇,切词时会更有优势。
4.2 多栏 PDF 的阅读顺序错乱
排过论文格式的人大概知道,双栏排版在 PDF 转文本时,容易把左栏下半部分和右栏上半部分混在一起。第二次解析多栏文件时,我才意识到必须依靠坐标切割,而不是依赖库的默认文本流。
具体操作上,我按页面宽度对文本块做聚类,把同属一栏的文本重新组合。这个工作可以写成一个简单的“栏切分函数”,核心逻辑是:按文本块左上角 x 坐标排序,如果相邻块的 x 坐标差超过一定阈值,则判定换栏。这听着粗略,实际用起来却很稳定,尤其适合论文、报告、杂志排版。
4.3 表格跨页后“丢了表头”
长表格经常会跨页,第二页的表格如果没有表头,模型就很难判断列名,后续抽取自然出错。
我的处理方式是把第一页的表头记录下来,并复制到跨页表格的前面,保证模型每次都能看到完整的列结构。这里需要注意一个细节:跨页表头不能强加到所有页面上。如果某页的表格列数明显少于上一页,且确认是表格延续,才需要插入表头,否则反而会产生干扰。
4.4 JSON 输出被截断或“强行补全”
长文档抽取时,如果输出字段特别多,模型偶尔会返回一段不完整的 JSON,末尾直接断开,或者自己补一段内容让 JSON 看起来完整。
对付截断,我的办法是让模型按“块”输出,每个块只负责一部分字段。比如合同解析,可以拆成“基础信息块”“金额明细块”“签署条款块”三个子任务,分别输出独立的 JSON,最后由代码合并。这样每个块的输出都短小完整,截断概率大幅降低。另一个技巧是要求模型在正式输出前先输出一个“完成标志”,比如先写一句 “JSON_START”,代码层检查到这个标记才开始解析内容,可以有效避开模型在开头添加解释性文字。
4.5 日期和金额的格式陷阱
日期格式是最容易“阴沟翻船”的字段。合同上可能出现“2025年3月14日”“2025/3/14”“Mar. 14, 2025”等多种写法,模型输出时也可能给你加个“约”字。
我在 Schema 里强制要求“YYYY-MM-DD”,但这还不够,因为在抽取阶段模型把它当成文本提取,并不会自动转换。所以我在代码里加了一道后置格式化函数,统一处理日期和金额转换。金额某些情况是“壹佰伍拾捌圆整”这类大写中文,这种情况需要专门的转换映射,不能指望模型自己理解成数字。我的原则是:能用规则函数处理的,绝不留给模型发挥。
5. 工具选型与质量评估速查表
5.1 按文档类型选择主方案
我根据自己的使用习惯,整理了一张速查表,可以直接套用:
| 文档类型 | 首选方案 | 备选方案 | 使用注意点 |
|---|---|---|---|
| 文本型 PDF | 布局保留解析 | PDF 转纯文本 | 留意多栏、页眉页脚 |
| 扫描型 PDF | 图像预处理 + OCR | OCR + 版式分析 | 先修正倾斜和分辨率 |
| Word 文档 | 原生结构读取 | 转换为纯文本 | 注意嵌套表格 |
| Excel 表格 | 表格读写组件 | 转换为 CSV 再处理 | 保留表头和数据行关系 |
工具不是越贵越好,我个人的经验是:先把“结构读取”和“语义抽取”两层分开。文本提取层追求“保结构”,模型抽取层追求“懂语义”,两者解耦后,替换任何一层都更灵活。
5.2 字段质量如何评估
判断解析做得好不好,不能只看“一两个例子很准”。我给自己定了三个核心指标:
第一个是字段存在率,统计被正确提取的字段占总字段数的比例;第二个是格式准确率,检查所有成功提取字段是否满足预设格式要求;第三个是证据回查率,随机抽若干条结果,返回原文查看是否真的存在该信息。
实践下来,一个合格的解析流程至少应该保证字段存在率在 90% 以上,格式准确率接近 100%。如果存在率不达标,先查输入质量;如果格式准确率低,先查 Schema 和输出约束。
5.3 建立一个“回归测试集”
文档解析的难点之一,是修好一个 Bug 反而弄坏另一个场景。我的做法是把每一类处理过的文件抽出一部分,构成一个固定的回归测试集,每次改动后必须全量跑一遍。
这类测试集不需要很大,几十份样本就能覆盖绝大多数典型问题。重点要包含几个难啃的样本:多栏论文、带合并单元格的报表、倾斜扫描件、跨页长表格。长期维护这套样本集,能让你的解析流程越用越稳。
6. 最后再分享几点容易被忽视的实操体会
第一,做解析前先看数据,而不是先写代码。我刚开始做合同解析时,急着把代码写好,结果发现样本里有一半是扫描件,另一半是双栏排版,策略完全不同。后来我先花时间翻了几十份原始文档,把版式类型和字段写法都记录下来,再动手拆分流程,效率反而更高。
第二,错误处理要比正常路径多花一倍心思。解析失败不等于任务结束,我习惯把解析结果分为“成功”“低置信度”“失败”三档。低置信度的结果不直接入库,而是进入人工复核队列,让后一步的操作人员快速确认。这个设计很朴素,却能大幅提升整体可用性,因为模型总会遇到没见过的新版式。
第三,把解析能力封装成独立服务,不要跟业务流程写死在一起。解析模块应该有独立的输入输出接口,方便后续被不同工作流调用。我在完成第一批解析后,把流程封装成了一个内部工具,后来陆续接入知识库问答、工单自动归档等好几个场景,全靠当初留下的接口边界。
文档解析这个方向,入门成本其实不高,但要做出稳定可用的效果,需要耐心打磨细节。我前后迭代了很多版,最终沉淀下来的核心经验,就是今天聊的这套链路:分桶输入、Schema 约束、证据回查、错误兜底。如果你正要开始做智能体方向的内容,建议先拿一批真实文档练手,把这一步走扎实再往上游走,后面的路会顺畅很多。