Jev Recipe / ドキュメント処理
Jev でドキュメント分類:分割してから判定する 2 段階パイプライン
メールに添付 3 件、スキャンに請求書 8 ページと契約書 1 ページが混在——まず Noul のページ単位境界判定でパケットを分割し、それから型付き Choice で 1 通ずつ分類します。較正済み信頼度がレビュー待ちへ自動振り分け。
文書パケット 2 段階パイプライン構築 6 ステップ
{
"is_single_document": {
"type": "noul",
"instructions": "このページは新しい文書の始まりですか、前のページの文書の続きですか?"
},
"needs_ocr": {
"type": "noul",
"instructions": "このページのテキストは欠落または判読不能で、信頼できる判定の前に OCR が必要ですか?"
},
"doc_type": {
"type": "choice",
"instructions": "この文書をちょうど 1 つのタイプに分類する",
"criteria": {
"invoice": "明細請求:行項目、数量、合計、税、支払条件",
"contract": "拘束力のある合意:当事者、条項、期間、署名欄",
"receipt": "支払完了の証憑:支払済みマーク、取引参照番号、未払いなし",
"letter": "人同士の連絡文書:頭語、本文、署名ブロック",
"other": "上記のいずれも確信を持って当てはまらない——無理にラベルを付けずエスカレーション"
}
}
}文書タイプ軸:5 つのラベルと、設計されたフォールバック
分類の品質はラベルセットから始まります。以下の 5 タイプでバックオフィス文書の大部分をカバーできます。6 枚目のカードが最も重要です。無理にラベルを付けるのではなくエスカレーションする、設計された「その他」です。tell はそのまま Choice スキーマの criteria 語彙に使えます。軸は小さく保ってください。ラベルを 1 つ増やすたびに、確率質量は 1 分ずつ薄まります。
請求書
未払いの金銭——パケットで最もよく見るペイロード。
- 数量と単価付きの行項目
- ヘッダーやフッターの請求書番号
- 税額と支払合計
- 支払条件:締め 30 日、支払期日、振込先
- 発行者と請求先のブロック
契約書
拘束力のある合意——リスクが高いため、レビューレーンの価値がある。
- 前文に名前のある当事者
- 番号付き条項と定義セクション
- 期間、発効日、更新日
- 氏名と日付入り署名欄
- 定型の法務ヘッダーとページ番号
領収書
支払の証拠——請求書と最も混同されるタイプ。
- 「支払済み」や取引完了のマーク
- 端末や POS のヘッダーと店舗情報
- 短い行項目で支払条件はない
- カード下 4 桁または支払参照番号
- 未払いゼロ——支払済み、残高なし
文書・メール対応
人同士の連絡——表紙メール、メモ、通知文。
- 頭語と結語に挟まれた自由な本文
- 本文中の依頼または通知事項
- 役職と連絡先入り署名ブロック
- メール本文の引用返信チェーン
- 請求フィールドはどこにもない
フォーム・申込書
構造化された収集——申請、請求、オンボーディング。
- ラベル付きフィールドに固定位置の値
- チェックボックス、ドロップダウン、選択マーク
- フォーム設計由来の番号付きセクション見出し
- 手書き欄や署名ボックス
- 「N ページ / M ページ中」のフッター
その他——設計されたフォールバック
失敗ではなくラベル:強制せず、振り分ける。
- 上記のどの基準にも当てはまらない文書
- 混在または空の抽出出力
- すべてのラベルで信頼度がしきい値未満
- 新しいベンダーの未知のレイアウト
- エラーではなくワークキューとして扱う
文書パケット 2 段階分割・分類シミュレーター
パケットを選んで 2 段階パイプラインを実行。ページ単位 Noul で境界を判定 → セグメント分割 → Choice でタイプと信頼度レーンを判定(フロントエンドのシミュレーションで、実 API は呼びません):
差出人 ops@acme.co——「今月の請求書、カード領収書、署名済み MSA 更新文を添付します。」
INVOICE #2026-0341 · ACME Supplies Ltd. · 14 明細 · 小計 $11,618.32 · VAT 7.4% · 支払合計 $12,480.00 · 締め 30 日
領収書 — 支払済み · Visa ****4218 · $12,480.00 · 参照 TXN-88213 · 2026-03-02 · 未払いなし
マスターサービス契約 · Acme Co. × Beta LLC · 期間 2026-04-01〜2028-03-31 · 署名:締結済み
「分割 + 分類を実行」を押すと、境界判定・セグメント分割・各文書のタイプ信頼度を表示します。
文書パケット分類比較:Jev 2 段階パイプライン vs 1 本の長い LLM プロンプト
実装コード:TypeScript / Python のパケットパイプライン
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");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 で分類しました。生成式パイプラインは同程度の入力費に加えて、判定ごとの出力トークン——そしてリトライごとに再度——を支払います。