Jev Recipe / ドキュメント処理

Jev でドキュメント分類:分割してから判定する 2 段階パイプライン

メールに添付 3 件、スキャンに請求書 8 ページと契約書 1 ページが混在——まず Noul のページ単位境界判定でパケットを分割し、それから型付き Choice で 1 通ずつ分類します。較正済み信頼度がレビュー待ちへ自動振り分け。

文書パケット 2 段階パイプライン構築 6 ステップ

01コードを書く前に 2 つの軸を定義する。分割軸:何が「新しい文書の始まり」か——新しい請求書番号、署名欄、レイアウトの切り替わり。タイプ軸:請求書、契約書、領収書、文書・メール、その他。どちらも、オペレーションチームが手作業でファイリングするときに普段使う語彙で criteria に書き起こす。
02State は最小限のテキストに。抽出器が吐いたページ単位のテキストと、判定に実際に影響するメタデータ——page_index、ソース(メール / スキャナ / FAX)、OCR を実行したかどうか——だけを渡す。Jev が読むのはピクセルではなくテキストであり、モデル内に OCR もレイアウト解析もないため、抽出は上流で行い、その出力が入力になる。
032 段階をページごとの 1 回の evaluate 呼び出しにまとめる。Noul の質問がパケットの境界を決め(is_single_document、needs_ocr)、doc_type Choice がそのページの属するセグメントにタイプラベルを付ける。答えはすべて較正済み信頼度付きで、約 100〜500ms の単一フォワードパスで返る。ページは並列に走るので、パケットが大きくなってもウォールタイムはほぼ横ばいだ。
04振り分けポリシーはコードに置く。doc_type の信頼度 0.85 以上で自動ファイリング、0.60〜0.85 はレビューキューへ、0.60 未満——さらにすべての「other」ラベルと needs_ocr ページ——は人手へエスカレーション。信頼度ゲート付きフォールバックチェーンと同じ 3 レーンの規律であり、カットオフはラベル付きパケットから導出するのであって、当て推量ではない。
05コミットする前にバッチ計算をする。State は入力トークンのみの課金($0.042/M)で、出力は無料。9 ページのパケットは 1 ページ約 300 トークンとして約 $0.0001、1 万パケットの既存アーカイブスキャンでも 1 ドルに届かない。エコシステムの公開データポイントでは、1,018 本の研究論文の分類合計が $0.08 だった。
06ラベルだけでなく境界を監視する。is_single_document が中間帯に落ちる頻度を追い、オペレーションの修正をラベル付きサンプルへ還流させ、四半期ごとにしきい値を再検証する。新しいベンダーの新しいレイアウトは分布シフトであり、フォールバックチェーンが再較正するのと同じドリフトである。
schema / パケット分割・分類コントラクト
{
  "is_single_document": {
    "type": "noul",
    "instructions": "このページは新しい文書の始まりですか、前のページの文書の続きですか?"
  },
  "needs_ocr": {
    "type": "noul",
    "instructions": "このページのテキストは欠落または判読不能で、信頼できる判定の前に OCR が必要ですか?"
  },
  "doc_type": {
    "type": "choice",
    "instructions": "この文書をちょうど 1 つのタイプに分類する",
    "criteria": {
      "invoice": "明細請求:行項目、数量、合計、税、支払条件",
      "contract": "拘束力のある合意:当事者、条項、期間、署名欄",
      "receipt": "支払完了の証憑:支払済みマーク、取引参照番号、未払いなし",
      "letter": "人同士の連絡文書:頭語、本文、署名ブロック",
      "other": "上記のいずれも確信を持って当てはまらない——無理にラベルを付けずエスカレーション"
    }
  }
}
実際のページを State として送ると、is_single_document・needs_ocr・doc_type の較正済み信頼度を確認できます。

文書タイプ軸:5 つのラベルと、設計されたフォールバック

分類の品質はラベルセットから始まります。以下の 5 タイプでバックオフィス文書の大部分をカバーできます。6 枚目のカードが最も重要です。無理にラベルを付けるのではなくエスカレーションする、設計された「その他」です。tell はそのまま Choice スキーマの criteria 語彙に使えます。軸は小さく保ってください。ラベルを 1 つ増やすたびに、確率質量は 1 分ずつ薄まります。

請求書

未払いの金銭——パケットで最もよく見るペイロード。

  • 数量と単価付きの行項目
  • ヘッダーやフッターの請求書番号
  • 税額と支払合計
  • 支払条件:締め 30 日、支払期日、振込先
  • 発行者と請求先のブロック

契約書

拘束力のある合意——リスクが高いため、レビューレーンの価値がある。

  • 前文に名前のある当事者
  • 番号付き条項と定義セクション
  • 期間、発効日、更新日
  • 氏名と日付入り署名欄
  • 定型の法務ヘッダーとページ番号

