全国オンライン対応受付 平日 9:00-18:00
お問い合わせ

開発記録 / 帳票AI読取制作記 / Vol.35 帳票AI読取制作記 Vol.1

記事 02

FastAPI + PostgreSQLで読取結果を管理する設計

FAX1枚が「注文書」であり、その中に複数の「明細行」がある。

2026-06-17 公開

FAX1枚が「注文書」であり、その中に複数の「明細行」がある。

この2層の構造をどう持つか。スプレッドシートで管理しようとしたら、「1枚の注文書に対して行数が変わる」という問題にすぐ直面する。月次カレンダー形式のA社なら1枚に20〜30行。D社なら数行。行数が可変のデータはスプレッドシートで扱いづらい。

PostgreSQLを選んだのはそのためだ。


テーブル設計

3つのテーブルで管理する。

CREATE TABLE orders (
  id          SERIAL PRIMARY KEY,
  order_code  VARCHAR(64) UNIQUE NOT NULL,
  supplier    VARCHAR(128),
  status      VARCHAR(32) DEFAULT 'draft',
  created_at  TIMESTAMPTZ DEFAULT NOW(),
  updated_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE order_items (
  id            SERIAL PRIMARY KEY,
  order_id      INTEGER REFERENCES orders(id),
  product_name  VARCHAR(256),
  quantity      NUMERIC(10, 2),
  unit          VARCHAR(32),
  delivery_date DATE,
  confidence    VARCHAR(16) DEFAULT 'medium',
  note          TEXT,
  confirmed     BOOLEAN DEFAULT FALSE
);

CREATE TABLE order_images (
  id          SERIAL PRIMARY KEY,
  order_id    INTEGER REFERENCES orders(id),
  image_path  VARCHAR(512),
  page_no     INTEGER DEFAULT 1
);

order_codeは「FAX-20260606-MR01」のような形式にした。日付と取引先コードと連番を組み合わせる。同じ日に同じ取引先から複数枚届いても区別できる。

confidenceはClaude Visionが各行の読取に対して返す確信度だ。high・medium・lowの3段階。これを確認画面のUIで活用する。


APIの構成

POST /api/orders/upload     # 画像アップロード+AI読取開始
GET  /api/orders            # 注文書一覧
GET  /api/orders/{id}       # 注文書詳細(明細含む)
PATCH /api/orders/{id}/items/{item_id}  # 明細修正
POST /api/orders/{id}/confirm           # 注文書確認完了
GET  /api/orders/{id}/images/{page}    # 画像取得

RESTful な構造で、フロントエンドはvanilla JSで書いた静的HTMLが叩く。


非同期処理の設計

画像をアップロードしてからClaude Visionの処理が返るまで、10〜30秒かかることがある。これを同期で処理するとAPIがタイムアウトしてしまう。

FastAPIのBackgroundTasksを使った。アップロードが完了した時点でorder_idをフロントエンドに返し、Claude Vision処理はバックグラウンドで走る。

@app.post('/api/orders/upload')
async def upload_order(file: UploadFile, background_tasks: BackgroundTasks):
    order_id = save_file_and_create_record(file)
    background_tasks.add_task(process_with_ai, order_id)
    return {'order_id': order_id, 'status': 'processing'}

フロントエンドはポーリング(3秒ごとに状態確認)で「processingからdraftへの変化」を検知する。変化を検知したら確認画面に切り替わる。


Claude Vision呼び出し

import anthropic, base64

client = anthropic.Anthropic()

def read_fax_image(image_path: str, supplier: str) -> list[dict]:
    with open(image_path, 'rb') as f:
        image_data = base64.standard_b64encode(f.read()).decode('utf-8')

    message = client.messages.create(
        model='claude-opus-4-8',
        max_tokens=4096,
        messages=[{
            'role': 'user',
            'content': [
                {
                    'type': 'image',
                    'source': {
                        'type': 'base64',
                        'media_type': 'image/jpeg',
                        'data': image_data,
                    },
                },
                {'type': 'text', 'text': build_prompt(supplier)}
            ]
        }]
    )

    return parse_response(message.content[0].text)

プロンプトは取引先ごとに変える。カレンダー形式のA社と、丸印形式のD社では、帳票の読み方の説明が根本的に違うからだ。


設計を決めてから開発する理由

このシステムは「動く」だけでなく「担当者が毎日使える」ことが条件だ。スキーマ設計を後から変えると、既存データの移行が必要になる。order_codeのUNIQUE制約・confidenceカラム・confirmedフラグ。これらは機能要件から逆算して最初から入れた。

後から追加するより、最初から正しく設計する方が結果的に速い。


*次回は「Claude Visionで帳票を読む——4種の帳票と精度の違い」*

*シンプルシステム株式会社 代表 伊藤勝彦*