智能体开发入门:文档解析如何从PDF稳定输出结构化数据

发布时间:2026/10/20 15:59:20
智能体开发入门:文档解析如何从PDF稳定输出结构化数据
前阵子有个刚接触智能体开发的同事问我一个特别朴素的需求他想把一堆合同 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 到结构化 JSON3.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图像预处理 OCROCR 版式分析先修正倾斜和分辨率Word 文档原生结构读取转换为纯文本注意嵌套表格Excel 表格表格读写组件转换为 CSV 再处理保留表头和数据行关系工具不是越贵越好我个人的经验是先把“结构读取”和“语义抽取”两层分开。文本提取层追求“保结构”模型抽取层追求“懂语义”两者解耦后替换任何一层都更灵活。5.2 字段质量如何评估判断解析做得好不好不能只看“一两个例子很准”。我给自己定了三个核心指标第一个是字段存在率统计被正确提取的字段占总字段数的比例第二个是格式准确率检查所有成功提取字段是否满足预设格式要求第三个是证据回查率随机抽若干条结果返回原文查看是否真的存在该信息。实践下来一个合格的解析流程至少应该保证字段存在率在 90% 以上格式准确率接近 100%。如果存在率不达标先查输入质量如果格式准确率低先查 Schema 和输出约束。5.3 建立一个“回归测试集”文档解析的难点之一是修好一个 Bug 反而弄坏另一个场景。我的做法是把每一类处理过的文件抽出一部分构成一个固定的回归测试集每次改动后必须全量跑一遍。这类测试集不需要很大几十份样本就能覆盖绝大多数典型问题。重点要包含几个难啃的样本多栏论文、带合并单元格的报表、倾斜扫描件、跨页长表格。长期维护这套样本集能让你的解析流程越用越稳。6. 最后再分享几点容易被忽视的实操体会第一做解析前先看数据而不是先写代码。我刚开始做合同解析时急着把代码写好结果发现样本里有一半是扫描件另一半是双栏排版策略完全不同。后来我先花时间翻了几十份原始文档把版式类型和字段写法都记录下来再动手拆分流程效率反而更高。第二错误处理要比正常路径多花一倍心思。解析失败不等于任务结束我习惯把解析结果分为“成功”“低置信度”“失败”三档。低置信度的结果不直接入库而是进入人工复核队列让后一步的操作人员快速确认。这个设计很朴素却能大幅提升整体可用性因为模型总会遇到没见过的新版式。第三把解析能力封装成独立服务不要跟业务流程写死在一起。解析模块应该有独立的输入输出接口方便后续被不同工作流调用。我在完成第一批解析后把流程封装成了一个内部工具后来陆续接入知识库问答、工单自动归档等好几个场景全靠当初留下的接口边界。文档解析这个方向入门成本其实不高但要做出稳定可用的效果需要耐心打磨细节。我前后迭代了很多版最终沉淀下来的核心经验就是今天聊的这套链路分桶输入、Schema 约束、证据回查、错误兜底。如果你正要开始做智能体方向的内容建议先拿一批真实文档练手把这一步走扎实再往上游走后面的路会顺畅很多。