Jev Recipe / 财务运营

用 Jev 做 AI 费用分类:银行流水批量入账与置信度门控

把刷卡记录和银行流水的每一行原始记录变成类型化的财务决策:category Choice、可抵扣 Score、needs_review Noul——整份账单一次 evaluate 调用,0.85 置信度门控把自动入账和人工复核分开。

实施 AI 费用分类的 6 个步骤

01动手写代码前先定好分类体系。直接从会计记账已经在用的科目出发——software_saas、travel、meals、office_supplies、professional_services,外加一个设计好的 other:宁可升级复核也不硬塞标签。轴保持精简:每多一个类目,概率质量就被多稀释一分;每个类目都应恰好对应一个记账科目。
02把契约设计成一次 evaluate 调用里的三个问题:带逐项 criteria 的 category Choice、判定税务处理的可抵扣 Score(1 = 个人支出,4 = 全额可抵扣),以及询问这行是否模糊到无法入账的 needs_review Noul。「多问题一次调用」的形状,和线索评分 Recipe 里销售优先级的类型化契约是同一套——模型报事实,策略留在你的代码里。
03门控策略写在应用代码里:category 置信度 ≥ 0.85 且 needs_review = false 自动入账,0.60–0.85 进复核队列,低于 0.60——加上所有「other」标签——交给会计人工确认。切分点从标注账单里推导,不要拍脑袋;0.85 之所以是一份准确率契约,是因为 Jev 的置信度经 RLCD 校准。
04批量之前先算账:state 只按输入 token 计费($0.042/M),输出免费。一份 30 行账单按每行约 50 token 算约 1,500 输入 token ≈ $0.00007,每月分类一万份账单不到一美元——完整账目见 Jev 定价页。整份账单装进一次 evaluate 调用。
05用对账闭环把修正变成资产:复核人员改判一笔分类时,把修正结果写回标注样本集,并用当前 schema 在影子模式下重跑。修正是发现体系缺口的方式——某个商家反复落进「other」,说明缺一个类目或 criteria 太窄,而不是要删掉的噪音。
06监控分布,而不只看准确率:跟踪自动入账占比、「other」率和 0.60–0.85 争议带的流量。新的银行导出格式、卡片换发、新的支出类型都是分布漂移——和置信度门控降级链要重校准的是同一件事。每季度重验阈值。
schema / 费用分类决策契约
{
  "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": "对每一行判断:是否模糊到无法可靠分类,需要记账人员先确认再入账?"
  }
}
把一条真实流水行作为 state 发送,检查 category、可抵扣 Score 与 needs_review 的校准置信度。
实时交互体验 / 银行流水分类沙盒

银行流水批量分类与置信度门控模拟器

选一份账单并运行管道:category Choice 判类目、可抵扣 Score 判税务处理、needs_review Noul 标记模糊行,0.85 门控分「自动入账 / 复核队列 / 人工记账」三道车道(纯前端模拟,不调用真实 API):

category + deductibility + needs_review
7 行
0103-01

AWS EMEA SARL

$184.20
0203-02

UBER *TRIP 88112

$23.75
0303-03

OFFICE DEPOT #221

$96.40
0403-04

SQ *BLUE BOTTLE COFFEE

$18.50
0503-05

GRANITE LEGAL PLLC

$2,400.00
0603-06

MERCH PAYMENT 8842 LLC

$312.88
0703-07

ZOOM.US

$15.99
source: 公司卡 / 3 月
Jev 决策输出
~86ms / ≈$0.0000147

点击「运行批量分类」,查看每行的类目、可抵扣评分与置信度分流。

代码中的策略:置信度 ≥ 0.85 且 needs_review = false → 自动入账 · 0.60–0.85 → 复核队列 · < 0.60 或「other」或 needs_review = true → 人工记账 · 金额与税额计算留在你的代码里 · 演示数据(模拟交易)

费用分类对比:Jev 类型化契约 vs 关键词规则与生成式 LLM

METRIC
Jev
关键词规则 + 生成式 LLM
类目体系变更
修改代码里版本化的 criteria——下一次调用就用上新体系;无需重训,也无需改正则
关键词规则随商家描述变化逐渐失效;生成式 LLM 把类目体系写在 prompt 里,每次运行都在漂移
置信度语义
RLCD 校准概率——0.85 约等于 85% 准确率契约,阈值可直接驱动自动入账
规则:没有置信度——关键词要么匹配要么不匹配。生成式:模型自报、未校准,需额外接入 logprob
未知商家
设计好的「other」加 needs_review Noul——处理不了的行进复核队列,而不是硬猜一个标签
规则:无法匹配的字符串悄悄落进默认桶。生成式:编一个看起来合理的类目,且没有任何「它在猜」的信号
单份账单延迟
整批约 70–100ms——30 行流水装进一次 evaluate 调用,单次前向推理完成
规则:瞬时但读不懂自由文本描述。生成式:逐 token 生成,每行 1.5–3 秒——30 行账单要 45–90 秒
批量成本
仅输入计费 $0.042/M、输出免费——30 行账单 ≈ $0.00007;每月一万份账单不到一美元
规则:运行免费、纠错昂贵——每次错账都烧人工分钟。生成式:每行都付输出 token,重试再付一遍
判定一致性
确定性——同一行每次都落在同一类目、同一置信度,账目跨运行可对齐
规则:确定但不透明——错账只能靠翻正则列表解释。生成式:采样波动让边界行在多次运行之间换类

生产代码:TypeScript 与 Python 双端费用分类管道

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)
python / jev 账单批量分类
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 lanes
typescript / 基线:规则 + 结构化输出重试
import { 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 };
}
typescript / jev 类型化账单管道
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 的时机。

继续扩展财务管道