領収書

支払の証拠——請求書と最も混同されるタイプ。

  • 「支払済み」や取引完了のマーク
  • 端末や POS のヘッダーと店舗情報
  • 短い行項目で支払条件はない
  • カード下 4 桁または支払参照番号
  • 未払いゼロ——支払済み、残高なし

文書・メール対応

人同士の連絡——表紙メール、メモ、通知文。

  • 頭語と結語に挟まれた自由な本文
  • 本文中の依頼または通知事項
  • 役職と連絡先入り署名ブロック
  • メール本文の引用返信チェーン
  • 請求フィールドはどこにもない

フォーム・申込書

構造化された収集——申請、請求、オンボーディング。

  • ラベル付きフィールドに固定位置の値
  • チェックボックス、ドロップダウン、選択マーク
  • フォーム設計由来の番号付きセクション見出し
  • 手書き欄や署名ボックス
  • 「N ページ / M ページ中」のフッター

その他——設計されたフォールバック

失敗ではなくラベル:強制せず、振り分ける。

  • 上記のどの基準にも当てはまらない文書
  • 混在または空の抽出出力
  • すべてのラベルで信頼度がしきい値未満
  • 新しいベンダーの未知のレイアウト
  • エラーではなくワークキューとして扱う
インタラクティブデモ / 文書パケット分割

文書パケット 2 段階分割・分類シミュレーター

パケットを選んで 2 段階パイプラインを実行。ページ単位 Noul で境界を判定 → セグメント分割 → Choice でタイプと信頼度レーンを判定(フロントエンドのシミュレーションで、実 API は呼びません):

is_single_document + needs_ocr + doc_type
4 ページ
p1

差出人 ops@acme.co——「今月の請求書、カード領収書、署名済み MSA 更新文を添付します。」

p2

INVOICE #2026-0341 · ACME Supplies Ltd. · 14 明細 · 小計 $11,618.32 · VAT 7.4% · 支払合計 $12,480.00 · 締め 30 日

p3

領収書 — 支払済み · Visa ****4218 · $12,480.00 · 参照 TXN-88213 · 2026-03-02 · 未払いなし

p4

マスターサービス契約 · Acme Co. × Beta LLC · 期間 2026-04-01〜2028-03-31 · 署名:締結済み

source: 受信 / ops@acme.co
Jev 判定出力
~148ms / ≈$0.0001

「分割 + 分類を実行」を押すと、境界判定・セグメント分割・各文書のタイプ信頼度を表示します。

コード側のポリシー: is_single_document = true → 新セグメント開始 · needs_ocr または doc_type = other → 人手 · 信頼度 ≥ 0.85 → 自動ファイリング · 0.60〜0.85 → レビューキュー · < 0.60 → 人手レビュー

文書パケット分類比較:Jev 2 段階パイプライン vs 1 本の長い LLM プロンプト

METRIC
Jev
1 本の長い生成系 LLM プロンプト
パケット分割精度
ページ単位 Noul+較正済み信頼度——争われた境界は推測せずレビューへ
パケット全体を 1 回の長いプロンプトで推測——間違った分割まで自信ありげに届く
タイプラベルの信頼性
バージョン管理された criteria に基づく型付き Choice——スキーマエラー 0%、解析不要
自由テキストの判定——2〜8% が不正 JSON、パースとリトライが必要
パケットあたりレイテンシ
1 ページ約 100〜500ms、ページは並列——9 ページのスキャンでも約 500ms 以内
全ページの判定をトークン単位で生成して 2〜6 秒
バッチコスト
9 ページパケットあたり約 $0.0001——入力のみ課金 $0.042/M、出力無料
ページごとに入力費を重複課金+判定とリトライごとの出力トークン
ハルシネーションリスク
生成ゼロ——モデルはページのテキストも金額もスキーマ外のラベルも捏造できない
行項目を言い換えたり捏造したりする可能性——抽出金額はすべて別途検証が必要
混在レイアウトへの頑健性
ページ独立——1 ページの抽出不良は 1 判定だけに影響し、needs_ocr で検出される
コンテキスト希釈——9 ページのノイズが重要な 1 条項を埋もれさせる

実装コード:TypeScript / Python のパケットパイプライン

typescript / 2 段階の分割と分類
const JEV_ENDPOINT = "https://api.typesafe.ai/v1/jev/evaluate";

// ポリシーはアプリケーションコードに置く。モデルは事実を報告するだけ。
const AUTO_FILE_CONFIDENCE = 0.85; // 0.85 以上 → 自動ファイリング
const REVIEW_MIN_CONFIDENCE = 0.6; // 0.60〜0.85 → レビュー、未満 → 人手

