Credits 积分计费
AutoGPT Platform 与经典最大的区别——它是计量收费 SaaS。今天深入计费:两阶段扣费、原子 SQL 保余额正确、Stripe 充值闭环。这是平台的"钱袋子"。
👶 小白:为什么要"先预扣、后对账"这么麻烦?执行完按实际用量一次性扣不行吗?
👨🏫 老师:不行,会出两种事故。① 透支:不先扣,用户余额只剩 1 元却跑了 100 元的活,钱花在上游了追不回。② 并发算错:同一用户十个任务同时结账,不用原子 SQL 会把余额算乱(各扣各的、少扣了)。所以先预扣挡住付不起的,实际用量出来再对账多退少补,原子 SQL 保证并发下余额永远准。这就是"计量收费 SaaS"的严谨之处。
计费全景
用户余额存 UserBalance 表,每笔变动记一条 CreditTransaction(充值 TOP_UP / 赠送 GRANT / 消费 USAGE)。Day 09 学了"每个 Block 值多少 credit",今天学"执行时怎么扣、怎么充"。
预扣 charge_usage
节点运行前,charge_usage()(executor/billing.py:118)按 pre-flight 估算扣费:
remaining_balance = db_client.spend_credits(
user_id=..., cost=cost,
metadata=UsageTransactionMetadata(graph_exec_id=..., block=block.name, ...))
# 动态计费 block(pre-flight=0)会检查余额必须为正,否则抛 InsufficientBalanceError
对账 charge_reconciled_usage
Block 跑完拿到真实用量,charge_reconciled_usage()(billing.py:200)算 delta = 真实 - 预扣:
delta = post_flight - pre_flight
if delta == 0: return
# delta > 0 → 补扣(fail_insufficient_credits=False,允许扣成负余额!)
# delta < 0 → 退款(比如 token 实际用得少)
spend_credits(..., cost=delta, fail_insufficient_credits=False)
10 credit。跑完拿到真实用量:· 情况 A(用多了):真实
13 → delta = 13−10 = +3 → 再补扣 3(允许扣成负余额)。· 情况 B(用少了):真实
7 → delta = 7−10 = −3 → 退回 3。· 情况 C(估准了):真实
10 → delta = 0 → return,不动账。最终用户被扣的永远是真实用量。fail_insufficient_credits=False:允许扣成负余额!因为服务已经消耗了(平台已付钱给供应商),宁可记用户欠款,也不能漏账。原子 SQL 扣费(最关键)
FOR UPDATE 行锁保证"同一时刻只有一个扣费能碰这一行余额",其他扣费排队等待。读、算、写、插流水在这条 SQL 内一气呵成——要么全成功、要么全回滚。这就是"原子性",也是所有正确的金融扣费的地基。所有余额变动走 _add_transaction()(data/credit.py:501),用一整条 PostgreSQL CTE + FOR UPDATE 行锁保证原子性(:583):
# 一条 SQL 里原子地:
# (a) 锁住并读取余额(FOR UPDATE)
# (b) 更新 UserBalance(带上溢/下溢保护)
# (c) 插入 CreditTransaction 流水
# 余额不足时 WHERE ... balance + $2 >= 0 让更新落空 → 抛 InsufficientBalanceError
Stripe 充值闭环
充值三步(data/credit.py):
top_up_intent()(:1123):调stripe.checkout.Session.create()生成支付页,插一条is_active=False的 TOP_UP 流水(未生效)。- 用户付款完成 → Stripe 回调 →
fulfill_checkout()(:1199):若payment_status="paid"就_enable_transaction()激活那条流水、真正加到余额。 - Webhook 入口
stripe_webhook()(api/features/v1.py:1381):construct_event()验签 + 事件去重(防 Stripe 重试重复入账)。
自动充值
spend_credits()(credit.py:739)扣完后检查是否触发自动充值(:757):余额低于 auto_top_up.threshold 就自动走 Stripe 补钱。还有余额预警 handle_low_balance()(billing.py:455,跌破阈值发邮件+Discord)。
平台成本追踪(内部盈亏)
executor/cost_tracking.py 是给平台自己算成本用的(不是给用户扣费)。当 Block 用平台系统凭据(Day 17)时,log_system_credential_cost()(:200)记录这次调用真实消耗了平台多少钱。
今日小结 + 动手
🧠 今天你应该能回答
- 计费账本由什么组成?(余额 + 流水)
- 预扣 + 对账为什么?补扣为什么允许负余额?
- 扣费为什么必须原子(CTE+行锁)?
- Stripe 充值为什么"先记未生效、付款后激活"?验签/去重防什么?
- 自动充值解决什么?平台成本追踪和用户扣费什么关系?
✋ 动手
P=autogpt_platform/backend/backend
grep -n 'def charge_usage\|def charge_reconciled_usage' $P/executor/billing.py
sed -n '501,660p' $P/data/credit.py | head -50 # _add_transaction 原子 SQL
grep -n 'def top_up_intent\|def fulfill_checkout\|def spend_credits' $P/data/credit.py