金融文档是出了名的难啃——年报里表格嵌套、注释跨页、格式千差万别;招股书里术语密集、数据点散落各处。传统 OCR 能把像素变成文字,却分不清"净利润"和"归属于母公司股东的净利润",更无法把第 87 页的附注和第 12 页的主表关联起来。这篇文章来看一条更务实的路径:用 Pulse AI 的文档理解能力做结构化抽取,再用 Amazon Bedrock 上的大模型做语义补全与微调,组成一条端到端的金融文档处理流水线。
为什么金融文档需要"两层"处理
金融文档的难点不在"识别字",而在"理解结构":
- 表格层次复杂:一张资产负债表可能包含合并与母公司两列,子项层层缩进,OCR 只能得到扁平文本。
- 跨页引用:审计报告中的"详见附注 X"需要跳转到另一页才能拿到完整上下文。
- 术语歧义:同一份文件里"收入"可能指营业收入、总收入或净收入,取决于上下文。
Pulse AI 专注第一层——把 PDF 里视觉上的表格、段落、标题还原成带语义标签的结构化对象(不是纯文本流)。Amazon Bedrock 专注第二层——对已经结构化的内容做语义补全、校验、摘要,甚至通过微调让模型学会特定行业或客户的表述习惯。两层叠加,才能从"像素"走到"可用的财务指标"。
整体架构:抽取 → 校验 → 微调
一条可落地的流水线大致如下:
PDF 文档
│
▼
Pulse AI 文档理解服务
│ → 输出:结构化 JSON(表格、段落、键值对、跨页链接)
▼
预处理脚本(清洗、字段映射、对齐)
│
▼
Amazon Bedrock
│ ├─ 推理:用 Claude / Titan 等模型做语义校验与补全
│ ├─ 微调:用抽取结果构建训练集,定制化模型
▼
下游系统(数据库、风控引擎、报表平台)
关键设计决策:不要让大模型直接读 PDF 原文。PDF 的视觉噪声太多,模型容易在格式细节上浪费注意力。先让 Pulse AI 把文档拆成干净的结构化片段,再喂给 Bedrock 上的模型,准确率和成本都会更好。
Pulse AI 抽取:从 PDF 到结构化 JSON
Pulse AI 的核心能力是把视觉文档还原成语义对象。调用方式是 REST API,下面是一个可改造的 Python 示例,演示如何上传 PDF 并获取结构化结果:
import requests
import json
import time
PULSE_API_URL = "https://api.pulseai.example.com/v1/documents"
PULSE_API_KEY = "your-pulse-api-key" # 替换为真实密钥
def extract_financial_document(pdf_path: str) -> dict:
"""上传 PDF 到 Pulse AI,等待结构化抽取完成,返回 JSON 结果。"""
# 1. 上传文档
with open(pdf_path, "rb") as f:
upload_resp = requests.post(
f"{PULSE_API_URL}/upload",
headers={"Authorization": f"Bearer {PULSE_API_KEY}"},
files={"file": (pdf_path, f, "application/pdf")},
data={"doc_type": "annual_report"} # 指定文档类型,提升抽取精度
)
upload_resp.raise_for_status()
doc_id = upload_resp.json()["document_id"]
# 2. 等待处理完成(轮询,生产环境建议用 webhook)
for _ in range(60):
status_resp = requests.get(
f"{PULSE_API_URL}/{doc_id}/status",
headers={"Authorization": f"Bearer {PULSE_API_KEY}"},
)
status = status_resp.json()["status"]
if status == "completed":
break
if status == "failed":
raise RuntimeError(f"Pulse AI 处理失败: {status_resp.json()['error']}")
time.sleep(5)
# 3. 获取结构化结果
result_resp = requests.get(
f"{PULSE_API_URL}/{doc_id}/result",
headers={"Authorization": f"Bearer {PULSE_API_KEY}"},
)
result_resp.raise_for_status()
return result_resp.json()
# 使用示例
result = extract_financial_document("annual_report_2024.pdf")
# result 的典型结构(示意):
# {
# "tables": [
# {
# "type": "balance_sheet",
# "page": 12,
# "rows": [
# {"label": "资产总计", "merged": "5,832,400", "parent_only": "3,210,800"},
# ...
# ]
# }
# ],
# "key_value_pairs": [
# {"key": "审计意见", "value": "无保留意见", "page": 87}
# ],
# "cross_references": [
# {"from_page": 12, "from_text": "详见附注15", "to_page": 87, "to_section": "应收账款"}
# ]
# }
print(json.dumps(result, indent=2, ensure_ascii=False))
注意:上面的 API URL 和字段名是示意,实际接入时需要对照 Pulse AI 的官方文档替换。
doc_type参数值得留意——标注为annual_report后,Pulse AI 会启用针对年报的抽取规则(比如识别"合并/母公司"双列结构),精度显著高于通用模式。
Bedrock 推理:语义校验与补全
拿到结构化 JSON 后,下一步是用 Bedrock 上的模型做两件事:
- 校验:检查抽取结果是否逻辑自洽(比如资产负债表左右是否平衡)。
- 补全:对缺失字段或模糊表述做推断(比如"收入同比增长 X%",原文只给了绝对数,模型可以算出百分比)。
下面用 Boto3 调用 Bedrock 的 Claude 模型,对抽取结果做校验:
import boto3
import json
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
MODEL_ID = "anthropic.claude-3-5-sonnet-20241022-v2:0"
def validate_balance_sheet(extracted_tables: list) -> dict:
"""用 Bedrock Claude 校验资产负债表抽取结果的逻辑一致性。"""
# 找到资产负债表
bs_table = next(t for t in extracted_tables if t["type"] == "balance_sheet")
# 构造 prompt——把结构化数据塞进去,让模型做校验
prompt = f"""你是一名资深财务分析师。以下是从年报中抽取的资产负债表结构化数据:
{json.dumps(bs_table["rows"], indent=2, ensure_ascii=False)}
请逐项检查:
1. 资产总计 = 流动资产合计 + 非流动资产合计?
2. 负债总计 = 流动负债合计 + 非流动负债合计?
3. 资产总计 = 负债总计 + 所有者权益合计?
4. 合并列与母公司列之间的差异是否合理?
如果发现不一致,指出具体行和数值。如果一致,给出确认结论。"""
body = {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 2048,
"messages": [
{"role": "user", "content": prompt}
]
}
response = bedrock.invoke_model(
modelId=MODEL_ID,
body=json.dumps(body),
contentType="application/json",
accept="application/json",
)
result = json.loads(response["body"].read())
return {
"validation_result": result["content"][0]["text"],
"input_tokens": result["usage"]["input_tokens"],
"output_tokens": result["usage"]["output_tokens"],
}
# 使用示例
validation = validate_balance_sheet(result["tables"])
print(validation["validation_result"])
print(f"Token 消耗: 输入 {validation['input_tokens']}, 输出 {validation['output_tokens']}")
这个模式的好处是:校验逻辑不用硬编码。硬编码检查"资产=负债+权益"看似简单,但真实年报里会有少数股东权益、其他综合收益等变数,规则越写越脆弱。让模型基于抽取数据做语义校验,覆盖面更广,维护成本更低。
Bedrock 微调:让模型学会你的文档风格
当处理量达到数百甚至上千份同类型文档后,通用模型的校验和补全可能仍会犯一些"行业特有"的错误——比如某家银行总是把"贷款减值准备"写成"减值准备-贷款",通用模型不一定能对上。这时候可以用 Bedrock 的微调能力,用已校验的高质量抽取结果做训练集。
以下是准备微调数据集并提交训练任务的示例:
import boto3
import json
bedrock = boto3.client("bedrock", region_name="us-east-1")
# 1. 构造微调数据集(从已校验的抽取结果中生成)
def build_finetune_dataset(extracted_results: list[dict]) -> list[dict]:
"""把多份文档的抽取+校验结果转成 Bedrock 微调格式。"""
training_examples = []
for doc_result in extracted_results:
# 每份文档生成多条 instruction-following 样本
for table in doc_result["tables"]:
example = {
"system": "你是一名金融文档分析专家,擅长从结构化数据中提取和校验财务指标。",
"messages": [
{
"role": "user",
"content": f"请从以下结构化表格中提取关键财务指标并校验一致性:\n{json.dumps(table['rows'], ensure_ascii=False)}"
},
{
"role": "assistant",
# 这里填入人工校验后的正确输出
"content": table["validated_summary"] # 需提前标注
}
]
}
training_examples.append(example)
return training_examples
# 2. 上传训练集到 S3
s3 = boto3.client("s3")
BUCKET = "my-financial-ml-datasets"
dataset = build_finetune_dataset(verified_results) # verified_results 是已标注数据
s3.put_object(
Bucket=BUCKET,
Key="finetune/financial_extraction_train.jsonl",
Body="\n".join(json.dumps(ex, ensure_ascii=False) for ex in dataset),
)
# 3. 创建微调任务
training_job = bedrock.create_model_customization_job(
jobName="financial-doc-finetune-v1",
customModelName="financial-doc-custom-v1",
baseModelIdentifier="anthropic.claude-3-5-sonnet-20241022-v2:0",
trainingDataConfig={
"s3Uri": f"s3://{BUCKET}/finetune/financial_extraction_train.jsonl"
},
outputDataConfig={
"s3Uri": f"s3://{BUCKET}/finetune/output/"
},
hyperParameters={
"epochCount": "3",
"learningRate": "0.0001",
"batchSize": "4",
},
roleArn="arn:aws:iam::123456789012:role/BedrockFineTuneRole", # 替换为你的 IAM Role
)
print(f"微调任务已创建: {training_job['jobArn']}")
微调的投入门槛:Bedrock 微调需要至少几百条高质量标注样本,每条样本都要有"模型应该输出的正确答案"。在早期阶段,更务实的做法是先用通用模型 + Prompt Engineering 跑通流水线,积累标注数据,等样本量够了再微调。微调不是第一步,是优化步。
落地时的几个关键决策
| 决策点 | 建议 | 原因 |
|---|---|---|
是否指定 doc_type |
是 | Pulse AI 对年报、招股书、审计报告有专用规则,精度远高于通用模式 |
| 先微调还是先 Prompt | 先 Prompt | Prompt 调优成本低、迭代快;微调需要标注数据积累 |
| 校验用硬规则还是模型 | 混合 | 简单算术校验(资产=负债+权益)用代码硬检查;语义歧义用模型 |
| 处理失败文档的策略 | 人工兜底 + 回流标注 | 失败案例是最好的微调训练素材,不要丢弃 |
| Bedrock 区域选择 | 选文档存储同区域 | 减少数据传输延迟和跨区域合规风险 |
风险与边界
- Pulse AI 的覆盖范围:并非所有金融文档类型都有专用规则,对某些小众格式(比如私募基金的非标准季报),抽取精度可能回落到通用水平,需要额外验证。
- Bedrock 微调成本:Claude 等模型的微调按训练时长计费,训练集太大或 epoch 过多都会显著增加成本。建议先用小数据集试跑,观察验证集指标再决定是否扩大。
- 数据合规:金融文档通常包含敏感信息,上传到 Pulse AI 和 Bedrock 前需要确认数据驻留策略和加密要求。Bedrock 支持 VPC 终端节点,可以避免数据经公网传输。
- 幻觉风险:模型补全缺失字段时可能"编造"数值。对关键财务指标,务必保留硬校验或人工复核环节,不要完全依赖模型输出。
一条可跑的端到端检查清单
把上面的片段串起来,落地时的最小闭环是:
- 选 5-10 份典型文档,用 Pulse AI 抽取,人工检查结构化 JSON 的准确率。
- 对抽取结果写硬校验脚本(算术一致性检查),跑一遍看哪些规则需要加。
- 对硬校验无法覆盖的语义问题,用 Bedrock Claude + Prompt 做校验,对比人工结论。
- 把步骤 1-3 中发现的错误案例整理成标注集,达到 200+ 条后考虑微调。
- 微调完成后,用预留的测试集评估提升幅度,如果提升 < 5%,回到步骤 3 优化 Prompt,而不是继续加 epoch。
金融文档处理不是一次性项目,而是持续优化的流水线。Pulse AI 解决"看懂结构",Bedrock 解决"理解语义",两者组合的真正价值在于:每一次处理的结果——无论成功还是失败——都可以成为下一轮优化的输入。