const QUESTIONS = {
  is_single_document: {
    type: "noul",
    instructions:
      "このページは新しい文書の始まりですか、前のページの文書の続きですか?",
  },
  needs_ocr: {
    type: "noul",
    instructions:
      "このページのテキストは欠落または判読不能で、信頼できる判定の前に OCR が必要ですか?",
  },
  doc_type: {
    type: "choice",
    instructions: "この文書をちょうど 1 つのタイプに分類する",
    criteria: {
      invoice: "明細請求:行項目、数量、合計、税、支払条件",
      contract: "拘束力のある合意:当事者、条項、期間、署名欄",
      receipt: "支払完了の証憑:支払済みマーク、取引参照番号、未払いなし",
      letter: "人同士の連絡文書:頭語、本文、署名ブロック",
      other: "上記のいずれも確信を持って当てはまらない",
    },
  },
} as const;

export type DocLane = "auto_file" | "review" | "human_review";

export async function evaluatePage(page: {
  text: string;
  page_index: number;
  source: string;
  has_ocr: boolean;
}) {
  const response = await fetch(JEV_ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ state: page, questions: QUESTIONS }),
  });
  if (!response.ok) throw new Error("Jev evaluate failed");
  const data = await response.json();

  const { is_single_document, needs_ocr, doc_type } = data;
  const lane: DocLane =
    needs_ocr.answer || doc_type.answer === "other"
      ? "human_review"
      : doc_type.confidence >= AUTO_FILE_CONFIDENCE
        ? "auto_file"
        : doc_type.confidence >= REVIEW_MIN_CONFIDENCE
          ? "review"
          : "human_review";

  return {
    page_index: page.page_index,
    starts_document: is_single_document.answer,
    boundary_confidence: is_single_document.confidence,
    needs_ocr: needs_ocr.answer,
    doc_type: doc_type.answer,
    doc_type_confidence: doc_type.confidence,
    lane,
  };
}

// 境界とタイプの答えは同じ呼び出しで返る。starts_document = true の
// ページが新しいセグメントを開き、残りは現在のセグメントに蓄積。
export function splitPacket(pages: Awaited<ReturnType<typeof evaluatePage>>[]) {
  const segments: (typeof pages)[] = [];
  for (const page of pages) {
    if (page.starts_document || segments.length === 0) segments.push([page]);
    else segments[segments.length - 1].push(page);
  }
  return segments;
}

// ページは互いに独立——本番では並列にファンアウト。9 ページのパケット
// は約 500ms 以内、入力トークンのみ課金、スキーマエラー 0%。
const results = await Promise.all(packetPages.map(evaluatePage));
const segments = splitPacket(results);
const reviewQueue = results.filter((r) => r.lane !== "auto_file");
python / 受信スキャンキューの一括分類
import requests

JEV_ENDPOINT = "https://api.typesafe.ai/v1/jev/evaluate"
AUTO_FILE_CONFIDENCE = 0.85   # 0.85 以上 → 自動ファイリング
REVIEW_MIN_CONFIDENCE = 0.60  # 0.60〜0.85 → レビューキュー、未満 → 人手

QUESTIONS = {
    "is_single_document": {
        "type": "noul",
        "instructions": "このページは新しい文書の始まりですか、前のページの文書の続きですか?",
    },
    "needs_ocr": {
        "type": "noul",
        "instructions": "このページのテキストは欠落または判読不能で、信頼できる判定の前に OCR が必要ですか?",
    },
    "doc_type": {
        "type": "choice",
        "instructions": "この文書をちょうど 1 つのタイプに分類する",
        "criteria": {
            "invoice": "明細請求:行項目、数量、合計、税、支払条件",
            "contract": "拘束力のある合意:当事者、条項、期間、署名欄",
            "receipt": "支払完了の証憑:支払済みマーク、取引参照番号、未払いなし",
            "letter": "人同士の連絡文書:頭語、本文、署名ブロック",
            "other": "上記のいずれも確信を持って当てはまらない",
        },
    },
}

def evaluate_page(page: dict) -> dict:
    resp = requests.post(
        JEV_ENDPOINT,
        json={"state": page, "questions": QUESTIONS},
        timeout=5,
    )
    resp.raise_for_status()
    data = resp.json()
    doc = data["doc_type"]

    if data["needs_ocr"]["answer"] or doc["answer"] == "other":
        lane = "human_review"
    elif doc["confidence"] >= AUTO_FILE_CONFIDENCE:
        lane = "auto_file"
    elif doc["confidence"] >= REVIEW_MIN_CONFIDENCE:
        lane = "review"
    else:
        lane = "human_review"

    return {
        "page_index": page["page_index"],
        "starts_document": data["is_single_document"]["answer"],
        "doc_type": doc["answer"],
        "confidence": doc["confidence"],
        "lane": lane,
    }

