vlm.md
← 所有食谱 · 文档理解 · 进阶

从合同图片中提取关键条款

让 agent 从扫描合同中自动提取甲乙方、金额、履约期限、违约条款等核心字段,输出结构化 JSON,对接审批流或风控系统。

2026/5/13 · vlm.md · 推荐模型: Claude 3.5 SonnetGPT-4oGemini 1.5 Pro

场景

你的 agent 需要处理法务或采购团队上传的合同扫描件,自动提取:

  • 甲方、乙方名称
  • 合同金额与货币
  • 合同签署日期、生效日期、到期日期
  • 主要履约义务(简短摘要)
  • 违约金条款(如有)
  • 管辖法院/仲裁机构

提取结果进入审批系统或合同台账,人工只需核对异常项。

推荐模型

模型适用场景
Claude 3.5 Sonnet长文档理解最稳,条款语义边界识别准确,首选
GPT-4o中文合同支持好,速度快,适合批量处理
Gemini 1.5 Pro超长合同(>20页)用 1M context 版本性价比最高

合同文本密度高、格式多变,Claude 的长上下文理解比其他模型更稳定。

Prompt 模板

你是一个合同信息提取专家。请从图片中提取以下字段,严格以 JSON 格式返回,不要有任何解释文字。

字段说明:
- party_a: 甲方全称(合同中标注为"甲方"或"委托方"的一侧)
- party_b: 乙方全称
- contract_amount: 合同金额(数字,不含货币符号;如有多个金额取总价)
- currency: 货币代码(CNY / USD / EUR 等)
- signing_date: 签署日期(YYYY-MM-DD,如无则 null)
- effective_date: 生效日期(YYYY-MM-DD,如无则与签署日期相同)
- expiry_date: 合同到期或终止日期(YYYY-MM-DD,如无则 null)
- obligations_summary: 乙方主要履约义务,50字以内概括
- penalty_clause: 违约金条款原文摘录(50字以内),如无则 null
- jurisdiction: 争议管辖法院或仲裁机构名称,如无则 null

字段不存在时返回 null,不要猜测或补全。

代码示例

import anthropic
import base64
from pathlib import Path

client = anthropic.Anthropic()

PROMPT = """你是一个合同信息提取专家。请从图片中提取以下字段,严格以 JSON 格式返回,不要有任何解释文字。

字段说明:
- party_a: 甲方全称
- party_b: 乙方全称
- contract_amount: 合同金额(数字)
- currency: 货币代码
- signing_date: 签署日期(YYYY-MM-DD)
- effective_date: 生效日期(YYYY-MM-DD)
- expiry_date: 到期日期(YYYY-MM-DD)
- obligations_summary: 乙方主要履约义务,50字内
- penalty_clause: 违约金条款摘录,50字内
- jurisdiction: 管辖法院或仲裁机构

字段不存在时返回 null。"""


def extract_contract(image_path: str) -> dict:
    data = base64.standard_b64encode(Path(image_path).read_bytes()).decode()
    suffix = Path(image_path).suffix.lower().lstrip(".")
    media_type = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png"}.get(
        suffix, "image/jpeg"
    )

    message = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1024,
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "image",
                        "source": {"type": "base64", "media_type": media_type, "data": data},
                    },
                    {"type": "text", "text": PROMPT},
                ],
            }
        ],
    )

    import json, re
    raw = message.content[0].text.strip()
    # 去掉可能的 markdown 代码块
    raw = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw, flags=re.MULTILINE).strip()
    return json.loads(raw)


if __name__ == "__main__":
    import json
    result = extract_contract("contract.jpg")
    print(json.dumps(result, ensure_ascii=False, indent=2))

预期输出:

{
  "party_a": "北京某某科技有限公司",
  "party_b": "上海某某信息服务有限公司",
  "contract_amount": 500000,
  "currency": "CNY",
  "signing_date": "2024-03-01",
  "effective_date": "2024-03-01",
  "expiry_date": "2025-02-28",
  "obligations_summary": "乙方须于合同生效后30日内完成系统部署,并提供12个月运维支持",
  "penalty_clause": "乙方逾期交付每日按合同总价0.1%支付违约金,最高不超过10%",
  "jurisdiction": "北京市海淀区人民法院"
}

踩坑记录

坑 1:甲乙方认定混乱

合同中”甲方”有时是买方,有时是服务方,模型偶尔会混淆。加一句说明:“以合同第一条中明确标注’甲方’的主体为准,不要根据常识推断”。

坑 2:金额出现多处,提取错误

合同里通常有预付款、尾款、总价多个金额。Prompt 里必须明确说”取合同总价”,否则模型可能返回任意一个金额。如果需要分期金额,单独让模型提取 payment_schedule 数组。

坑 3:扫描件分辨率不足导致文字模糊

合同扫描件低于 150 DPI 时,模型对细小数字(如违约金比例)的识别率明显下降。建议在传图前检查:

from PIL import Image

def check_dpi(path: str) -> int:
    with Image.open(path) as img:
        dpi = img.info.get("dpi", (72, 72))
        return int(dpi[0])

# dpi < 150 时提示用户重新扫描
if check_dpi("contract.jpg") < 150:
    print("警告:扫描分辨率过低,提取结果可能不准确")

坑 4:多页合同只传了首页

关键条款(尤其违约金、管辖条款)通常在合同末尾。如果只传首页,这些字段会返回 null。多页合同需要合并所有页再提取,或分页提取后合并结果(以非 null 值优先)。