Jev Recipe / 文档处理
用 Jev 做文档分类:先拆分包,再判类型的两阶段管道
一封邮件带 3 份附件、一次扫描混入 8 页发票和 1 页合同:先用 Noul 判定页级边界拆开文档包,再用类型化 Choice 逐份分类——校准置信度自动分流复核队列。
搭建文档包两阶段管道的 6 个步骤
{
"is_single_document": {
"type": "noul",
"instructions": "这一页是新文档的开始,还是上一页文档的延续?"
},
"needs_ocr": {
"type": "noul",
"instructions": "这一页的文本是否缺失或乱到无法可靠判定,需要先跑 OCR?"
},
"doc_type": {
"type": "choice",
"instructions": "把这份文档归入恰好一个类型",
"criteria": {
"invoice": "逐项计费:明细行、数量、总额、税、付款条款",
"contract": "有约束力的协议:签约方、条款、期限日期、签名栏",
"receipt": "支付完成凭证:已付标记、交易参考号、无待付金额",
"letter": "人工往来沟通:称呼、正文、签名块",
"other": "以上都不匹配且无把握——升级复核,不要硬塞标签"
}
}
}文档类型轴:五个标签和一个设计好的兜底
分类质量从标签集开始。下面五个类型覆盖了绝大多数后台文档量;第六张卡最重要——一个设计好的「其他」,宁可升级也不要硬塞标签。tells 直接当 Choice schema 的 criteria 词汇用,轴保持精简:每多一个标签,概率质量就被多稀释一分。
发票
应收的钱——文档包里最常见的载荷。
- 带数量和单价的明细行
- 页眉或页脚里的发票号
- 税额与应付总额
- 付款条款:账期 30 天、到期日、收款账户
- 开票方与收票方信息块
合同
有约束力的协议——风险高,值得单设复核车道。
- 导言里有具名的签约方
- 编号条款与定义章节
- 期限、生效日、续约日
- 签名栏含打印姓名和日期
- 法律样版页眉与页码
收据
支付凭证——最容易和发票混淆的类型。
- 「已支付」或交易完成标记
- 终端或 POS 台头与门店信息
- 明细行很短,没有付款条款
- 卡号后四位或支付参考号
- 无待付金额——已付清,余额为零
信函与往来邮件
人与人之间的沟通——封面邮件、备忘录、通知函。
- 称呼与落款包着自由正文
- 正文里有一个请求或通知事项
- 签名块含职务和联系方式
- 邮件正文里的引用回复链
- 通篇没有任何账单字段
表单与登记
结构化采集——申请、理赔、入职登记。
- 带标签的字段、值在固定位置
- 复选框、下拉框和选项标记
- 表单设计自带的编号章节标题
- 手写区或签名框
- 「第 N 页 / 共 M 页」页脚
其他——设计好的兜底
它是一个标签,不是失败:分流处理,不要硬判。
- 不匹配上面任何标准的文档
- 抽取输出混杂或为空
- 所有标签的置信度都低于阈值
- 新供应商带来的未知版式
- 当作工作队列处理,而不是报错
文档包两阶段拆分与分类模拟器
选一个文档包并运行两阶段管道:逐页 Noul 判边界 → 拆分段 → Choice 判类型与置信度分流(纯前端模拟,不调用真实 API):
发件人 ops@acme.co——「你好,本月账务文件见附件:3 月发票、刷卡收据、已签署的 MSA 续约。」
INVOICE #2026-0341 · ACME Supplies Ltd. · 14 条明细 · 小计 $11,618.32 · 增值税 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 两阶段管道 vs 一条超长 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: "把这份文档归入恰好一个类型",
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% schema 错误。
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": "把这份文档归入恰好一个类型",
"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:
# 每页 ~100-500ms,仅输入计费 $0.042/M:一万包存量扫描
# 不到一美元,完全不需要 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文档分类常见问题
文档分类和文本分类有什么区别?
文本分类给一段自足的文本打标签——一张工单、一条评论、一条消息。文档分类面对的是文档包:带 3 份附件的邮件、混入 8 页发票和 1 页合同的扫描批次、把不相关文档拼在一起的 PDF 导出。两阶段管道补上了文本分类从来不需要的一步——用页级 Noul 判定一份文档在哪里结束、下一份从哪里开始——然后才用类型化 Choice 给每个分段分类。单文本的通用场景见文本分类 API 指南。
Jev 能读扫描件、图片或手写体吗?
不能——把这一点说清楚,正是演示和生产管道的区别。Jev 的 state 是文本:模型里没有 OCR、没有版面分析、也没有视觉能力。扫描文档包在前游跑 OCR 和抽取,把产出的文本传进来。Jev 补的是 needs_ocr Noul:把文本缺失或乱到无法判定的页标出来,送进修复队列,而不是给出一个自信的错误标签。OCR 质量差会封顶所有下游分类器的准确率——类型化的和生成式的都一样。
分包到底是怎么拆的?
每一页都会得到一个 is_single_document Noul:「这一页是新文档的开始,还是上一页的延续?」答案为 true 的页(第一页永远为 true)开启一个新分段,其余页面归入当前分段。校准置信度比布尔值更重要:0.55 置信度的边界就是有争议的边界,而有争议的边界——和其他所有低置信度判定一样——应该落进复核队列,而不是悄悄把合同和它的发票拆散。
置信度阈值应该设多少?
从置信度门控降级链的三车道起步:≥ 0.85 自动归档,0.60–0.85 进复核队列,低于 0.60——加上所有「other」标签和所有 needs_ocr 页——升级人工。然后用标注样本推导自己的切分点:把预测置信度和运营判定在真实文档包上画成曲线,把每个切分点放在「归错档也容易捞回来」的位置。强制生效前先影子模式,之后每季度重验——新供应商意味着新版式,意味着分布漂移。
DocJev 是什么?是官方的吗?
DocJev 是 LlamaIndex 创始人 Jerry Liu 开源的一个库,用于分类和拆分复杂文档包——和本页实装的是同一个两阶段模式。它入选了 madewithjev 展示区的 Documents & OCR 栏目,作者自报单包约 139ms、自建基准 40/40 正确(作者自报数据,非独立审计)。DocJev 是第三方生态项目,不是 TypeSafe AI 官方产品——madewithjev 也不是。
大批量分类要花多少钱?
只按输入 token 计费($0.042/M),输出免费。一个 9 页文档包按每页约 300 token 算约 2,700 输入 token ≈ $0.0001,一万包的存量扫描不到一美元,完全不需要 GPU。生态里的公开数据点:1kpapers 把 1,018 篇 AI 研究论文全部分类只花了 $0.08,中位延迟约 256ms。生成式管道要付相当的输入账单,外加每个判定的输出 token——每次重试还要再付一遍。