def classify_packet(pages: list[dict]) -> list[list[dict]]:
    # ページは互いに独立——本番では並列にファンアウト。
    results = [evaluate_page(p) for p in pages]
    segments: list[list[dict]] = []
    for page in results:
        if page["starts_document"] or not segments:
            segments.append([page])
        else:
            segments[-1].append(page)
    return segments

def scan_inbound_queue(packets: list[list[dict]]) -> dict:
    # 1 ページ約 100〜500ms、入力のみ課金 $0.042/M:1 万パケットの
    # 既存アーカイブスキャンは 1 ドル未満、GPU 不要。
    lanes: dict[str, int] = {}
    for packet in packets:
        for segment in classify_packet(packet):
            lane = segment[0]["lane"]
            lanes[lane] = lanes.get(lane, 0) + 1
    return lanes

ドキュメント分類のよくある質問

ドキュメント分類とテキスト分類の違いは何ですか?

テキスト分類は 1 つの自己完結したテキスト——1 件のチケット、1 件のレビュー、1 通のメッセージ——にラベルを付けます。ドキュメント分類が相手にするのはパケットです。添付 3 件のメール、請求書 8 ページに契約書 1 ページが混ざったスキャン束、無関係な文書をつなげた PDF エクスポート。2 段階パイプラインは、テキスト分類には不要だったステップ——ページ単位の Noul で「ある文書がどこで終わり、次がどこから始まるか」を決める——を追加し、その後に各セグメントを型付き Choice で分類します。単一テキストの一般形はテキスト分類 API ガイドで扱っています。

Jev はスキャンした PDF や画像、手書きを読めますか?

読めません——そしてこれを正直に言うことが、デモとパイプラインの分かれ目です。Jev の State はテキストです。モデル内に OCR も、レイアウト解析も、画像認識もありません。スキャン文書は上流で OCR と抽出を実行し、その結果のテキストを渡します。Jev が追加するのは needs_ocr Noul です。テキストが欠落していたり判読不能だったりするページにフラグを立て、自信のある誤ラベルを与える代わりに修復キューへ送ります。乱れた OCR は、型付きであれ生成式であれ、すべての下流分類器の精度の上限を切り下げます。

パケット分割は実際にはどう動きますか?

各ページが is_single_document Noul を受け取ります。「このページは新しい文書の始まりか、前の文書の続きか」。答えが true のページ(最初のページは常に true)が新しいセグメントを開き、他のページは開いているセグメントに蓄積します。ブール値よりも較正済み信頼度の方が重要です。信頼度 0.55 の境界は「争われた境界」であり、争われた境界は——他のすべての低信頼度判定と同じく——契約書を請求書から黙って切り離すのではなく、レビューキューに落とすべきです。

信頼度のしきい値はいくつにすべきですか?

信頼度ゲート付きフォールバックチェーンと同じ 3 レーンから始めます。0.85 以上で自動ファイリング、0.60〜0.85 はレビューキュー、0.60 未満——すべての「other」ラベルと needs_ocr ページを含む——は人手へ。そのうえで、ラベル付きサンプルから自社のカットオフを導出します。実物のパケットで予測信頼度とオペレーションの判定をプロットし、「誤ファイリングしても回収しやすい」位置にカットオフを置いてください。強制の前にシャドーモードで通し、四半期ごとに再検証を。新しいベンダーは新しいレイアウトを意味し、それは分布シフトを意味します。

DocJev とは何ですか?公式ですか?

DocJev は LlamaIndex の創業者 Jerry Liu によるオープンソースライブラリで、複雑な文書パケットの分類と分割を行います——このページが実装しているのと同じ 2 段階パターンです。madewithjev のショーケースに Documents & OCR カテゴリで紹介されており、作者は 1 パケット約 139ms、自身のベンチマークで 40/40 を報告しています(作者の自己報告であり、独立した監査ではありません)。DocJev はサードパーティのエコシステムプロジェクトであって TypeSafe AI の公式製品ではありませんし、madewithjev も同じです。

大量バッチの分類にはいくらかかりますか?

課金は入力トークンのみ($0.042/M)で、出力は無料です。9 ページのパケットは 1 ページ約 300 トークンとして約 2,700 入力トークン ≈ $0.0001。1 万パケットの既存アーカイブスキャンでも 1 ドル未満で、GPU は不要です。エコシステムの公開データポイントでは、1kpapers が 1,018 本の AI 研究論文を合計 $0.08、中央値約 256ms で分類しました。生成式パイプラインは同程度の入力費に加えて、判定ごとの出力トークン——そしてリトライごとに再度——を支払います。

ドキュメントパイプラインを広げる