用 Pulse AI + Amazon Bedrock 搭建金融文档抽取与微调流水线

2026-05-14 27 预计阅读时间:1 分钟
来源:aws.amazon.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:15 分钟

金融文档是出了名的难啃——年报里表格嵌套、注释跨页、格式千差万别;招股书里术语密集、数据点散落各处。传统 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 上的模型做两件事:

  1. 校验:检查抽取结果是否逻辑自洽(比如资产负债表左右是否平衡)。
  2. 补全:对缺失字段或模糊表述做推断(比如"收入同比增长 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 终端节点,可以避免数据经公网传输。
  • 幻觉风险:模型补全缺失字段时可能"编造"数值。对关键财务指标,务必保留硬校验或人工复核环节,不要完全依赖模型输出。

一条可跑的端到端检查清单

把上面的片段串起来,落地时的最小闭环是:

  1. 选 5-10 份典型文档,用 Pulse AI 抽取,人工检查结构化 JSON 的准确率。
  2. 对抽取结果写硬校验脚本(算术一致性检查),跑一遍看哪些规则需要加。
  3. 对硬校验无法覆盖的语义问题,用 Bedrock Claude + Prompt 做校验,对比人工结论。
  4. 把步骤 1-3 中发现的错误案例整理成标注集,达到 200+ 条后考虑微调。
  5. 微调完成后,用预留的测试集评估提升幅度,如果提升 < 5%,回到步骤 3 优化 Prompt,而不是继续加 epoch。

金融文档处理不是一次性项目,而是持续优化的流水线。Pulse AI 解决"看懂结构",Bedrock 解决"理解语义",两者组合的真正价值在于:每一次处理的结果——无论成功还是失败——都可以成为下一轮优化的输入。


相关推荐