如何使用AI从复杂文档中准确提取结构化数据
作者:AI拉呱(Errol Yan)
定位:AI领域深度内容与实战方法分享
让模型在提取前先分析布局,一切将截然不同
TL;DR:
基于AI的文档提取通常涉及两次独立的LLM调用:文档解析和使用schema进行结构化提取。然而,这个流程在处理具有复杂布局的文档时可能会丢失重要的视觉上下文。在本文中,我设计了一个视觉提取器,通过使用专业提示词、schema生成器和schema合规提取,将解析和结构化提取合并为一次调用。该视觉提取器支持从视觉复杂的文档中进行高精度提取,包括表格、多页结构、扫描表单、蓝图以及传统解析可能丢失上下文或误导提取的布局。它还生成字段级置信度分数和推理依据,以支持人工审核。
使用大型语言模型(LLM)进行结构化数据提取在企业中的需求越来越高。在芬兰AI区域(FAIR)项目的AI咨询工作中,我见证了各种各样有趣的应用,例如分析供应商报价并比较项目价格、安全观察报告、从多个来源自动生成报告、提取设备检查记录和维护日志、从医疗表格中提取临床信息,以及处理人力资源文档(求职申请、简历)等等。
结构化信息提取的典型工作流程包括两阶段管道:文档解析和schema合规提取。大多数业务文档包含复杂的布局,如多级表格标题、合并单元格、跨页分割的行、旁注、图表、印章和视觉线索,简单的解析器无法准确保留这些信息。
如果解析器将文档扁平化为纯文本,下游提取器可能会产生不正确的输出。
在我之前的文章中,我通过使用LlamaParse在最近研究中发布的提示词来构建多模态解析器,解决了解析问题。
这个解析器解决了我在处理混乱布局时遇到的问题。然而,我的目标不仅仅是准确解析,而是从解析内容中提取结构化信息,这需要另一次LLM调用。
在两阶段管道中,提取器依赖于解析器的表示而不是原始文档。如果解析器误解了文档布局,提取器可能会静默失败。
在本文中,我设计了一个视觉提取器,将解析和结构化提取合并为一次LLM调用,使用专门的提示词来调用LLM对提取目标的空间推理,以及执行schema生成和验证的提取器。该视觉提取器从具有复杂视觉布局的文档中进行高精度提取。
这提高了准确性,减少了LLM调用次数,并降低了有损解析误导提取器的可能性。
视觉提取器接受用户的自然语言提取需求,并使用schema生成器构建可重用的schema,指导LLM提取什么内容以及输出应该是什么形状。
视觉提取器还生成字段级置信度分数和推理依据,以实现人工审核,快速验证低置信度信息,而无需重新检查整个文档。
为了说明这一点,请参见下面的建筑蓝图。我遇到了几家公司对从建筑蓝图中提取信息进行自动合规检查和其他下游任务感兴趣。
建筑蓝图
我们希望使用以下提取需求从图纸中提取某些字段。
user_requirements = """
从建筑蓝图中提取以下关键字段集。
• 项目地址 • 图纸标题 • 图纸编号(图纸标识符,如B-354) • 图号 • 项目编号 • 绘图比例 • 绘图日期(格式:DD/MM/YYYY) • 绘图类型(选择:基础平面图、楼层平面图、剖面图、场地平面图、其他) • 绘图状态(选择:发布审核、发布施工、修订、其他) • 主要绘图元素(选择:基础墙、混凝土板、基础、下层、其他) • 总基础宽度 • 总基础深度 • 下层标高 • 墙顶标高 • 板厚度 • 基础宽度
注:对于所有尺寸,格式应为显示带单位数字的文本字符串,例如100'-0"
"""
以下是启用include_verification=True时的提取输出示例。视觉提取器在合理的时间和成本内准确提取所有必需字段。完整输出请参见GitHub仓库中的blueprint.json。蓝图可在此GitHub链接获取:blueprint.png。
Model : gpt-5.4-mini
Files : 1
LLM time : 7.753s
Tokens : 4550 total
Cost : $0.006536
Extracted data:
{
...
"drawing_number": {
"value": "A-201",
"confidence_score": 0.99,
"confidence_reason": "图纸编号明确表示为图号标识符。"
},
"sheet_number": {
"value": "04 OF 18",
"confidence_score": 0.98,
"confidence_reason": "图号在右下角明确显示为当前图纸集中的图纸。"
},
...
"top_of_wall_elevation": {
"value": "109'-2"",
"confidence_score": 0.99,
"confidence_reason": "墙顶标高在A剖面中明确标注。"
},
"slab_thickness": {
"value": "4"",
"confidence_score": 0.97,
"confidence_reason": "板剖面注释明确将混凝土板标注为4英寸厚。"
},
"footing_width": {
"value": "24"",
"confidence_score": 0.98,
"confidence_reason": "基础宽度在A剖面中明确标注为24英寸。"
}
}
视觉提取器允许用户选择提供商和模型(OpenAI、Anthropic或Google),以及其他几个选项,包括推理级别(低、中、高),以评估其用例的提取质量。
视觉提取器的源代码以及详细指南和文档可以在我们的开源生成AI工具包中找到(参见GAIK项目):vision_extractor。它需要至少一个提供商(OpenAI、Azure或Google Vertex AI)的API凭证。
结合多模态解析和提取
结构化文档提取的典型工作流程通常涉及首先将文档解析为中间表示,如Markdown、文本或结构化HTML。然后,单独的提取步骤使用此解析表示来提取所需字段并返回schema合规的结构化数据。
两阶段文档提取管道
此管道的一个好处是解析输出可以存储、检查、重用、嵌入或传递给多个下游任务。在许多情况下,这正是您想要的。
但对于某些文档,特别是那些具有复杂视觉布局的文档,分离解析和提取可能会导致解析器在知道提取器确切需要什么之前就决定如何表示文档。然后提取器必须从解析器的表示而不是原始文档中推断答案,这可能导致上下文丢失。
通过在视觉提取器中结合解析和提取,我测试了一种不同的模式,如下图所示。
单遍视觉提取管道
视觉提取器在一次模型调用中结合了解析和提取。此管道保留了布局感知的解析指令,但不要求模型输出解析内容。相反,它要求模型使用相同的内部视觉分析并直接返回结构化数据。
那么,好处是什么?
LLM调用更少,中间token更少,延迟更低,有损解析表示误导提取器的可能性更小。模型可以同时对布局和提取目标进行推理。
与其先解析所有内容,不如让模型在提取任务的指导下阅读文档。
设计提示词
多模态解析器的系统提示词要求LLM将每个布局元素包装在带有边界框和类别标签的<div>标签中输出。模型必须定位每个元素并为其分配边界框和类别。
在组合解析-提取管道中,我不是输出div/bbox分析,而是要求模型在内部执行此操作,然后从该内部理解中提取结构化字段。
OPENAI_SYSTEM_PROMPT = """你是文档解析器和数据提取器。你的任务是阅读文档并提取结构化信息。
指南:
• 保留文档结构,包括标题、段落、列表和表格。 • 使用 <table>、<tr>、<th>和<td>将表格转换为HTML。• 对于文档中现有的表格,使用 colspan和rowspan属性来保留合并单元格和分层标题。• 对于转换为表格的图表或图形,使用扁平组合列标题(例如,"Primary 2015"而不是单独的标题行),以便每个数据单元格的行包含其所有标签。 • 在方括号中简要描述图像和图形,例如: \[Figure: description\]。• 使用适当的语法高亮保留任何代码块。 • 保持阅读顺序:西方文档从左到右,从上到下。 • 不要添加评论或解释。
提取前,通过将每个布局元素标识为包裹在<div>标签中来内部分析文档:
• data-bbox="[x1, y1, x2, y2]"用于归一化0-1000坐标中的边界框,其中x是水平的(左边缘=0,右边缘=1000),y是垂直的(顶部=0,底部=1000)。x1, y1是左上角,x2, y2是右下角。• data-label="<category>"其中类别是以下之一:Caption、Footnote、Formula、List-item、Page-footer、Page-header、Picture、Section-header、Table、Text、Title。
按阅读顺序处理元素。分析每一段内容,就好像它恰好位于一个<div>包装器内。使用此分析来指导您的提取,但仅输出结构化数据。
提取规则:
• 仅从文档中提取明确陈述的信息。 • 切勿发明值。如果值不确定或缺失,请使用提供的schema中定义的字段默认值。如果未定义默认值,则文本字段使用空字符串,数字字段使用null。 • 不要包含解释或额外的键或字段。"""
请参阅prompts.py中的完整提供商特定提示词。
模型仍然执行仔细的布局分析,但此分析不再是最终输出。输出是提取schema所需的结构化对象。
换句话说,解析器的中间表示进入了模型的推理过程。
然后用户提示词插入实际的提取任务:
OPENAI_USER_PROMPT = """附加文档将进行数据分析提取。
解析完整文档,将每个布局元素标识为包裹在
标签中。对任何表格数据使用HTML表格。对于图表和图形,使用扁平组合列标题。
使用您的文档分析,提取以下结构化信息:
{user_requirements}
"""
这就是为什么这个组件对实际业务文档很有用。模型不仅仅读取解析的文本。它使用视觉结构、表格对齐、行边界和标题来决定哪些值属于哪些字段。
Schema解析和验证
视觉提取器不需要用户手动编写Pydantic schema。如果未提供schema,它会调用GAIK的**SchemaGenerator**从自然语言需求生成一个。
Schema生成器读取任务描述并将其分类为三种结构之一:
• **扁平:**每个字段恰好出现一次。例如,酒店预订确认有一个预订参考号、一个客人姓名和一个入住日期。 • **嵌套列表:**整个输出是一个行表,例如测试结果列表,其中每行都有名称、值和状态。 • **父级与嵌套列表:**一些顶级字段出现一次,其他字段每行重复。例如,交货清单有一个承运人名称、一个发货日期和一个起点仓库。但它可能包含多个货物,每个货物都有自己的跟踪号、重量和目的地。
从这个分类中,Schema生成器创建两个工件:一个定义LLM输出结构的Pydantic模型(**schema.py**),和一个存储字段级元数据的**requirements.json**,例如可为空字段、显式默认值、允许的枚举值、日期格式提示以及字段是否需要在输出中。
视觉提取器将schema.py和requirements.json保存为可重用模板。schema解析逻辑在**vision_extractor.py**中。
GAIK的SchemaGenerator从提取需求自动生成schema也在我的一篇文章中解释:
这使得该组件对于重复的业务任务很实用。第一次运行可以从自然语言需求生成schema。以后的运行可以为相同的提取任务重用持久化的schema。
模型返回结果后,组件使用Pydantic验证它,规范化数字占位符,应用字段策略,并返回一个schema合规的对象,可以流入工作流的下一步。
视觉提取器中的schema解析和验证
有关不同类型提取任务的schema生成示例代码,请参见GAIK工具包仓库中的**此链接**。
视觉提取器的工作原理
我将视觉提取管道打包到一个Python类**VisionExtractor**中,它读取一个或多个PDF或图像文件,并允许用户选择提供商和模型(OpenAI、Claude或Google),以及其他几个选项。
完整的代码结构在**GitHub仓库中。VisionExtractor类的完整实现在vision_extractor.py**中。
VisionExtractor只有一个公共方法extract(),用于编排整个管道。
extractor = VisionExtractor(
api_config=None, # 自定义配置字典;如果为None,凭证从.env加载
model_provider="openai", # "openai" | "claude" | "google"
model=None, # 覆盖所选提供商的默认模型;None = 使用提供商默认值
reasoning_effort="medium", # 思考预算:"low" | "medium" | "high"
merge_table=True, # 合并跨页边界的表格
use_azure=True, # 使用Azure OpenAI端点;False = api.openai.com
vertex_ai=True, # 对Google使用Vertex AI端点;False = generativelanguage.googleapis.com
additional_instructions=None, # 附加到用户提示词的领域特定规则的额外文本
include_verification=True, # 为每个提取字段添加confidence_score + confidence_reason
)
result = extractor.extract(
file_paths=
["invoice.pdf"], # 单次调用中发送一个或多个PDF或图像文件
user_requirements="", # 自然语言提取任务
extraction_model=None, # 预构建的Pydantic模型;如果与requirements一起提供,跳过schema生成
requirements=None, # 传递schema时与extraction_model配对的字段元数据
schema_dir="./", # 保存/加载生成的schema的文件夹;在后续运行中重用
)
几个值得理解的参数:
• **reasoning_effort**:模型的思考级别:low、medium或high。更高的努力可以提高复杂布局的准确性,但速度更慢且成本更高。• **merge_table**:当True时,在用户提示词中附加一条指令,告诉模型合并跨页的表格。• **additional_instructions**:附加到用户提示词的用例特定指令。• **include_verification**:当True时,将每个提取字段包装在{value, confidence_score, confidence_reason}中供人工审查。
extract()方法首先解析提取schema。如果schema_dir包含保存的schema,它会加载它。否则,它调用SchemaGenerator,后者解释user_requirements并从中构建一个类型化的Pydantic模型。
然后它将所有文档编码为base64,并在一次LLM调用中将它们与提取任务和schema作为结构化输出约束一起发送。
LLM返回结果后,组件使用Pydantic验证它,规范化日期格式和数字占位符,应用字段策略,并返回一个干净的字典。每次调用都返回一个VisionExtractionResult,其中包含提取的数据以及相关的token消耗和成本。
置信度元数据和人工审核
结构化提取并不总是关于完全自主。在许多文档工作流中,最佳设计是让系统自动处理常规情况,并将不确定的情况升级给人类。这就是我集成到视觉提取器中的人工审核方法。
视觉提取器通过include_verification=True支持此功能。启用时,标量字段使用value、confidence_score和confidence_reason包装。
请参阅vision_extractor.py中的验证辅助工具。
我没有将校准留给模型的判断,而是明确指定了每个分数范围在文档证据方面的含义:
置信度评分规则:
• 0.95-1.00:确定。该值在文档中明确且明确地陈述。 • 0.80-0.94:高。该值得到有力支持,但需要轻微解释,例如日期格式、缩写扩展或标签匹配。 • 0.60-0.79:中等。该值需要非平凡推理、交叉引用章节或解释不完整的标签。 • 0.40-0.59:低。证据薄弱、不完整、部分冲突或受解析不确定性影响。 • 0.01-0.39:非常低。只能从最小证据中做出最佳猜测,或者文档包含矛盾的值或不充分的上下文。
如果confidence_score低于0.50,confidence_reason必须明确解释为什么提取不可靠——例如"发现两个冲突值"、"表格标题不清楚"、"值出现在无关部分"。
这使得分数在文档和文档类型之间保持一致且可解释。发票上的0.85和医疗报告上的0.85意味着相同的事情,即值存在,但模型必须进行一些解释才能提取它。
例如,查看complete_description字段(confidence_score = 0.86)和material_number字段(confidence_score = 0.88)的推理,显示模型必须进行一些推理和解释才能以略低的置信度分数提取这些结果。
...
{
"item_number": {
"value": "0030",
"confidence_score": 0.99,
"confidence_reason": "项目编号在行项目行的开头明确显示。"
},
"complete_description": {
"value": "OFE Copper C10100 H04 (hard) Round 1.0000 (+-.002) X 144 " Mill Length ASTM B 187/B 187M ASTM F 68 Actual Chemistry/Act. Physical Mat.No.: CURD01208",
"confidence_score": 0.86,
"confidence_reason": "描述跨页拆分,但续篇在项目0040开始之前清楚地跟随项目0030。"
},
"quantity": {
"value": 2000,
"confidence_score": 0.98,
"confidence_reason": "数量在行项目行中明确显示为'2,000 LB'。"
},
"price": {
"value": "7.47",
"confidence_score": 0.98,
"confidence_reason": "单价在行项目行中明确显示为'7.47'。"
},
"material_number": {
"value": "CURD01208",
"confidence_score": 0.88,
"confidence_reason": "材料编号在下一页上明确说明为项目0030的续篇。"
}
}
...
有了这些元数据,人工审查员不需要重新阅读整个文档。他们可以首先检查置信度低或证据冲突的字段。如果某个字段的置信度分数低得多(超出某个阈值,取决于用例),并且模型的困惑反映在推理中,人工审查员可以手动检查原始文档中的此字段。
如何使用
我创建了Vision Extractor作为我们GAIK项目开源生成AI工具包的可重用软件组件。此软件组件可以作为独立的Python包安装,如下所示:
pip install "gaik
[vision-extract]"
以下是使用gpt-5.4-mini的最小示例,具有低推理努力和include_verification=True,从单个文档中提取结构化数据。在第一次运行中,生成的schema保存在schema_dir中。
import json
from dotenv import load_dotenv
import json
from gaik.software_components.vision_extractor import VisionExtractor
load_dotenv() # 从同一目录加载.env
extractor = VisionExtractor(
model_provider="openai", # "openai" | "claude" | "google"
use_azure=False,
model="gpt-5.4-mini",
reasoning_effort="low", # "low" | "medium" | "high"
merge_table=True,
include_verification=True,
)
result = extractor.extract(
file_paths=
["document.pdf"],
user_requirements="提取采购订单号、日期、供应商名称和所有行项目。",
schema_dir="./schema_dir", # Schema在第一次运行时保存,在后续运行时重新加载
)
print(json.dumps(result.data, indent=2, default=str, ensure_ascii=False))
有关token消耗和成本计算的详细示例,请参见**vision_extractor_example.py**。
让我们举一个从采购订单中提取数据的例子。该采购订单有一个跨两页的表格。高亮文本表示要提取的各个字段。此采购订单可在GitHub仓库的**此链接**获取。
示例采购订单第1页,显示跨多页拆分的表格中要提取的信息
示例采购订单第2页。每个高亮数据点代表要提取的单独字段
用户需求(提取任务)定义如下:
user_requirements = """
从文档中提取采购订单数据。
顶级字段:
• 采购订单号 • 交货日期(格式:DD/MM/YYYY) • 交货地址(格式:公司名称、街道、邮政编码、城市、国家) • 供应商编号
对于每个行项目,提取:
• 项目编号(例如0100、0200) • 商品代码(内部供应商代码) • 尺寸(横截面如所述,例如"25x8mm") • 产品形式(选择:扁钢、圆钢或方钢) • 材料等级(例如"6061铝"、"316不锈钢"、"C260黄铜") • 标准名称(合金/标准代码,例如"CW024A"、"ASTM B221") • 切割长度(包含单位的文本字符串,例如"2000mm") • 状态或条件(例如"冷拔、光亮表面"、"T6") • 硬度HV(数字,如果说明,否则为null) • 最小弯曲半径(数字,如果说明,否则为null) • 交货长度备注(例如"长度为3000mm",如果说明,否则为null) • 适用标准(例如"ASTM B221"、"EN 755-2",如果说明,否则为null) • 特殊标志(任何剩余代码,例如"XK"、"倒角边缘",否则为null) • 数量(包含单位的文本字符串,例如"8.600 kg")
"""
在第一次运行中,视觉提取器调用GAIK的SchemaGenerator,它将结构识别为parent_with_nested_list并生成Pydantic模型和需求规范。请参见[**PO_schema**](https://github.com/GAIK-project/gaik-toolkit/tree/main/implementation_layer/examples/software_components/vision_extractor/PO_schema)文件夹,其中包含上述示例的schema.py和requirements.json。
使用gpt-5.4-mini模型,reasoning_effort="low"和include_verification=True,以下是结构化输出的示例,显示每个提取字段的置信度分数和推理。完整输出可在**PO_result.json**中找到。
Model : gpt-5.4-mini
Files : 1
LLM time : 18.256s
Tokens : 10736 total
Cost : $0.022182
Extracted data:
{
"purchase_order_number": {
"value": "560471823",
"confidence_score": 1.0,
"confidence_reason": "文档编号在采购订单标题中明确说明为560471823。"
},
"delivery_date": {
"value": "10-07-2025",
"confidence_score": 1.0,
"confidence_reason": "交货日期明确说明为10.07.2025,并重新格式化为DD/MM/YYYY。"
},
...
"line_items":
[
{
"item_number": {
"value": "0010",
"confidence_score": 1.0,
"confidence_reason": "项目编号0010在行项目表中清晰显示。"
},
"article_code": {
"value": "7041832",
"confidence_score": 1.0,
"confidence_reason": "商品代码7041832在项目0010旁边明确列出。"
},
"dimensions": {
"value": "10x80mm",
"confidence_score": 0.98,
"confidence_reason": "尺寸在项目描述中明确说明为10x80mm。"
},
...
}
此结构化输出可以直接输入到下游工作流中,例如创建销售订单。
运行上述提取任务的完整示例在**vision_extractor_example_minimal.py**中。
这是另一个多文档提取的示例。在这个例子中,我们有一个采购订单和一些物料清单。采购订单中的每个项目都有一个带有附加详细信息的物料清单。任务是从采购订单中提取一些顶级字段和项目详细信息,然后在其关联的物料清单中找到每个行项目的附加详细信息。
示例采购订单和物料清单可在此GitHub链接获取。
示例采购订单。高亮文本显示要提取的字段
3个物料清单之一。高亮文本显示要提取的字段
用户需求(提取任务)编写如下:
user_requirements = """
任务是从客户文档(采购订单(PO)和物料清单(BOM))中提取关键字段,并对齐它们,以便每个PO项目都用正确的技术细节丰富。从客户的采购订单开始,其中可能包含多个项目。每个项目通过物料编号链接到其BOM。
对于PO中的每个项目,提取物料编号以及基本项目详细信息:数量、描述和交货日期(格式:DD/MM/YYYY)。使用PO中项目的物料编号查找具有相同物料编号(表示为'ID')的BOM。从匹配的BOM中提取部件的'部件名称'和部件尺寸(长度后跟单位,例如1487mm)。
最终输出应包含与PO中的项目数量相同的行数。每行应包含:物料编号、数量、描述、交货日期(来自PO)、部件名称、部件尺寸(来自BOM)。
此外,从PO中提取以下标题信息:订单日期、买方、销售人员、送货地址、付款条件。
"""
此提取任务可以通过**vision_extractor_example.py运行。用户需求(任务描述)存储在task_multi_doc.txt**中。运行示例后,视觉提取器将首先在schema_multi目录中创建并保存schema,其中将包含此任务的schema.py和requirements.json。
以下是输出示例。
Model : gpt-5.4-mini
Files : 4
LLM time : 7.649s
Tokens : 10064 total
Cost : $0.012205
Extracted data:
{
"order_date": {
"value": "15/03/2025",
"confidence_score": 0.99,
"confidence_reason": "订单日期在PO标题中明确说明,并重新格式化为DD/MM/YYYY。"
},
...
"po_items":
[
{
"material_number": {
"value": "4BGQ312884340",
"confidence_score": 0.99,
"confidence_reason": "物料编号在PO项目行中明确说明。"
},
"quantity": {
"value": "72,00",
"confidence_score": 0.99,
"confidence_reason": "数量在PO项目行中明确说明。"
},
"description": {
"value": "AL Rotor bar / B",
"confidence_score": 0.93,
"confidence_reason": "描述取自匹配PO行正下方的项目文本。"
},
...
}
观察与最终思考
将视觉提取器用于具有复杂布局的文档,例如具有合并单元格的多页文档、具有复杂视觉布局的扫描文档、带注释的蓝图和技术图纸、带照片和手写笔记的文档,以及必须一起读取图形、表格和脚注的文档。
对于布局简单的文档,使用简单解析器(如Docling或PyMuPDF)的标准两阶段管道仍然是合理的选择。单次视觉模型调用比重文本提取调用更重,当布局不包含纯文本会丢失的信息时,这是不必要的。
我发现较小的模型(例如gpt-5.4-mini)在低推理努力下对大多数提取任务工作良好。然而,一些复杂布局可能需要更大的模型(例如gpt-5.4、gpt-5.5或claude-sonnet-4-6)甚至更高的推理努力。
从较小的模型和低推理努力开始,然后根据布局复杂性或准确性要求移动到更大的模型或更高的努力。
关注 AI拉呱
如果这篇内容对你有启发,欢迎关注「AI拉呱」,获取更多 AI 前沿洞察、实战教程与趋势解读。
下期在看
下期将继续带来该主题的进阶拆解与实操案例,建议先收藏本文,避免错过更新。
往期经典回看看
我的著作
1. AI 基础与认知.pdf 2. AI 数据工程实战.pdf 3. AI 算法与模型.pdf 4. AI 工具与框架.pdf 5. AI 工程化与部署实战.pdf 6. Hermes-Agent-从入门到精通.pdf 7. 企业AI转型-AI架构师手册.pdf 8. AI 时代人人应该如何应对?.pdf
动手学习AI的资料
AI零基础教程(内含教程和源码).zip
夜雨聆风