
做 AI 产品技术跑通只是万里长征第一步。真正让人头疼的是Demo 演示时全场惊艳一上线就开始心慌——用户越多亏得越快。我在过去一年里做过几个 AI 应用也被“每卖一单亏一笔”的情况狠狠教育过最后才把成本模型和计费系统当成一等公民来设计。这篇内容不是讲理论而是把一个可盈利 AI 产品最核心的“算账 收费”闭环完整拆开从成本构成、单位经济模型到计费模式的业务选型再到用量计量、计价扣费的具体代码实现最后把我在实战中踩过的坑一并列出。全程以 Python/FastAPI PostgreSQL 作为落地栈适合独立开发者、小团队技术负责人以及正在把 AI 功能产品化的产品经理。不管你是做 AI Agent、AI 写作工具还是图像生成服务这套思路可以直接抄。1. 先算清账AI 产品的成本到底由什么构成1.1 Token 不是唯一成本一份完整的 AI 产品成本拆解清单很多人一想到 AI 产品成本第一反应就是“大模型 API 的 token 费用”。但实际上一个线上可用的 AI 产品成本是分层的。如果只盯着 token 单价后面一定会出问题。先给一份我常用的成本拆解清单成本类别具体项目说明推理计算大模型 API 调用费按输入/输出 token 计费是最大的变动成本推理计算自建推理 GPU 算力如果是自部署开源模型按 GPU 小时算基础设施云服务器、数据库、对象存储用户管理、文件存储、日志存储都要钱第三方服务向量数据库、语音识别、OCR、图片处理Agent 类产品经常要调多个模型或工具边缘损耗失败重试、空转心跳、链路调试相当多 token 消耗在“没有产生价值”的调用上固定成本域名、短信验证码、邮件服务、监控容易被忽略但每月都在扣人力成本开发、运维、客服对独立开发者可能不计但团队必须算这里我特别想强调“边缘损耗”。很多 AI 产品真正赔钱的地方不在单次成功调用的成本而在于大量失败重试、用户未等到结果就刷新导致的重复请求、以及调试 Agent 链路时反复触发模型调用。我之前做过一个客服机器人上线后统计发现大约 18% 的 token 消耗在了超时重试和空会话上。这意味着你的真实成本比“理想单次成本”至少高 20%。所以在做成本模型之前先建立一个认知真实成本 理想情况下的 token 成本 × 损耗系数 固定成本分摊。损耗系数建议从 1.2 起步如果链路复杂、经常需要工具调用取 1.5 不夸张。1.2 单位经济模型从单次调用成本倒推定价底线算清楚总成本之后要落到“单次调用成本”上否则没法定价。单位经济模型的核心就是算穿一笔账用户每用一次你的 AI 功能你到底花了多少钱。以最常见的文本生成场景为例计算逻辑如下平均每次请求输入 prompt 2000 token输出 800 token模型价格示意输入 0.15 元 / 1K token输出 0.60 元 / 1K token那么单次理想成本就是输入成本 2000 / 1000 × 0.15 0.30 元 输出成本 800 / 1000 × 0.60 0.48 元 单次理想成本 0.78 元然后加上损耗系数 1.2 和固定成本分摊。假设固定成本服务器 数据库 第三方服务每月 500 元预计月调用量 2 万次固定分摊就是 0.025 元/次。真实单次成本 0.78 × 1.2 0.025 ≈ 0.961 元也就是说你向用户收一次费的成本底线接近 1 元。如果一次调用收 0.5 元那就是标准的“卖一单亏一单”。我建议每个 AI 产品都在上线前做一张这样的表格把模型单价、平均 token 输入量、输出量、损耗系数、固定成本全列出来。这张表是定价和套餐设计的唯一依据。没有这张表谈盈利都是拍脑袋。1.3 成本优化的工程手段缓存、模型路由与上下文裁剪算完成本还不够还得主动把成本降下来。我试过几类手段效果差异很大但每一样都有用Prompt 缓存是最容易落地的一刀。如果用户的系统提示词很长且多条请求共享同一段前缀直接把这段前缀对应的计算结果缓存下来。很多大模型 API 已经提供上下文缓存能力缓存命中时输入价格可以降到原来的 1/10 甚至更低。我自己做知识库问答类产品时把一份几千字的系统提示词命中缓存后单次请求的输入成本几乎可以忽略。模型路由则是把不同难度的请求分给不同规模的模型。简单的意图识别、分类、关键词提取用便宜的小模型就够复杂的推理、长文写作才上强模型。这一招能省下 40% 到 60% 的 token 费用但前提是你要有一套可靠的判断逻辑或者用一个轻量模型先做路由判断。不要小看这个“路由判断”本身的一次调用成本哪怕用小模型也要把它算进整体成本里。上下文裁剪是 Agent 类产品的关键。多轮对话里用户问了几轮之后历史记录越来越长如果不加控制每次请求都要把所有历史重新发给模型。我在实际项目里做过一个简单的策略超过 10 轮对话后把更早的历史做一次摘要用摘要替代完整原文只保留最近几轮的完整消息。这样既保留上下文又控制输入 token 增长。还有个容易被忽略的点失败重试必须做退避控制。很多 SDK 默认失败后会立刻重试用户在弱网环境下反复触发成本会成倍上升。我给请求加了超时控制和指数退避策略最多重试两次且每次重试间隔递增。从成本视角看不是为了更稳定而是为了不烧钱。2. 计费系统的业务设计不是“收钱”那么简单2.1 计费模式的选型逻辑按量付费、订阅套餐还是混合计费成本模型解决的是“花多少”计费系统解决的是“收多少”。计费模式选得不对要么用户不敢买要么买完你说亏本。目前主流有三种模式风险和收益各不相同模式适合场景优点缺点按量付费B 端 API、低频高价值工具用户按实际用量付费心理门槛低收入波动大用户担心费用失控订阅套餐C 端工具、高频刚需场景收入稳定可预期用户无心理负担需要精心设计额度防止超用混合计费中大型产品套餐兜底 超额按量计费逻辑复杂需要更严谨的系统我个人的建议是起步阶段不要做太复杂的混合计费先用订阅套餐加上一个超量停止策略跑通闭环。订阅套餐的好处是用户容易理解、收入可预测同时在套餐里设置“额度耗尽后自动停止服务”或“提示升级”避免用户在不知不觉中产生巨额超额费用既保护用户体验也避免坏账。如果做 API 服务比如你开放一个翻译接口、摘要接口给其他开发者按量付费会更合适。但这时必须在控制台提供实时用量追踪和预算警报不然开发者最怕的就是“夜里跑了个脚本第二天账单爆炸”。混合计费比如基础套餐 19 元/月包含 500 次超出部分按 0.1 元/次是很多产品的最终形态但我强烈建议把它放到第二期再做。第一期先跑通“订阅 限额”积累真实成本数据之后再放开超额通道。2.2 计费系统的核心模块拆解计量、计价、扣费、账单计费系统听起来是一个模块拆开看至少包含四个核心环节计量、计价、扣费、账单。这四个环节必须清晰分离否则后期改一个环节会牵一发而动全身。计量记录用户每次请求消耗的资源量。对 AI 产品来说就是 input_tokens、output_tokens以及调用次数。计量数据是所有后续环节的基础计量不准后面全是错的。计价把计量结果转换成金额。这里涉及定价表rate card不同套餐、不同模型对应不同单价。计价过程要求绝对严谨不能有浮点误差。扣费把金额从用户账户余额或套餐额度中扣除。扣费是资金操作必须保证一致性尤其是在并发场景下不能重复扣、不能超扣。账单生成用户可查的记录包括每次调用明细、扣费时间、余额变动。账单既是用户信心的来源也是你排查问题的依据。用生活类比解释的话整个流程就像你家的水电费计量是电表在走字计价是按照“多少度电 × 单价”算出钱数扣费是银行从你账户划走这笔钱账单则是月底寄给你的明细单。这四个环节任何一环出了问题用户都会来找你。在实际代码架构上我习惯把计量和计价做成两个独立的函数模块扣费走数据库事务。这样即便以后要调整价格、调整套餐也只是改计价层不需要动计量和扣费层。2.3 预付费与后付费余额模型、信用额度与风控计费系统除了算钱还要管账户。目前主流的设计是预付费余额模型用户先充值或购买套餐系统将金额或额度记入账户然后每次调用实时扣减。预付费模型的核心数据结构是账户account和余额balance。这里有一个很多新手会踩的坑不要把“充值赠送的金额”和“用户实付金额”混在一起。我之前做过一个活动充值 100 元赠送 50 元结果用户发起退款时系统把赠送的 50 元也退了直接亏了一笔。正确的做法是把余额拆成两个字段可用余额可消费、可提现的实付部分和赠送余额只可消费、不可提现。扣费时优先扣赠送余额退款时只退实付部分赠送部分直接回收。这套规则一开始就要设计好否则后补极其痛苦。后付费模型先用量、月底统一结算主要面向企业客户。如果要做后付费必须引入信用额度概念并且做实时风控。这里有个常用做法给每个企业客户设置一个信用额度当实时累计欠费超过额度 80% 时触发告警超过 100% 时直接停止服务。我的经验是小团队尽量不要放开后付费企业客户的回款周期和坏账风险都很考验现金流先用预付费积累足够用户信任度再说。3. 代码实现从成本核算到计费闭环的落地脚手架3.1 成本核算模块每次请求的 Token 统计与成本计算实践阶段我们从一个最基础的问题入手怎么在代码里准确拿到每次请求的 token 用量并算出成本。大部分大模型 API 返回结果里都带着 usage 字段比如{ usage: { prompt_tokens: 2000, completion_tokens: 800, total_tokens: 2800 } }问题在于很多团队在业务代码里根本不关心这个 usage直接丢掉。这是巨大的浪费——成本核算的数据源就这样没了。正确的做法是设计一个统一的模型调用封装把 usage 完整保留下来。下面是一个最小可用的成本核算模块Python 示例from dataclasses import dataclass from decimal import Decimal dataclass class UsageRecord: user_id: str model: str prompt_tokens: int completion_tokens: int property def total_tokens(self) - int: return self.prompt_tokens self.completion_tokens # 价格表这里用 Decimal 而非 float避免浮点误差 PRICE_TABLE { gpt-4o-mini: { prompt: Decimal(0.150), # 元 / 1K token completion: Decimal(0.600), # 元 / 1K token } } def calculate_cost(record: UsageRecord) - Decimal: price PRICE_TABLE.get(record.model) if not price: raise ValueError(f未知模型: {record.model}) prompt_cost Decimal(record.prompt_tokens) / 1000 * price[prompt] completion_cost Decimal(record.completion_tokens) / 1000 * price[completion] return (prompt_cost completion_cost).quantize(Decimal(0.0001))这里有几个实践要点必须用 Decimal 而不是 float。float 在做金额计算时会出现 0.1 0.2 0.30000000000000004 这类问题计费系统里每一分钱都要精确Decimal 是底线。价格表单独维护不要散落在业务代码里。以后调整价格、增加模型时只改这个配置。成本计算的结果要写日志或入库作为成本分析的原始数据。3.2 用量计量与计价引擎精确到分钱的扣费逻辑有了成本核算的雏形下面就是计费系统的主体计量 计价 扣费。先看数据库表设计我用 PostgreSQL 为例。最小可用设计包含四张表-- 用户账户表 CREATE TABLE account ( id BIGSERIAL PRIMARY KEY, user_id VARCHAR(64) NOT NULL UNIQUE, balance_cents BIGINT NOT NULL DEFAULT 0, -- 可用余额实付单位分 bonus_balance_cents BIGINT NOT NULL DEFAULT 0, -- 赠送余额单位分 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 用量日志表 CREATE TABLE usage_log ( id BIGSERIAL PRIMARY KEY, request_id VARCHAR(64) NOT NULL, user_id VARCHAR(64) NOT NULL, model VARCHAR(64) NOT NULL, prompt_tokens INT NOT NULL, completion_tokens INT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 扣费流水表 CREATE TABLE transaction_log ( id BIGSERIAL PRIMARY KEY, request_id VARCHAR(64) NOT NULL UNIQUE, user_id VARCHAR(64) NOT NULL, amount_cents BIGINT NOT NULL, -- 扣费金额单位分 balance_after_cents BIGINT NOT NULL, -- 扣费后余额 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );这里有个重要设计金额一律用“分”作为最小单位存储在整数列里。虽然 Decimal 可以在应用层处理精度但数据库层面的整数运算更可靠也更容易做索引和聚合。应用层展示时再转换成元的格式。接着是计价引擎。通常我们允许用户使用套餐额度所以我再加一个用户当前套餐查询然后统一走“先算成本再映射价格最后扣减”的流程from decimal import Decimal def calculate_price(record: UsageRecord, plan: dict) - Decimal: plan 示例 { type: subscription, monthly_fee_cents: 1900, included_requests: 500, over_price_cents: 10, # 超出每次 0.1 元按请求数计费 } if plan[type] per_request: return Decimal(plan[price_cents]) / 100 # 订阅套餐消耗一次请求次数不额外收费 return Decimal(0)但在混合计费场景下你还需要一个查询用户当月已用次数的逻辑。比如用户套餐包含 500 次当前已经用了 480 次这次请求属于套餐内第 481 次不扣钱第 501 次开始按超额单价计算。3.3 关键代码实现事务与原子扣费的完整流程计费系统最核心的部分就在这一步。扣费绝不是简单的balance - amount而是必须在事务里用条件更新完成原子扣减。下面是一个完整的扣费函数我把请求中的用量上报、计价、扣费整合在一起from fastapi import FastAPI, HTTPException, Header from sqlalchemy import text from sqlalchemy.orm import Session app FastAPI() # 通过 request_id 保证幂等防止重复扣费 def deduct_balance( db: Session, user_id: str, request_id: str, amount_cents: int, ) - bool: # 先检查 request_id 是否已经处理过幂等控制 existing db.execute( text(SELECT id FROM transaction_log WHERE request_id :rid), {rid: request_id}, ).first() if existing: return True # 已扣过直接返回成功 # 原子扣减优先扣赠送余额再扣实付余额 # 这里用 UPDATE ... WHERE balance amount 来保证余额充足 result db.execute( text( UPDATE account SET bonus_balance_cents bonus_balance_cents - :amount WHERE user_id :uid AND bonus_balance_cents :amount ), {uid: user_id, amount: amount_cents}, ) if result.rowcount 0: # 赠送余额不足剩余部分从实付余额扣 result db.execute( text( UPDATE account SET balance_cents balance_cents - :amount WHERE user_id :uid AND balance_cents :amount ), {uid: user_id, amount: amount_cents}, ) if result.rowcount 0: return False # 余额不足 # 写入流水 db.execute( text( INSERT INTO transaction_log (request_id, user_id, amount_cents, balance_after_cents) VALUES (:rid, :uid, :amount, 0) ), {rid: request_id, uid: user_id, amount: amount_cents}, ) db.commit() return True这段代码的核心逻辑是幂等控制每个请求有唯一 request_id重复请求不会重复扣费。这个在用户网络超时重试时尤其重要否则用户可能被扣两次。原子条件更新UPDATE ... WHERE balance amount本身就是原子操作数据库行锁会保证并发环境下不会超扣。赠送余额优先符合业务规则的“先用赠送再用实付”。在实际调用模型的位置流程是接收请求 → 调用模型 → 获取 usage → 算价 → 扣费 → 返回结果。注意扣费应发生在“返回结果前”。如果先返回结果再异步扣费可能会导致用户拿到结果但余额扣费失败形成坏账。3.4 计费 API 设计给上层业务使用的统一入口为了让业务方接入简单我习惯把计费能力封装成两个 API 接口一个是“确认可用”的预检接口一个是“上报用量并扣费”的最终接口。预检接口在用户进入耗钱的 AI 操作之前先检查余额或套餐额度是否足够。并不是完全必要的但对大额请求有意义能提前阻断超额调用。app.get(/v1/billing/check) def check_balance(user_id: str, estimated_cost_cents: int): # 伪代码查询余额和赠送余额 # 如果 total estimated_cost_cents 返回允许 # 否则返回错误码和当前余额 return {allowed: True, balance_cents: 12345}扣费接口真正的扣费动作一般由服务端内部调用不直接暴露给客户端。app.post(/v1/billing/charge) def charge(request_id: str, user_id: str, amount_cents: int): ok deduct_balance(db, user_id, request_id, amount_cents) if not ok: raise HTTPException(status_code402, detail余额不足) return {status: success, request_id: request_id}这里补充一个经验不要让前端传金额。前端传金额意味着用户可以通过篡改请求来绕过计费或者因为前端 bug 导致扣费金额错误。正确的做法是后端根据 usage 记录自己计算金额前端只传递 request_id。4. 常见问题与避坑实录4.1 并发场景下的扣费一致性为什么不能先查询再扣减我第一次写扣费逻辑时用的是“先查余额判断够不够再更新余额”的流程。上线第一天就在压测下出了问题多个并发请求同时读到余额为 100同时判断“够扣”然后同时更新为 90凡是被扣了 10 元的请求都成功了但总共只扣了 10 元另外 90 元凭空消失了。这就是经典的“先查再改”并发问题。解决方案就是前面代码里写的直接用UPDATE ... SET balance balance - amount WHERE balance amount。这条语句在数据库层面是原子的底层会加行锁保证同一时间只有一个请求能成功扣减。如果使用 Redis 做余额存储更简单的方式是使用 Lua 脚本或DECR操作配合判断返回值是否小于 0。但无论如何永远不要用“读再写”的方式处理金融金额。4.2 Token 统计口径不一致为什么日志里的成本和账单对不上大模型 API 返回的 usage 是一个值但你自己用 tiktoken 估算可能是另一个值。原因在于不同模型的 tokenizer 不同同一个文本在不同模型里 token 数量不一样。更常见的问题出在请求重试和多工具调用上一次业务操作可能触发多次模型调用如果把每次调用的 usage 单独记用户会看到“明明只问了一个问题账单里却有 3 条记录”。我的解法是引入会话级聚合一次用户请求对应一个 request_id内部可能包含多次模型调用最后账单展示时按 request_id 聚合成一条记录内部明细单独保留可查。这样用户看到的是“一次提问、一个价格”对账时又能展开明细。同时在消费 usage 日志时我额外记录了自己的 token 估算值方便两边对账而不是等到用户投诉时才被动排查。4.3 退款、赠送额度与账务处理这几个规则的坑一定要注意退款虽然频率不高但一旦出问题就是资金损失加用户投诉。我整理了几个必须提前确定的规则赠送余额一律不退。充值赠送的钱是用来激励消费的不是用户资产。退款时只退实付余额。退款后回收赠送额度要小心。如果用户消费时是“先用赠送后现实付”退款时就要算清楚实际消耗了谁的额度避免出现“用户想退 100实际他已经消耗了 80 赠送 20 实付最终退 20 还是退 100”这样的账务纠纷。套餐额度按月重置。定期任务里要把所有订阅用户的已用量清零同时记录上月用量作为运营数据。这个重置过程也要做幂等防止重复执行导致额度翻倍。我曾经就因为没把赠送余额单独拆字段导致活动结束后对账差了一千多块排查了两天。事后我把账户表拆成了“实付余额 赠送余额 冻结金额”三个字段再有类似活动就再也没出过问题。4.4 快速迭代中的计费灰度与可观测性最后一个坑是团队只在“要收钱了”的时候才想起计费系统结果上线即事故。计费系统应该从第一天就埋点。我建议至少在日志系统里记录以下内容每次请求的 token 用量和模型名称用户的套餐类型和当前剩余额度计价计算结果和后端扣费金额请求耗时尤其是大模型调用耗时有了这些日志你才能回答用户的三连问“我什么时候调的”“调了什么”“为什么扣了这么多”没有结构化日志客服只能干瞪眼开发只能查半天数据库。灰度发布时计费逻辑一定要做 A/B 对比新老逻辑同时跑一段时间用线上数据对比同一个请求在新旧计算方式下是否得出一致的费用。我在改价格表时踩过一次新旧计价引擎在“四舍五入”规则上不一致导致部分订单差了 0.01 元虽然后续补丁修复了但这种信任损伤很难修复。回到开头说的那句话AI 产品的技术能力决定用户愿不愿意用成本模型和计费系统决定你能不能活下来。写代码的时候别把 usage 丢掉上线之前把单位成本算明白扣费操作永远用原子更新这三件事做好了你已经跑赢了绝大多数只重视“模型效果”的同类产品。最后一个小建议计费系统的代码要单独隔离不要跟业务逻辑混在同一个服务里部署哪怕暂时是一个代码仓库的两个目录也要保持边界清晰。这样等用户量上来、需要单独扩展计费服务的时候你不需要重写任何逻辑。