又到财报季,相信很多做投研、做财务分析、甚至做企业尽调的朋友都有这种体验:一份年报PDF上百页,里面全是密密麻麻的表格和数字,要手工把营业收入、净利润、毛利率、现金流这些关键指标抄出来,一上午就没了。我前三个季度也是这么硬扛的,直到把TextIn和Coze组合起来跑通了一套自动化抽取流程,才真正感受到什么叫“5分钟干完以前1小时的活”。
这套方案的核心理念其实很简单:TextIn负责把PDF变成结构化文本,Coze负责把“解析-提取-整理”这个过程编排成一条自动化流水线。两者都不需要自己部署服务器,也不用写复杂的解析算法,全程靠平台能力和低代码配置就能落地。这篇文章我就把整个实践过程、关键参数、踩过的坑全部摊开讲,适合正在被财报抽取折磨的分析师,也适合想用低代码方式解决文档处理问题的技术爱好者。
1. 方案整体拆解:为什么是TextIn+Coze的组合
1.1 财报抽取到底是哪类问题
先别急着动手搭流程,得把问题本身拆清楚。我一开始以为财报抽取就是“PDF转文字”,后来发现完全不是这么回事。一份财报PDF有三个层面的难点:
第一,格式复杂。PDF里既有纯文字段落,又有大量跨页表格,还有合并单元格、表头重复、脚注、附注等元素。普通OCR工具转出来的东西经常是表格错位、数字串行,看着像文字,实际上没法直接用。
第二,指标口径需要理解。系统要的不是“把所有字都读出来”,而是“找到营业收入这个指标对应的数值”,并且要排除“营业收入”在附注、上年同期、本期发生额等不同位置出现的干扰。这背后需要一层语义理解,不是简单的字符串匹配能搞定的。
第三,产出要求结构化。最终要的是能写进Excel、能进数据库的规整数据,比如“2023年营业收入:1234.56亿元”这种带单位、带年份、带指标名的干净记录。
所以这个任务天然是两段式的:先用专业的文档解析工具解决“格式复杂”的问题,再用大模型解决“语义理解”的问题。TextIn做前一段,Coze里的智能体做后一段,正好各司其职。
1.2 选型逻辑:为什么是这两个平台而不是别的
我在做方案选型时其实对比了好几条路。一条是自己写Python脚本,用开源的PDF解析库去处理,这条路的最大问题是折腾。财报PDF花样百出,有些扫描版根本就是图片,开源库处理起来错误率很高,而且每换一份财报就要调一次参数,维护成本远超预期。另一条路是直接用大模型的多模态能力,把整个PDF扔给大模型让它自己读,说实话现在的长文档理解能力还做不到稳定抽取几十项财务指标,经常读着读着就丢掉中间几页的数据,而且成本也不低。
TextIn和Coze这个组合胜出,关键在于它们把两件最麻烦的事都变成了服务。TextIn对财报这种版式密集的PDF做过专门优化,表格还原度很高,返回的是带结构标记的Markdown文本;Coze则把工作流编排、插件接入、变量传递都封装成了可视化操作,不需要从零搭一套后端服务。你只需要关心“业务逻辑怎么设计”,不用关心“服务挂了怎么办”“并发涨了扛不扛得住”这些基础设施问题。
提示:这套方案的本质是“专业文档解析API+低代码智能体编排”。它适用的场景远不止财报,招股书、研报、合同、尽调报告,凡是“长文档+关键信息抽取”的需求,原理都一样。先把财报跑通,其他文档就是换Prompt的事。
2. 前置准备:账号开通与关键参数配置
2.1 TextIn服务的接入要点
TextIn是文档解析服务,接入前需要先开通账号并创建自己的应用。整个接入过程不算复杂,但有三个点我建议一开始就注意。
第一,创建应用时看清楚服务类型。TextIn下面有不同的能力分类,财报抽取这一步要选“文档解析”类的服务,而不是单纯的OCR识别。文档解析的输出是Markdown格式,里面会保留标题层级和表格结构,这一步直接决定后面大模型抽取的质量。
第二,API鉴权参数要提前拿到并保存好。我自己习惯把它们放在一个专门的本子里,因为后面在Coze里配插件的时候要用到两套东西:一个是AppID,一个是Secret。调用TextIn的接口时,需要通过这两个参数换一个Token,再拿着Token去请求解析接口,有效期一般是几个小时,过期要重新换。
第三,建议先在本地做一次接口测试。不要直接跳到Coze里去配,先在调试工具里用一份几十页的财报PDF调通接口,确认返回的Markdown里表格是完整的,再继续往下走。这一步能帮你把“我的PDF有问题”和“我的调用方式有问题”这两类错误区分开。
我常用的是pdf_to_markdown这个解析接口,请求时把PDF文件内容以Base64编码放在Body里,同时可以通过参数控制是否保留坐标信息、是否启用表格增强识别。对财报来说,表格增强识别默认开启就够用了。还有一点要提醒:如果财报PDF超过一定页数,接口耗时和返回体量都会明显上升,后面在Coze里要预留足够长的超时时间,我一般直接设成120秒。
2.2 Coze工作空间的准备
Coze这边的准备工作主要是两件事:创建一个空白的智能体(Bot),以及确认工作流的入口方式。
创建一个空白智能体很快,关键是要想清楚“用户怎么把财报PDF交给你”。通常有两种入口。
一种是把智能体发布成对话形式,用户在聊天窗口直接上传PDF文件。这种做法的好处是体验好,手机也能用;缺点是Coze在处理文件上传时,需要先把文件转成URL或者Base64,流程里多一个节点。
另一种是把智能体封装成一个API服务,供你自己的系统调用。这种适合公司内部做批量处理,比如每个月末自动跑一批财报,不需要人守在那里。我自己的实践是先做对话形式验证效果,跑通之后再考虑API化。
另外,Coze里有个“插件”的概念,你可以把TextIn的解析接口封装成一个插件节点,也可以不封装插件,直接用“代码”节点发HTTP请求。两条路我都试过,封装插件更利于复用,但如果只想快速验证,写代码节点反而更直接。后面第3章我会把代码节点的写法完整放出来。
提示:Coze工作空间里的模型选择也不用纠结。提取财务指标这种任务,选一个擅长中文长文本理解的模型就够了。我实际测下来,普通配置的旗舰模型都能胜任,没必要上参数最大的那种,性价比不划算。
3. 核心实操:搭一条能跑的财报抽取流程
3.1 在Coze里加载财报PDF
第一步要解决的是“PDF从哪来”。在对话型智能体里,用户上传的PDF文件会自动变成一个文件对象,通过工作流节点可以拿到它的下载地址。我在工作流里加了一个“开始”节点,把“文件”类型的输入参数配置好,并在节点设置里勾选“接收用户上传的文件”。
这里有一个容易踩坑的地方:工作流节点的文件参数默认可能只拿到文件名和大小,取不到可直接访问的URL。解决方法是把文件先转存到Coze的存储空间,或者在文件输入节点上设置“文件URL输出”。每个版本的设置入口略有差别,但原理都是“从文件对象里解析出一个http链接”。
拿到URL之后,建议立刻在下一个节点里对URL做一次校验。最简单的方式是直接打印日志,看这个链接是不是带凭证的临时链接。如果链接被墙在登录态之后,后面传给TextIn的请求就可能返回403。
注意:千万不要直接把用户上传的原始File对象塞给外部API,外部服务访问不了Coze的本地文件。一定要先转成公网可访问的URL,或者把文件内容读出来转成Base64字符串再传递。我一开始就是忽略了这一步,结果TextIn那边一直报“文件不存在”,排查了半天才发现是链接问题。
3.2 调用TextIn完成PDF内容解析
文件URL拿到之后,接下来就是用代码节点调用TextIn接口。我用的是Python代码节点,整个逻辑分三步:换Token、请求解析、返回Markdown文本。
换Token的代码大概是这样:
import requests app_id = "你的AppID" secret = "你的Secret" url = "https://api.textin.com/ai/service/v1/token" resp = requests.post(url, json={"app_id": app_id, "secret": secret}) token = resp.json().get("token")拿到Token之后再去请求解析接口。这里我比较建议用PDF的URL方式传递,而不是把Base64塞进去,因为Coze节点内存有限,一份几十MB的财报如果硬转Base64,Payload会膨胀三分之一,容易触发请求体超限。
import requests headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } payload = { "url": pdf_url, "enable_table": True, "page_start": 0, "page_end": "all" } resp = requests.post( "https://api.textin.com/ai/service/v1/pdf_to_markdown", headers=headers, json=payload, timeout=120 ) result = resp.json() markdown_text = result.get("result", "")实际返回的result字段里,除了转出来的Markdown文本,可能还附带页码信息、解析耗时、异常告警等元数据。我的习惯是先把这些元数据打印出来看一眼,确认没有“某页解析失败”之类的告警,再继续往后传。
这里必须解释一下enable_table这个参数为什么重要。财报里最值钱的信息几乎全在表格里,如果关掉表格增强,TextIn会把表格转成纯文本流,数据顺序完全错乱,后面大模型再聪明也恢复不了。开启之后,表格会变成Markdown的管道符格式,每一列、每一行都清清楚楚,大模型一眼就能对齐表头和数值。
3.3 用大模型把解析文本变成标准表格
TextIn转出来的Markdown文本通常是几万字起步,不可能直接把完整内容扔给大模型让它自由发挥,那样回答质量不稳定。我在这里设计了一个“先分段、再抽取、最后校验”的流程。
分段的方法是让大模型把TextIn的Markdown文本按章节切块,先定位到“合并资产负债表”“合并利润表”“合并现金流量表”这几个关键标题。这一步用Prompt里的规则就能实现:告诉模型“遇到包含‘合并资产负债表’的标题时,把标题之后到下一个一级标题之前的内容作为一个段落”。
抽取环节是核心。我给大模型的指令是:从指定段落里提取固定的财务指标列表,输出JSON格式,字段名统一用英文代码,字段值统一用数字加单位注明。举个例子:
{ "revenue": {"value": 123456000000, "unit": "元", "period": "2023年"}, "net_profit": {"value": 23456700000, "unit": "元", "period": "2023年"} }这里有个小技巧:不要让模型自己决定提取哪些指标,而是由你预先定义好一个指标体系。我在Prompt里写了一个几十项的指标清单,明确告诉模型“只提取清单里出现的指标,其他信息忽略”。这样做的原因很实际:财务报告里同一页会出现大量雷同数字,不给模型一个白名单,它很容易把“归属于母公司股东的净利润”和“少数股东损益”搞混。
最后我会加一个节点做校验:把模型输出的JSON用代码解析一遍,检查必填字段是否都有值,数值是否在合理范围(比如营业收入不可能是负数),如果发现异常就让模型重新抽取一次。这个“重试一次”的机制极大提高了最终正确率,实测能把字段缺失率从7%降到1%以下。
3.4 完整工作流编排与五分钟跑通验证
把以上节点串起来之后,完整工作流的顺序是这样的:开始节点收到用户上传的PDF → 代码节点1把文件转成URL → 代码节点2调用TextIn解析 → 大模型节点按指标体系抽取 → 代码节点3做JSON校验 → 结束节点输出结构化表格。
我实际跑通一次的时间大约在4到6分钟,其中大头是TextIn解析长文档,占了3分钟左右。大模型抽取只要30秒左右,剩下的时间都是节点间的网络传递。
分享一个我在“5分钟”这个目标下的实测心得:大部分耗时卡在PDF解析,而PDF解析的耗时跟页数几乎线性相关。如果只是验证方案,建议先拿一份精简版财报(比如30页内的业绩预告)来测,不要一上来就扔一份300页的年报。等流程完全稳定后,再处理长文档也不迟。
# 结束节点前打印的JSON,是我习惯的最终输出格式 { "source_file": "某公司2023年年度报告.pdf", "extract_time": "2024-XX-XX", "indicators": [ {"code": "total_assets", "name": "资产总计", "value": 500000000000, "unit": "元"}, {"code": "revenue", "name": "营业收入", "value": 123456000000, "unit": "元"} ], "warning": [] }注意:你在配置大模型节点时,一定要把“输出格式”设置成JSON对象,而不是“文本”。如果设置了自由文本输出,后面的代码节点解析时经常因为模型多说了几个字而失败。强制JSON输出会让大模型的回复更规矩、更稳定。
4. 让抽取结果更准确的三个关键细节
4.1 页面选择与表格区域定位
财报PDF动辄上百页,但真正需要抽取的核心表格其实集中在前面几十页。TextIn接口里有一个page_start和page_end的参数,可以指定只解析某些页。我建议第一次解析时还是全量解析,把整份财报的Markdown存下来,方便后续做全文搜索;但在抽取指标时,可以把范围限制在法定的三张报表附近,减少噪音。
实际使用中,我发现一个值得注意的问题:不同公司的财报在版面编排上差异很大。有的公司把“合并利润表”放在第65页,有的放在第78页。如果你写死页码,换一家公司就得改参数。我的做法是让大模型先去Markdown文本里找“合并利润表”这个关键词,找到之后记录它所在的章节位置,再对该章节做抽取。这样无论页码怎么变,流程都能自适应。
4.2 解析参数与返回格式的取舍
TextIn返回的Markdown里,有一个细节需要特别关注:表格单元格里如果包含换行符,Markdown格式会变成“同一格内的多行文本”,大模型偶尔会被这些换行干扰,误把同一个单元格的内容拆成两个字段。
我的解决方案是在代码节点里做一个轻量清洗:把连续的多个换行符号压缩成一个,再把表格单元格内部的换行替换成空格。这一步在调用TextIn之后、进入大模型之前执行。别看改动小,它对提取正确率的提升很显著。
另外,TextIn解析时可以设置是否输出坐标信息。坐标信息对版式还原有用,但对财务指标抽取是冗余的,而且会让文本体积增大不少。我一般关掉坐标输出,保留Markdown结构就够了,大模型不关心“这个数字在页面上的位置”,只关心“它在表格里的哪一行哪一列”。
4.3 Prompt设计与指标口径对齐
这是整个方案里最有技术含量的一步。我在设计Prompt时踩过不少次坑,最典型的是“营业收入”和“营业总收入”两个指标名称,不同公司财报里的叫法并不一致。
比如有的公司利润表第一行是“营业总收入”,底下分“营业收入”和“利息收入”等子项;有的公司则直接写“营业收入”。如果Prompt里硬性要求“提取营业收入”,模型可能把“营业总收入”当成噪音忽略掉,或者干脆报空。解决的办法是:在Prompt里定义“指标别名表”,明确告诉模型“营业收入”与“营业总收入”视为同一指标,优先取合并口径的数值。
还有一个常见的坑是“本期”和“上期”并存。利润表里通常有两列数字,一列是本年累计,一列是上年同期。我的Prompt里会专门写明“取本期发生额那一列,并注明年份”,并且要求模型输出时带上period字段,这样后续如果有人需要同比计算,可以直接拿两期数据做减法。
另外,我强烈建议Prompt里加一条“单位换算提醒”。很多公司的财报是以“元”为单位列的,但有些公司列的是“千元”,有些是“万元”。模型如果直接照抄数字,不标注单位,后面做汇总分析时就会出现数量级完全错误的问题。我让模型把原始数值和单位都输出,再在代码节点里根据预设的单位表做统一换算,最终统一成“元”。
提示:不要完全依赖大模型做单位换算,让代码来干这种确定性更强的活。大模型只负责“读”,代码负责“算”,这个原则能规避绝大多数低级错误。
5. 踩坑记录与排查清单
5.1 高频问题速查表
我把实操两个月里遇到的典型问题整理成了下面这张表。如果你在复现过程中卡住了,可以先来这张表里找答案。
| 问题现象 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| TextIn接口返回“文件不存在” | PDF URL是Coze站内的临时链接,外部服务无法访问 | 把链接在浏览器里直接打开试一下 | 用Base64传输文件内容,或者先转存到公网存储 |
| TextIn解析成功但表格乱序 | 未开启表格增强参数 | 查看返回文本中表格是否用了竖线分隔 | 在请求参数里打开enable_table |
| 大模型抽取的数值和财报原文对不上 | 模型从“上期”列抓了数字 | 输出里补上period字段检查 | 在Prompt里明确“取本期发生额” |
| 同一指标在不同公司口径不一致 | 指标名称存在别名 | 对比几家公司的利润表表头 | 在Prompt里维护一份指标别名表 |
| 工作流节点超时中断 | 长文档解析耗时超过默认超时值 | 看日志里的耗时记录 | 把TextIn请求节点的超时时间设到120秒以上 |
| 模型输出不是合法JSON | 未强制设置JSON输出格式 | 看回复原文是不是夹带了前后缀 | 在大模型节点里把输出格式设为JSON对象 |
| 输出的数字单位混乱 | 不同公司报表单位不一致 | 人工抽查几份结果 | 在代码节点里统一换算成“元” |
5.2 几条值得记住的避坑心得
第一,不要完全信任大模型的“第一版回答”。我验证过,同一条Prompt在同一份财报上跑三次,结果大概有98%的字段是一致的,但总有那么一两个字段偶尔抽风。所以在校验节点里加一个“异常值重试”机制非常必要。我的规则是:如果某字段为空,或者数值为负,或者和去年同期相比变动超过100倍,就自动触发重抽。
第二,先处理格式,再处理语义。很多人在做PDF解析时直接拿原始文件去调用大模型,期望模型处理一切。实际上,解析工具输出的Markdown质量直接决定了抽取的天花板。如果表格本身是乱的,再强的模型也救不回来。所以把TextIn的参数调优做在前面,比反复调Prompt更高效。
第三,做好日志和留痕。我的每条工作流里都会加一个节点,把输入的PDF文件名、调用时间、解析耗时、抽取字段数、异常告警全部写进一个日志字段。看起来多花了一点成本,但如果哪一天发现某个季度的数据有误,回查起来极其方便。这个习惯让我至少避免了两次数据事故。
第四,批次跑的时候控制并发。如果你要把这套流程用在几十家公司上,不要一个接一个地全速跑,也不要一次性开太多并发。我实测下来,并发控制在5个以内最稳,既不会触发平台的限流,也不会因为资源争抢导致超时。
5.3 从财报到其他文档的扩展思路
方案跑通之后,我第一个反应是它不应该只停留在财报上。招股书里的“公司股权结构”章节、研报里的“盈利预测表”、债券募集说明书里的“主要财务指标”,本质上都是同一类任务:长文档里的关键信息抽取。
扩展方式很简单,核心变动只有两处:一处是TextIn的解析参数按需调整(比如招股书页码更多,需要调整超时),另一处是大模型的Prompt里换一套指标体系。工作流的骨架完全不用动。
我自己后续的计划是把它做成一个定时任务:每月末自动拉取指定公司的财报PDF,跑完抽取后把结果写进数据库,这样分析团队每天看到的就是自动更新的结构化数据,而不是一堆等着人工整理的原始PDF。这也是这套“5分钟方案”真正从临时脚本走向长期工具的关键一步。
我个人在实际操作中的体会是:自动化工具最怕的不是跑不通,而是跑通了但没人敢信它的结果。所以每一步都要留证据,每一条数据都要可追溯。TextIn保证了“读得准”,Coze保证了“流程稳”,而真正让这套方案在公司里被接受的,是那些日志节点和校验节点带来的信任感。如果你也正在搭类似的抽取流程,我建议你从一开始就把“可验证性”当成和“抽取率”一样重要的指标。