摘要
把 4 份真实企业文档丢给 AI Agent 做信息抽取:第一份全对,往后连环翻车——Prompt 换文档就废、同一份跑三次口径就漂、结果散在聊天记录、金额错了没人拦。挖到底,四个断点全在模型外面。本文用 Agent 两种用法(裸对话 / 工程化)对照 Scriber"字段定义—抽取—审核—交付"全链路,实测数字全部摊开。
一、问题复现:一个"看起来能跑"的 Agent 抽取流程
做企业数据的人迟早会接到这样的需求:业务部门要一张结构化表,数据来源是一堆合同、公告和年报。拿模型抽,还是拿人填?现在的答案通常是前者——大模型这么强,丢进去让它输出 JSON 就完事了。
我也是这么开始的。测试物料 4 份,都是真实业务文档,不是造出来的:
| 编号 | 文档 | 类型 | 页数 | 字段口径 |
|---|---|---|---|---|
| D1 | 贵州茅台 2025 年年度报告 | 富格式长文档(信披) | 143 | 16 个字段 + 前十大股东关系表 |
| D2 | 广州市政府采购委托代理协议 | 富格式长文档(合同) | 5 | 17 个字段 |
| D3 | 白云电器 2026 年半年度权益分派实施公告 | KV 制式文件 | 4 | 16 个字段 |
| D4 | 乐心股份 2026 年半年度权益分派实施公告 | KV 制式文件 | 4 | 同 D3 |
任务口径就是三张字段表。以权益分派公告为例,业务要的字段长这样(节选):
{ "证券代码": "603861", "每股现金红利(元)": 0.02, "每10股派现金额(含税,元)": 0.20, "股权登记日": "2026-10-14", "除权(息)日": "2026-10-15", "现金红利发放日": "2026-10-15", "是否差异化分红送转": true, "参与分配股本基数(股)": 536006217 }Agent 侧的用法,用企业里最主流、最不折腾的那套,不刻意搭稻草人:
你是文档信息抽取助手。请从文档中提取以下字段,以 JSON 输出: 证券代码、证券简称、公告编号、每股现金红利(元)、每10股派现金额(含税,元)、 是否送红股、股权登记日、除权(息)日、现金红利发放日、是否差异化分红送转、 参与分配股本基数(股)、分红总额(元)。 字段值必须来自原文,没有的置 null。**第一份,D3 白云电器公告,抽得相当不错。**证券代码、每股 0.02 元、每 10 股 0.20 元、三个日期、差异化分红为"是",全对,JSON 格式工整。如果测评到此为止,结论会是"Agent 已经够用了"。
但企业场景没有"到此为止"。接着往下走,四个现场依次出现:
现场一:换一份文档类型,Prompt 作废。把 D2 委托代理协议喂进去,还是这条 Prompt。模型很"诚实"地把证券代码返回了 null——委托代理协议里当然没有证券代码,但委托人、受托人、法定代表人、采购预算、采购方式这 17个真正要的字段,它一个都没抽。字段口径换了文档类型就换了整套,Prompt 得重写。
现场二:同一份文档,跑三次,输出没能完全一致。
还是 D3,还是同一条 Prompt,连跑 3 次。输出大体正确,但三个金额字段在数字与字符串之间摇摆:每股现金红利一次是0.02、两次是 “0.02”;最典型的是参与分配股本基数——一次是 536006217,两次是 “536,006,217”,带千分位逗号的字符串。同一个数,两种形态。下游按整数接收的代码,收到后者当场解析失败。(关键字段对照见第三章实验二。)
现场三:4 份文档 = 4 轮对话 + 4 次手工复制。D1 年报 143 页,一次对话窗口塞不下还要想办法切片;4 份文档抽完,结果散在 4 段聊天记录里,要进 Excel 还得手工整理。抽完了,但"数据"还不存在。
现场四:这次没抄串——但没有任何机制在兜底。白云电器公告同时写着"每股现金红利 0.02 元"和"每 10 股派发现金红利 0.20 元(含税)"。3 次运行都没有抄串——但注意这个“没出事”的性质:流程中没有任何一环在核对这两处金额。这次没抄是运气,哪天抄了,它会 静默地直接进下游。
四个现场对应四个问题:换文档要重写 Prompt、输出不稳定、没法批量、没有审核。这不是某个模型菜——同样的模型,同样的文档,单次抽取质量并不差。问题出在别处。
二、原因分析:四个断点,都断在模型外面
先说结论:四个现场没有一个是"模型能力不足"。把每个现象拆到工程原理层,会发现缺的是围绕模型的生产设施。
2.1 断点一:Prompt 是"无契约的配置"
Prompt 的本质是一份配置:字段名、业务口径、输出格式,全部写在自然语言里。而配置在工程里是要被管理的——有版本、能 diff、可回归测试。自然语言 Prompt 三样全无:
- 无 Schema:字段约束(类型、枚举、是否必填)没有机器可读的定义,模型靠"理解"执行,读者靠"肉眼"验收;
- 无版本:改一个字段口径,没法知道哪些历史输出用的是旧口径;
- 无回归:调 Prompt 的人都知道,修好一个 case 惊动三个 case,因为没有测试集帮你盯。
企业字段的口径还特别"厚"。同样是"股本"——总股本、扣减回购股份后的股本基数、参与分配的股本,三个数在同一份公告里同时存在,差一个定语就是差一个数量级的错误。这种口径,一次对话里说不清,说清了也锁不住。
对比一个有契约的世界长什么样:
{ "name": "每股现金红利(元)", "type": "数字", "提取规则说明": "重要内容提示「A股每股现金红利」", "check": "逻辑校验:每10股派现金额 = 本字段 × 10" }字段名、类型、原文定位提示、校验规则,四件事写死在定义里。这是后面平台化方案的出发点。
2.2 断点二:无约束解码 = 不稳定输出
第二个现场(同 Prompt 跑 3 次口径就漂)的机制有三层。
采样层:大模型解码是概率采样,温度不为零,同一输入不保证同一输出。这是特性不是缺陷——对话产品需要多样性。但数据生产要的是确定性。
约束层:会话式输出没有结构化绑定。JSON Schema、function calling、structured output 这些能力在 API 层是存在的,但在"把文档丢进对话框"这个用法里,输出契约完全靠模型自觉——字段名漂移(每股现金红利→每股红利)、类型漂移(0.02 vs “0.02” vs “2分”)都是这么来的。
定位层:拿现场四的两处金额做个解剖。白云电器公告里,0.02 元在文首"重要内容提示"区,0.20 元(每 10 股)在第二节的"分配方案"里——两处相隔约 800 字、跨越一个章节边界。模型读的是整段线性文本,没有任何机制告诉它"每股红利"这个字段的锚点位置,它靠注意力在全文里"找回"这个值,而注意力有近因偏好、有噪声。同一个值离哪个数字更近,是文档排版决定的,不是字段口径决定的。当字段没有锚点,排版就是命运。(实测 3次没有抄串——运气站在了这边,但机制并不存在。)
还有一种漂移更隐蔽:表格串列。D1 茅台年报的"主要会计数据"表长这样:
| 指标 | 本期 | 上年同期 | 增减幅 |
|---|---|---|---|
| 营业收入(元) | 168,838,102,514.79 | 170,899,152,276.34 | -1.21% |
| 基本每股收益(元/股) | 65.66 | 68.64 | -4.34% |
三列并排,业务只要"本期"。模型没有错认数字,它只是偶尔取错列——而"取哪列"这件事,Prompt 里往往忘了说。这不是识别问题,是口径没有随字段一起被定义。
2.3 断点三:会话式 I/O 不是数据流水线
第三个现场(4 份文档 4 轮对话)的本质:会话是给人用的交互形态,不是给数据用的。
企业批量抽取要的东西,会话模式一样都不提供:没有队列(100 份文档排队)、没有并发编排、没有失败重跑(第 37 份超时了怎么办)、没有结果汇聚(抽完自动合成一张表)。要让 Agent 批量跑,得自己写胶水:
# 把会话包装成流水线,需要自己补的部分 docs = load_documents("inbox/") # 1. 文件汇聚:自己写 results, failed = [], [] for doc in docs: # 2. 队列:就是个 for 循环,无并发 try: out = agent.extract(doc, PROMPT) # 3. Prompt 硬编码在代码里 results.append(out) except Exception as e: failed.append((doc, e)) # 4. 失败处理:自己定义 save(results) # 5. 汇聚入库:自己写二十行脚本看着不难,难的是它没有尽头:口径变了要改 Prompt、模型升级要回归全量、老板要个失败率报表还得再加两百行。"抽取完成"和"数据可用"之间,隔着一整条数据工程的距离。
2.4 断点四:没有校验层,错误直接入库
第四个现场(没有任何机制在兜底)是四个里最危险的,因为它不可见——前三个现场至少肉眼可查,这一个要靠人对着原文逐字核。
数据生产系统的常识是:上游错误率不为零,所以要建校验层。文档抽取场景里,至少四类校验是刚需:
def review(record): # 为空校验:核心字段缺失即拦截 assert record["证券代码"], "证券代码为空" # 正则校验:格式契约 assert re.fullmatch(r"\d{6}", record["证券代码"]), "证券代码格式错误" # 逻辑校验:字段间关系 assert record["股权登记日"] <= record["现金红利发放日"], "日期逻辑矛盾" # 计算校验:可推导字段交叉核对 expected = record["每股现金红利"] * 10 assert abs(expected - record["每10股派现金额"]) < 0.01, "金额勾稽不符"这十行代码能拦下的,恰恰是现场四那类错误。拿白云电器的真实数值代入验算一遍:正确抽取时 per=0.02、per10=0.20,|0.02×10−0.20|<0.01,放行;一旦抄串成 per=0.20、per10=0.02,|0.20×10−0.02|=1.98,必然拦截。这组数字天然互为校验码——正因为它们分居两处、口径相关,抄串才发生;也正因为口径相关,一条计算校验就能逮住它。规则审核不是碰运气,是把文档里本来就存在的勾稽关系显式化。
但"把文档丢进对话框"的用法里,这段代码没有位置——它的输出口是聊天窗口,不是校验管道。
定性一下:**这不是模型能力问题,是缺一层 guardrail。**模型负责抽得准,guardrail 负责抽错时兜得住。四个断点全部指向同一个结论——文档抽取要的不是一个更聪明的模型,而是一条围绕模型的数据生产线。
2.5 先接一个反问:把 Agent 工程化,不行吗?
写到这里,懂行的读者已经准备反驳了:你说的四个断点,API 层都有解——structured output 锁输出契约,自写脚本做校验和编排,不就补齐了?
这条路确实存在,而且每一步都真的有效。逐个看:
| 断点 | 工程化补法 | 有效吗 |
|---|---|---|
| Prompt 无契约 | Prompt 改用 JSON Schema 声明 + structured output | 有效,命名/类型漂移基本消失 |
| 输出不稳定 | 同上,加上温度调 0 | 有效,同一输入近乎恒定 |
| 无批量流水线 | 写编排脚本:队列 + 重试 + 汇聚(2.3 节那二十行只是开头) | 有效,但要自己维护 |
| 无校验层 | 校验规则写成代码(2.4 节那十行) | 有效,覆盖四类规则 |
也就是说,断点不是模型能力问题,API 层的补法都成立——这正是我想说的。问题在于这条路走到头是什么:字段口径写在代码里,只有工程师能改;业务方要加一个字段、改一个口径,得提需求、排期、发版;校验规则散在脚本里,没有复核工作流——谁来看告警、复核结果记在哪、改完的数据流向哪,全是空白;模型每次升级换版,全量回归自己扛。
这条路能走。但走到头你会发现:自己在用代码手搓一个文档抽取平台——契约、流水线、校验、复核全要自己造,而且造出来的东西工程师之外没人能用。手搓一遍的最大价值,是让你知道成熟的生产线应该长什么样。所以第三章的对照实验,Agent 组我准备了两套:一套是第一章的"裸对话"用法(企业里的主流现实),一套是本节的"工程化"用法(技术上的上限)——分别对照,结论更公平。
三、对照实测:把四个断点逐个补上
接下来做对照实验。Agent 组按 2.5 节的结论准备两套:“裸对话”(企业主流用法)和"工程化"(structured output + 自写校验编排);对照组我选了庖丁科技的 Scriber ——一个专门的文档信息抽取平台,覆盖"字段定义→抽取→审核→结构化交付"全链路。选它的原因很简单:四个断点各自的解法,它恰好各有一个模块对应,适合做"断点—解法"的一一对照,而不是泛泛的功能罗列。
**注意:**Scriber 是纯 B 端产品,按客户需求定制化交付,没有自助试用入口;也常作为抽取环节嵌入更大的方案,与审核/比对/撰写等产品打包提供。本文实测在厂商提供的测试环境中进行。
先把实验环境交代清楚(Agent 侧保证可复现;Scriber 侧为厂商开通的测试环境)
| 项 | 配置 |
|---|---|
| Agent 模型与版本 | 裸对话:智谱清言网页版默认模型;工程化:glm-5.2 |
| 采样参数 | 裸对话:默认温度、每次独立新会话;工程化:temperature=0 |
| Agent 用法 | ①裸对话:文档 + 自然语言 Prompt;②工程化:JSON Schema 声明 + structured output + 自写校验/编排脚本 |
| 对照组 | Scriber(B 端定制化交付,无自助试用;本次实测为厂商提供的测试环境) |
| 对照基准 | 不建全量金标准——关键字段直接对照公告/年报原文核对 |
| 重复次数 | 稳定性实验每文档 2 次 |
下文表格里,凡两套 Agent 用法结果可能分化的(稳定性、审核、终态指标),都拆成"裸对话 / 工程化"两列分别呈现——只用裸对话做对照,对 Agent 不公平;配置工作量(3.1)和批量形态(3.3)两套用法相近,合并为一列。
3.1 实验一:Prompt 复用性——换文档类型的成本
方法:用"权益分派公告"的 Prompt 去抽 D2 委托代理协议(Agent 组);Scriber 侧建’权益分派公告’字段模板(10 个字段),另两类文档同理
| 配置项 | Agent 组 | Scriber 组 |
|---|---|---|
| 文档类型 A:权益分派公告 | 手写 Prompt,约 150字 | 模板 1:10 字段可视化配置 |
| 文档类型 B:委托代理协议 | 重写 Prompt(字段全换) | 模板 2:17字段 |
| 文档类型 C:年报 | 再重写(含表格取列口径) | 模板 3:10 字段 + 关系表 |
| 口径变更(如字段改名) | 改 Prompt + 全量重跑验证 | 改模板字段,模板版本留存 |
| 跨类型复用 | 无机制,靠复制粘贴改 | 模板按类型组织,长期复用 |
结论:Agent 的配置是"一次性自然语言",每次换型重写、改完没法追踪;平台把字段定义沉淀为可复用的模板资产。差别不在打字快慢,在配置有没有被当作资产管理。
3.2 实验二:抽取稳定性——同一文档 × 3 次
方法:D3 白云电器公告,Agent 同一 Prompt 连跑 3 次(3 个独立新会话),只看 6 个关键字段的输出形态;Scriber 同一模板重复抽取 3 次,核对输出是否一致。
关键字段对照(●=与首次一致,○=漂移,空=漏抽;只需看这 6 个字段,不用全量 10 个):
暂时无法在飞书文档外展示此内容
类型漂移 3 次(股本基数还叠加千分位格式问题);无命名漂移、无取值错误、无漏抽。
结论:
裸对话的漂移集中在类型上:实测三个金额字段在数字与字符串之间摇摆,股本基数还出现过千分位字符串——值没错,形态不统一。对人工两种写法都能看懂,对系统一种是数据、一种是错误。Scriber 侧字段名和类型由模板锁定,重复抽取输出一致。
3.3 实验三:批量处理——2 份同构公告
方法:Agent 组 2 份逐份对话;Scriber 组 2 份公告批量上传、一次抽取、表格总览
| 记录项 | Agent 组 | Scriber 组 |
|---|---|---|
| 总耗时 | 5min | 2min |
| 人工介入次数 | 2 | 0 |
| 结果形态 | 4 段聊天记录 | 1 张结构化表 |
| 文件列表支持重新解析 / 重新预测 / 重新审核 | 无 | 有 |
结论:批量的差距不在速度在形态——Agent 组抽完拿到的是 2 段文本,Scriber 组拿到的是 1 张表。前者离"入库"还差一步人工,后者已经到位。规模放大后,这一步人工是线性增长的。
3.4 实验四:审核机制——三条核心校验现场过
方法:从字段定义的审核规则里挑三条最硬的,预先写下"预期翻车点",然后看两组谁能把错误拦在入库前。完整规则清单见文末物料包的字段定义 JSON,有余力可加测。
| # | 校验规则 | 预期翻车点 | Agent 裸对话 | Agent 工程化 | Scriber |
|---|---|---|---|---|---|
| 1 | 每10股派现 = 每股红利 × 10 | D3 两处金额相隔一节,易抄串(见现场四) | 未触发(3 次无错误,但无机制保证) | 通过 ✓(拦截演示当场拦住 0.20/0.02 抄串) | 通过 ✓ |
| 2 | 股权登记日 ≤ 除息日 ≤ 发放日 | 日期串行 | 未触发(3 次无错误,但无机制保证) | 通过 ✓ | 通过 ✓ |
| 3 | 必填字段为空即拦截 | 核心字段漏抽 | 未触发(3 次无错误,但无机制保证) | 通过 ✓(4 类校验全部通过) | 通过 ✓ |
裸对话 3 次未发生错误(预期翻车未翻,但无任何机制兜底);工程化脚本两份公告 4 类校验全部通过,拦截演示确认计算校验能拦住抄串数据;Scriber 命中明细:把每股现金红利手动改成 0.20(与每10股派现 0.20 形成抄串)后重新审核,规则 1 当场标红"不合规",判定依据为"每10股派现金额(含税,元)≠ 每股现金红利(元)× 10";恢复 0.02 后再审,全部通过。正确数据下三条规则无漏报、无误报。
如实记录预期之外的发现(保持测评诚实度):
- ① 裸对话的漂移只发生在类型层(字符串 vs 数字,股本基数叠加千分位),数值三次全对——比预期温和;工程化(温度 0 + Schema 声明)输出干净数字,约束有效,但这批校验和编排代码从此归自己维护(见 2.5)。
- ② Scriber 侧如实记录:日期只锁类型、不归一化格式——白云抽成 2026/10/14、乐心抽成 2026 年 10 月 14 日,两份同类型公告出来两种日期写法;三条规则在公式搭建器里配置约 10 分钟,无漏报无误报。
制造一条真实警告:将每股现金红利(元)输入框中的0.02改成0.20,提交重新审核,审核结果当场标红。
恢复现场,再重新审核
3.5 实验五:结构化交付
方法:两组结果各模拟一次"进 Excel / 进业务系统"。Agent 组 4 份输出手工整理;Scriber 组直接导出。
Agent 组 4 份输出手工复制进 Excel、对齐字段,约 8 分钟;Scriber 组在结果页直接导出 Excel(xlsx,按"非组合类型字段/组合类型字段"分 sheet),两份公告即下即用;Schema 页另支持 CSV/JSON 导出。
小结:五个实验的指向一致——Agent 输出的是"给人看的文本",Scriber 输出的是"给系统用的数据"。四个断点在后者那里各有一个明确的补法。下一章把整条链路串起来跑一遍。
四、Scriber 实战:一条完整的数据生产线
4.1 四个断点,四个模块
对照第二章的机制分析,Scriber 的模块和断点是严格对应的:
| 断点(第二章) | 机制缺失 | Scriber 对应模块 |
|---|---|---|
| Prompt 无契约 | 自然语言配置,无 Schema | 可视化字段定义:字段名、类型、口径说明,按文档类型组织 |
| 输出无约束 | 会话式输出无绑定 | 模板锁定字段契约,输出恒定 |
| 无批量流水线 | 会话式 I/O | 批量上传、任务化处理、结果表格化总览 |
| 无校验层 | 无 guardrail | 审核规则:逻辑校验 / 为空校验 / 大模型校验(正则、计算用逻辑公式实现) |
两个值得单独说的设计:
字段值可追溯定位。每个抽取结果能回链到原文位置。这不是体验优化,是审核的前提——复核员要"对着原文改",没有溯源就得重新全文找一遍,审核成本翻几倍。这层能力来自它底层的 PDFlux 解析(跨页表格、无线框表格的原样输出),对 D1 那种 143 页、表格重排的文档是刚需。
标注沉淀为模型资产。复核时的人工修正,可以反哺自训练提取模型——改一次,同类文档下次少错一次。对照 Agent 用法:人工修正的成果散落在聊天记录里,模型永远学不到。这是"Prompt 调教"和"数据资产"的分野。
4.2 全链路实录:2 份文档从上传到导出
全流程跑完,不做百分比式的准确率统计——拿 D3 的几个关键字段逐格对照原文,比一个"96.7%"更有信息量:
| 字段(D3 白云电器) | 原文值 | Agent 裸对话(3 次输出) | Scriber |
|---|---|---|---|
| 每股现金红利(元) | 0.02 | “0.02” / 0.02 / “0.02”(两次为字符串) | 0.02 |
| 每10股派现金额(含税,元) | 0.20 | “0.20” / 0.20 / “0.20”(两次为字符串) | 0.20 |
| 股权登记日 | 2026/10/14 | 三次一致 | 2026/10/14 |
| 现金红利发放日 | 2026/10/15 | 三次一致 | 2026/10/15 |
| 参与分配股本基数(股) | 536,006,217 | “536,006,217” / 536006217 / “536,006,217” | 536006217 |
4.3 边界讨论:什么时候不需要它
测下来也不是没有要交代的:
- 一次性、探索式需求,抽完就给人看、不入库——Agent 完全够用,为这个上平台是杀鸡用牛刀;
- 字段口径极简单(比如只要个公司名和日期),Agent 的漂移也在容忍范围内;
- 平台有配置成本:三套字段模板 + 审核规则的首次搭建,比写一条 Prompt 慢。这笔投入换的是"第二次起免费",文档量越大摊得越薄。
- 它不是开箱即用的通用 SaaS:按客户需求定制交付、有实施门槛,轻量、一次性的场景不必选它;如果本身已有审核、比对、撰写的需求,抽取环节可以一并打包考虑。
一句话:结果只是给人看,用 Agent;结果要进系统,才需要生产线。
五、实践方案:文档抽取选型自查清单
不管最后选 Scriber 还是别的方案(同类能力可对照 TextIn xParse 等产品横向比较),这套清单是通用的。注意 Scriber 的获取方式和这类可自助试用的产品不同(B 端定制、联系厂商),选型时要把交付成本也算进去。
清单一:先问结果要去哪里
结果只是给人看一眼 → Agent / OCR 已足够 结果要进 Excel 定期更新 → 需要字段定义 + 批量 结果要进数据库/业务系统 → 需要全链路(定义→抽取→审核→交付)清单二:能力项检查表
| # | 检查项 | 为什么重要 |
|---|---|---|
| 1 | 字段 Schema 可配置,且能写业务口径 | 断点一:契约 |
| 2 | 输出被 Schema 锁定(类型、命名恒定) | 断点二:稳定 |
| 3 | 字段值可追溯定位回原文 | 审核的前提 |
| 4 | 批量处理 + 失败重跑 | 断点三:流水线 |
| 5 | 审核规则:为空/正则/逻辑/计算/一致性 | 断点四:guardrail |
| 6 | 结构化导出与系统集成 | 数据要去的地方 |
| 7 | 长文档与 KV 制式双引擎、复杂表格 | 版式多样性 |
| 8 | 人工修正能反哺模型 | 第二次起免费 |
清单三:决策树
文档抽取需求 ├─ 一次性 / 少量 / 结果给人看 │ └─ AI Agent 直接抽(写清字段口径和输出格式即可) └─ 批量 / 周期性 / 结果进系统 ├─ 字段口径稳定 ──→ 平台化抽取(按清单二选型) └─ 口径常变 ─────→ 平台化 + 支持模板版本管理的方案六、总结:抽取的上限,在字段定义时就定了
回到开头的四个现场。同一份文档、同一个模型,单次抽取质量并不差——差的从来不是模型,是围绕模型的设施:字段有没有契约、输出有没有约束、批量有没有流水线、结果有没有校验。
四个断点也标定了 Agent 和平台的分工:**Agent 负责理解文档,平台负责让理解的结果可复用、可批量、可信。**模型负责"抽得准",生产线负责"每次都抽得准、能批量抽、抽完敢用"。
企业文档信息抽取的本质,不是一次模型调用,而是一条从文件到业务数据的生产线。这条线的第一站是字段定义——所以标题那句话值得再说一遍:抽取的上限,在字段定义时就定了。
本文用到的 4 份物料和 3 套字段定义都是公开文档,公众号后台回复"抽取物料"可取,欢迎拿同一套口径复现