ストーリー
田
田中VPoE
Document AIの基礎を学んだ。次は最も需要の高い「請求書処理」と「契約書処理」の具体的な実装を見ていこう
あなた
経理部が月3,000件の請求書を手作業で処理しているやつですね
あ
田
田中VPoE
そうだ。請求書は定型・準定型・非定型の3パターンがある。それぞれに対して最適な処理戦略を使い分けることで、高い精度と効率を両立できる
田
田中VPoE
加えて契約書処理は、条項の解釈やリスク分析までAIで支援できる。法務部の業務効率化にも直結する重要なテーマだ
請求書処理の戦略
文書パターン別の処理戦略
| パターン | 割合 | 特徴 | 処理戦略 |
|---|
| 定型 | 60% | 主要取引先30社の固定フォーマット | テンプレートマッチング + ルールベース |
| 準定型 | 30% | フォーマットは異なるが項目は共通 | VLM + 構造化プロンプト |
| 非定型 | 10% | 海外取引先、特殊フォーマット | VLM + 人的確認 |
定型請求書のテンプレートマッチング
from dataclasses import dataclass
@dataclass
class InvoiceTemplate:
vendor_name: str
regions: dict # フィールド名 → 座標領域
# テンプレートDB
INVOICE_TEMPLATES = {
"株式会社ABC": InvoiceTemplate(
vendor_name="株式会社ABC",
regions={
"invoice_number": {"x": 400, "y": 80, "w": 200, "h": 30},
"invoice_date": {"x": 400, "y": 120, "w": 200, "h": 30},
"total_amount": {"x": 350, "y": 600, "w": 250, "h": 40},
}
),
}
def process_templated_invoice(image_path: str, vendor_name: str) -> dict:
"""テンプレートマッチングで定型請求書を処理"""
template = INVOICE_TEMPLATES.get(vendor_name)
if not template:
return None
from PIL import Image
img = Image.open(image_path)
result = {}
for field_name, region in template.regions.items():
# 領域をクロップしてOCR
cropped = img.crop((
region["x"], region["y"],
region["x"] + region["w"],
region["y"] + region["h"]
))
text = ocr_region(cropped) # OCR関数(省略)
result[field_name] = text
return result
VLMベースの請求書処理
def process_invoice_with_vlm(image_path: str) -> dict:
"""VLMで請求書を処理(準定型・非定型対応)"""
client = anthropic.Anthropic()
with open(image_path, "rb") as f:
image_data = base64.b64encode(f.read()).decode()
prompt = """この請求書から以下の情報を正確に抽出し、JSON形式で返してください。
注意事項:
- 金額は数値のみ(カンマ、円マーク、$マークは除去)
- 日付はYYYY-MM-DD形式
- 読み取れない/存在しない項目はnull
- 各フィールドの信頼度(0.0〜1.0)も付与してください
{
"vendor": {
"name": {"value": "", "confidence": 0.0},
"address": {"value": "", "confidence": 0.0},
"registration_number": {"value": "", "confidence": 0.0}
},
"invoice_number": {"value": "", "confidence": 0.0},
"invoice_date": {"value": "", "confidence": 0.0},
"due_date": {"value": "", "confidence": 0.0},
"line_items": [
{
"description": {"value": "", "confidence": 0.0},
"quantity": {"value": 0, "confidence": 0.0},
"unit_price": {"value": 0, "confidence": 0.0},
"amount": {"value": 0, "confidence": 0.0}
}
],
"subtotal": {"value": 0, "confidence": 0.0},
"tax": {"value": 0, "confidence": 0.0},
"total": {"value": 0, "confidence": 0.0}
}"""
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_data}},
{"type": "text", "text": prompt}
]
}]
)
return json.loads(response.content[0].text)
バリデーションとクロスチェック
数値の整合性検証
def validate_invoice(data: dict) -> dict:
"""請求書データの整合性を検証"""
errors = []
warnings = []
# 1. 明細合計と小計の一致確認
line_total = sum(
item["amount"]["value"]
for item in data.get("line_items", [])
if item["amount"]["value"] is not None
)
subtotal = data.get("subtotal", {}).get("value", 0)
if subtotal and abs(line_total - subtotal) > 1:
errors.append(f"明細合計({line_total})と小計({subtotal})が不一致")
# 2. 小計 + 税 = 合計の確認
tax = data.get("tax", {}).get("value", 0) or 0
total = data.get("total", {}).get("value", 0) or 0
if subtotal and total and abs((subtotal + tax) - total) > 1:
errors.append(f"小計({subtotal})+税({tax})と合計({total})が不一致")
# 3. 税率の妥当性チェック(日本の場合)
if subtotal and tax:
tax_rate = tax / subtotal
if not (0.08 <= tax_rate <= 0.10 + 0.01): # 8%〜10%(誤差1%許容)
warnings.append(f"税率が異常: {tax_rate:.2%}")
# 4. 日付の妥当性チェック
from datetime import datetime, timedelta
invoice_date = data.get("invoice_date", {}).get("value")
if invoice_date:
try:
dt = datetime.strptime(invoice_date, "%Y-%m-%d")
if dt > datetime.now():
warnings.append("請求日が未来日付")
if dt < datetime.now() - timedelta(days=365):
warnings.append("請求日が1年以上前")
except ValueError:
errors.append(f"日付形式が不正: {invoice_date}")
# 5. 信頼度チェック
low_confidence_fields = []
for field_name in ["invoice_number", "total", "vendor"]:
field = data.get(field_name, {})
if isinstance(field, dict) and field.get("confidence", 1.0) < 0.9:
low_confidence_fields.append(field_name)
if low_confidence_fields:
warnings.append(f"信頼度が低いフィールド: {', '.join(low_confidence_fields)}")
return {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
"needs_human_review": len(errors) > 0 or len(low_confidence_fields) > 0
}
契約書処理
契約書分析のアプローチ
def analyze_contract(document_path: str) -> dict:
"""契約書を分析"""
client = anthropic.Anthropic()
with open(document_path, "rb") as f:
doc_data = base64.b64encode(f.read()).decode()
prompt = """この契約書を分析し、以下の情報をJSON形式で出力してください。
{
"basic_info": {
"contract_type": "契約種別",
"parties": [{"name": "当事者名", "role": "甲/乙"}],
"effective_date": "YYYY-MM-DD",
"expiration_date": "YYYY-MM-DD",
"auto_renewal": true/false
},
"key_clauses": [
{
"clause_number": "条項番号",
"title": "条項タイトル",
"summary": "条項の要約",
"risk_level": "HIGH/MEDIUM/LOW",
"risk_reason": "リスクの理由(該当する場合)"
}
],
"financial_terms": {
"contract_value": 0,
"payment_terms": "支払条件",
"penalties": "違約金条項"
},
"termination": {
"notice_period": "解約通知期間",
"termination_conditions": ["解約条件"]
},
"risk_summary": {
"overall_risk": "HIGH/MEDIUM/LOW",
"key_risks": ["主要リスクのリスト"],
"recommendations": ["推奨事項"]
}
}"""
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": doc_data}},
{"type": "text", "text": prompt}
]
}]
)
return json.loads(response.content[0].text)
リスク分析のチェックポイント
| チェック項目 | リスクレベル判定基準 |
|---|
| 自動更新条項 | 解約通知期間が3ヶ月以上 → HIGH |
| 損害賠償上限 | 上限なし → HIGH |
| 知的財産権 | 成果物の権利帰属が不明確 → HIGH |
| 準拠法 | 海外法が適用 → MEDIUM |
| 競業避止 | 期間2年以上 → MEDIUM |
| 解約条件 | 理由なし解約不可 → MEDIUM |
処理パイプラインの統合
文書入力
│
▼
[文書分類] → 請求書/契約書/その他
│
├── 請求書 → [取引先識別] → 定型/準定型/非定型
│ ├── 定型 → テンプレートマッチング
│ ├── 準定型 → VLM抽出
│ └── 非定型 → VLM抽出 + 人的確認
│ │
│ ▼
│ [バリデーション] → [会計システム登録]
│
└── 契約書 → [VLM分析] → [リスク評価]
│
▼
[法務担当者レビュー] → [契約管理DB登録]
まとめ
| 項目 | 内容 |
|---|
| 請求書処理 | 定型→テンプレート、準定型→VLM、非定型→VLM+人的確認 |
| バリデーション | 数値整合性、税率、日付、信頼度のクロスチェック |
| 契約書分析 | 条項抽出 + リスクレベル判定 + 推奨事項生成 |
| 統合パイプライン | 分類 → 抽出 → 検証 → 登録 |
チェックリスト
推定所要時間: 30分