Jev Recipe / 财务运营
用 Jev 做 AI 费用分类:银行流水批量入账与置信度门控
把刷卡记录和银行流水的每一行原始记录变成类型化的财务决策:category Choice、可抵扣 Score、needs_review Noul——整份账单一次 evaluate 调用,0.85 置信度门控把自动入账和人工复核分开。
实施 AI 费用分类的 6 个步骤
{
"category": {
"type": "choice",
"instructions": "对每一行支出,将其归入恰好一个类目",
"criteria": {
"software_saas": "SaaS 订阅、云服务、数字工具与授权",
"travel": "机票、酒店、地面交通、差旅补贴",
"meals": "餐厅、咖啡、客户宴请与团队聚餐",
"office_supplies": "办公用品、设备、家具、仓储采购",
"professional_services": "法务、会计、咨询、外包、代理商",
"other": "以上都不匹配且无把握——升级复核,不要硬塞标签"
}
},
"deductibility": {
"type": "score",
"instructions": "对每一行评估税务可抵扣程度,1(个人支出,不可抵扣)到 4(全额可抵扣的经营支出)"
},
"needs_review": {
"type": "noul",
"instructions": "对每一行判断:是否模糊到无法可靠分类,需要记账人员先确认再入账?"
}
}银行流水批量分类与置信度门控模拟器
选一份账单并运行管道:category Choice 判类目、可抵扣 Score 判税务处理、needs_review Noul 标记模糊行,0.85 门控分「自动入账 / 复核队列 / 人工记账」三道车道(纯前端模拟,不调用真实 API):
AWS EMEA SARL
$184.20UBER *TRIP 88112
$23.75OFFICE DEPOT #221
$96.40SQ *BLUE BOTTLE COFFEE
$18.50GRANITE LEGAL PLLC
$2,400.00MERCH PAYMENT 8842 LLC
$312.88ZOOM.US
$15.99点击「运行批量分类」,查看每行的类目、可抵扣评分与置信度分流。
费用分类对比:Jev 类型化契约 vs 关键词规则与生成式 LLM
生产代码:TypeScript 与 Python 双端费用分类管道
import json
import os
import time
import requests
# 基线:先跑关键词规则,规则匹配不了的交给生成式 LLM。
# 两套系统,两种失败模式。
KEYWORD_RULES = {
"software_saas": ["AWS", "NOTION", "ZOOM.US", "ADOBE"],
"travel": ["DELTA AIR", "UBER", "LYFT", "MARRIOTT"],
"meals": ["RESTAURANT", "COFFEE", "SUSHI", "CAFE"],
"office_supplies": ["OFFICE DEPOT", "STAPLES", "AMZN MKTP"],
}
CATEGORIES = set(KEYWORD_RULES) | {"professional_services", "other"}
def rule_category(line: str) -> str | None:
upper = line.upper()
for category, keywords in KEYWORD_RULES.items():
if any(keyword in upper for keyword in keywords):
return category
return None # "SQ *BLUE BOTTLE"、"MERCH PAYMENT 8842 LLC" 匹配不到任何规则
def generative_category(line: str) -> dict:
# 每行 1.5-3 秒,每次尝试都付输出 token,而且 json.loads 对幻觉类目
# 和真实类目一视同仁地放行。
for attempt in range(3):
resp = requests.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"},
json={
"model": "gpt-4o-mini",
"response_format": {"type": "json_object"},
"messages": [{
"role": "user",
"content": f'把这行支出归类为 {", ".join(sorted(CATEGORIES))} 之一,'
f'以 JSON 返回:{{"category": "..."}}\n流水行:{line}',
}],
},
timeout=30,
)
try:
category = json.loads(
resp.json()["choices"][0]["message"]["content"]
)["category"]
if category in CATEGORIES: # json_mode 不会帮你做的值校验
return {"category": category, "confidence": None}
except (KeyError, json.JSONDecodeError):
time.sleep(2 ** attempt) # 每次重试都为完整生成重新付费
return {"category": "other", "confidence": None} # 静默失败
def categorize_line(line: str) -> dict:
category = rule_category(line)
if category is not None:
return {"category": category, "confidence": None} # 规则没有置信度
return generative_category(line)import requests
JEV_ENDPOINT = "https://api.typesafe.ai/v1/jev/evaluate"
AUTO_POST_CONFIDENCE = 0.85 # ≥ 0.85 → 自动入账
REVIEW_MIN_CONFIDENCE = 0.60 # 0.60~0.85 → 复核队列,低于 → 人工
QUESTIONS = {
"category": {
"type": "choice",
"instructions": "对每一行支出,将其归入恰好一个类目",
"criteria": {
"software_saas": "SaaS 订阅、云服务、数字工具",
"travel": "机票、酒店、地面交通",
"meals": "餐厅、咖啡、客户宴请",
"office_supplies": "办公用品、设备、仓储采购",
"professional_services": "法务、会计、咨询、外包",
"other": "以上都不匹配且无把握",
},
},
"deductibility": {
"type": "score",
"instructions": "对每一行评估税务可抵扣程度,1(个人支出)到 4(全额可抵扣)",
},
"needs_review": {
"type": "noul",
"instructions": "对每一行判断:是否模糊到需要记账人员确认后才能入账?",
},
}
def categorize_statement(lines: list[dict]) -> list[dict]:
# 整份账单作为一个 state——三个类型化问题在单次约 70-100ms 的
# 前向推理中逐行返回答案。
resp = requests.post(
JEV_ENDPOINT,
json={"state": {"lines": lines}, "questions": QUESTIONS},
timeout=5,
)
resp.raise_for_status()
data = resp.json()
results = []
for i, line in enumerate(lines):
category = data["category"][i]
flagged = data["needs_review"][i]["answer"]
if flagged or category["answer"] == "other":
lane = "human_review"
elif category["confidence"] >= AUTO_POST_CONFIDENCE:
lane = "auto_post"
elif category["confidence"] >= REVIEW_MIN_CONFIDENCE:
lane = "review"
else:
lane = "human_review"
results.append({
"line_index": line["line_index"],
"category": category["answer"],
"confidence": category["confidence"],
"deductibility": data["deductibility"][i]["answer"],
"lane": lane,
})
return results
def sweep_statements(statements: list[list[dict]]) -> dict:
# 仅输入计费 $0.042/M:30 行账单约 1,500 token ≈ $0.00007,
# 每月一万份账单不到一美元。
lanes: dict[str, int] = {"auto_post": 0, "review": 0, "human_review": 0}
for lines in statements:
for row in categorize_statement(lines):
lanes[row["lane"]] += 1
return lanesimport { z } from "zod";
import OpenAI from "openai";
// 基线:先跑关键词规则,剩下的走结构化输出 LLM。
const KEYWORD_RULES: Record<string, string[]> = {
software_saas: ["AWS", "NOTION", "ZOOM.US", "ADOBE"],
travel: ["DELTA AIR", "UBER", "LYFT", "MARRIOTT"],
meals: ["RESTAURANT", "COFFEE", "SUSHI", "CAFE"],
office_supplies: ["OFFICE DEPOT", "STAPLES", "AMZN MKTP"],
};
const CATEGORIES = [
"software_saas",
"travel",
"meals",
"office_supplies",
"professional_services",
"other",
] as const;
const Decision = z.object({ category: z.enum(CATEGORIES) });
const client = new OpenAI();
function ruleCategory(line: string): string | null {
const upper = line.toUpperCase();
for (const [category, keywords] of Object.entries(KEYWORD_RULES)) {
if (keywords.some((k) => upper.includes(k))) return category;
}
return null; // "MERCH PAYMENT 8842 LLC" 匹配不到任何规则
}
export async function categorizeLine(line: string) {
const category = ruleCategory(line);
if (category) return { category, confidence: null }; // 规则没有置信度
for (let attempt = 0; attempt < 3; attempt++) {
try {
const resp = await client.chat.completions.create({
model: "gpt-4o-mini",
response_format: { type: "json_object" },
messages: [
{
role: "user",
content: `把这行支出归类为 ${CATEGORIES.join(", ")} 之一,以 JSON 返回:{"category": "..."}\n流水行:${line}`,
},
],
});
// 形状有保证,值是否正确没有保证——
// 而且没有任何可以驱动自动化的置信度数字。
return {
category: Decision.parse(
JSON.parse(resp.choices[0].message.content!),
).category,
confidence: null,
};
} catch {
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
}
}
return { category: "other" as const, confidence: null };
}const JEV_ENDPOINT = "https://api.typesafe.ai/v1/jev/evaluate";
// 策略写在应用代码里——模型只负责报告事实。
const AUTO_POST_CONFIDENCE = 0.85; // ≥ 0.85 → 自动入账
const REVIEW_MIN_CONFIDENCE = 0.6; // 0.60~0.85 → 复核队列
type StatementLine = {
line_index: number;
merchant: string;
amount: number;
date: string;
memo?: string;
};
const QUESTIONS = {
category: {
type: "choice",
instructions: "对每一行支出,将其归入恰好一个类目",
criteria: {
software_saas: "SaaS 订阅、云服务、数字工具",
travel: "机票、酒店、地面交通",
meals: "餐厅、咖啡、客户宴请",
office_supplies: "办公用品、设备、仓储采购",
professional_services: "法务、会计、咨询、外包",
other: "以上都不匹配且无把握",
},
},
deductibility: {
type: "score",
instructions: "对每一行评估税务可抵扣程度,1(个人支出)到 4(全额可抵扣)",
},
needs_review: {
type: "noul",
instructions: "对每一行判断:是否模糊到需要记账人员确认后才能入账?",
},
} as const;
export type ExpenseLane = "auto_post" | "review" | "human_review";
export async function categorizeStatement(lines: StatementLine[]) {
const response = await fetch(JEV_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state: { lines }, questions: QUESTIONS }),
});
if (!response.ok) throw new Error("Jev evaluate failed");
const data = await response.json();
// 答案逐行返回,与 state.lines 的顺序对齐。
return lines.map((line, i) => {
const category = data.category[i] as {
answer: string;
confidence: number;
};
const flagged = data.needs_review[i].answer as boolean;
const lane: ExpenseLane =
flagged || category.answer === "other"
? "human_review"
: category.confidence >= AUTO_POST_CONFIDENCE
? "auto_post"
: category.confidence >= REVIEW_MIN_CONFIDENCE
? "review"
: "human_review";
return {
...line,
category: category.answer,
confidence: category.confidence,
deductibility: data.deductibility[i].answer as number,
lane,
};
});
}
// 金额、税额和按类目汇总留在你的代码里——模型从不做四则运算。
// 30 行账单 ≈ 1,500 输入 token,墙钟约 70-100ms,输出 token $0。费用分类常见问题
什么是 AI 费用分类?
费用分类把每一笔支出行——刷卡记录、银行流水条目、收据——归入记账体系里的一个类目:软件、差旅、餐饮、办公用品、专业服务。会计手工做这件事已经几百年了;自动化要回答的问题是:软件能不能读懂自由文本的商家描述,做出同样的判断。本页把每一行当作一个类型化的财务决策来处理:一个覆盖你类目体系的 Choice、一个可抵扣 Score 和一个 needs_review 标记,由校准置信度决定哪些自动入账、哪些由人工确认。
银行流水怎么自动分类?
导出账单,把每一行解析成对象(商家描述、金额、日期、备注),然后把行作为 state 发给一次 Jev evaluate 调用——上面这个 Recipe 把 30 笔演示交易装进了一个请求。每行返回三个类型化答案:category Choice、可抵扣 Score 和 needs_review Noul。你的代码套用 0.85 门控:高置信度的行入账,其余进复核队列。解析导出文件是你这一侧的边界——Jev 读的是流水行的文本,不是 PDF 版面;那个前置的「先拆再分」问题正是文档分类 Recipe 的主题。
AI 费用分类到底准不准?
诚实的答案是:逐行判定,配合门控。Jev 的置信度经 RLCD 校准,0.85 的阈值表现为一份 85% 准确率契约:过这道门的 100 笔自动入账里约有 85 笔分类正确——而管道的设计保证剩下 15 笔不会悄悄入账。0.60–0.85 区间的行、所有「other」和 needs_review 行都交给会计,会计的改判回流成标注样本。准确率在真正要紧的地方提升:不是因为模型重新训练了,而是因为复核队列让错账永远到不了账本。
这和通用 LLM 分类有什么区别?
三个契约层面的区别。第一,有界输出:生成式分类器可以用一段话回答「应该是餐饮吧,这家看着像拉面连锁」;Jev 只返回你类目体系里的一个值加一个校准概率,没有别的内容。第二,置信度数字有含义:RLCD 校准让 0.85 成为准确率契约,而 token logprob 度量的是「打字时有多确定」,不是决策准确率。第三,批处理:一次 evaluate 调用在约 70–100ms 内回答整份账单的全部三个问题,只按输入计费——没有逐判定的输出 token,也没有重试。单文本的 REST 与 CLI 通用形态,见文本分类 API 指南。
类目应该怎么定?
从会计已经在用的记账科目出发,而不是从模型想象的类目出发。轴保持精简——五到八个类目加一个设计好的「other」——因为每多一个标签,概率质量就被多稀释一分,而且每个类目都应恰好对应一个记账科目。criteria 用复核人员的词汇来写(「客户宴请」「云服务」),像其他政策一样在代码里做版本管理;某个商家反复落进「other」,就是类目缺失或 criteria 太窄的信号。
遇到不认识的商家怎么办?
「不认识」是被设计好的结果,不是错误。像「MERCH PAYMENT 8842 LLC」这样无法解析的描述会拿到 other 标签或 needs_review 标记,落到 0.85 门控之下,进入人工确认队列——这条 human-in-the-loop 车道保证一条奇怪的流水不会入错账。复核人员确认一次,修正进入标注样本集;如果同样的未知形态反复出现,那就是该加类目或收紧 criteria 的时机。