用 Flask + SQLite 构建 AI API 消费排行榜:Token 费用透明化实战

发布时间:2026/8/30 14:49:37
用 Flask + SQLite 构建 AI API 消费排行榜:Token 费用透明化实战 在个人开发者圈子里AI API 花费失控是一个几乎没人聊透的问题。很多人月初看着 20 美元额度觉得自己能用很久结果写几个脚本、跑几轮批量任务账单就悄悄翻倍。更尴尬的是你根本说不清钱花在了哪个项目、哪次请求、哪个模型上。TokenMaxxer 这类“AI 消费排行榜”项目正是冲着这个痛点来的它把 token 消耗和费用变成一张可以和朋友比拼的榜单让每一笔 API 调用都从“隐形支出”变成“可见行为”。这篇文章不打算只做概念分析而是会拆解 TokenMaxxer 背后真正核心的东西如何设计 token 用量记录模型、如何做多模型费用折算、如何聚合生成排行榜以及如何把整个功能做成一个可运行的小型 Web 应用。读完你可以自己实现一个相同思路的“AI 花费排行榜”接到自己的 API 网关、团队小工具或个人开发环境里。1. 这篇文章真正要解决的问题先说结论TokenMaxxer 解决的不是“怎么省 token”而是“怎么让 token 花费可见并且变得有动力去控制”。如果你只是一个人开发账单再高也只有自己心疼。一旦你身边有几个同样在玩 AI 模型的朋友或者你在一个 5 到 10 人的小团队里token 花费就变成了一件需要透明化的事。过去大家各刷各的 API Key月底财务一看费用超标只能所有人一起降级模型。现在通过排行榜谁用得最多、谁的调用次数最高、谁在哪个模型上烧钱一目了然。更关键的是排行榜这种形式改变了用户行为。普通监控面板只是“看数字”看完就关排行榜却引入了比较和社交压力。当大家都看到某个同事用 gpt-4o 跑了一批超长对话费用冲到第一其他人自然会反思自己的调用方式。这比发公告、发邮件提醒有效得多。所以这篇文章要解决的问题包括三件事如何统一记录不同模型的 token 消耗和费用。如何把零散的调用记录聚合成个人排行榜。如何用最小的技术成本把它做成 Web 页面让朋友和队友能直接查看。适合读这篇文章的人主要是正在做 AI 工具、API 网关、团队成本管理或者单纯想给自己 AI 使用习惯“上点压力”的开发者。读完后你不仅能理解原理还能照着代码在本地跑通一个完整示例。2. TokenMaxxer 的核心概念与适用场景2.1 Token 到底是什么在 OpenAI、Anthropic、Google 等大模型 API 中Token 是模型处理文本的最小单位。简单理解一个 Token 可以是半个词、一个词甚至是一个标点。不同模型的 Tokenizer 不同中文场景下通常一个汉字可能对应 1 到 2 个 Token英文一个单词大约对应 1 到 2 个 Token。Token 分两种Input Tokens请求里你发给模型的文本。Output Tokens模型返回给你生成的文本。计费时大多数 API 按 Input Tokens 和 Output Tokens 分别计价Output 单价通常比 Input 高。比如某模型的输入可能是每 100 万 Token 收 3 美元输出可能是每 100 万 Token 收 15 美元。这种“双向计费”的特点决定了你不能只记录总 Token 数必须拆开记录。在 TokenMaxxer 场景里我们记录的是每次调用的输入、输出 Token 数然后乘以对应模型的单价得到一次调用的费用。原始 Token 数只是中间指标真正的排行榜排序依据是折算后的美元费用。否则一个用免费模型的用户可能调用 100 万 Token另一个用高价模型的用户只调用 1 万 Token两者根本没有可比性。2.2 费用折算为什么不能省有读者可能会问直接用总 Token 数排行不行吗表面上可以但不公平也不真实。假设朋友 A 天天用某个轻量模型跑日志分析Token 消耗巨大但费用很低朋友 B 只用了几个长文档总结请求调用次数少但输出很长费用可能反而更高。如果只按 Token 数排名A 永远第一B 的“高成本习惯”就被掩盖了。只有把每条记录折算成费用排行榜才有业务意义。不过费用折算有个隐藏问题模型价格会变。某家大模型公司今天调价明天另一个模型降价如果价格表写死在代码里排行榜数据就会失真。所以在实现 TokenMaxxer 时应该把价格表独立成一个配置模块或者放在数据库里方便随时调整。这样既保留灵活性又不影响历史数据。2.3 排行榜适合哪些场景排行榜本身是一个很轻的社交化功能但它的适用场景可以很广个人开发小圈子几个朋友各自调用 AI API互相比赛谁更省。团队内部成本透明化让团队成员看到各自 API 消费形成成本意识。开源项目演示给项目加一个“Token 消耗排行榜”增加趣味性。企业内部 AI 平台统计不同部门或项目的模型调用成本为预算分配提供依据。不适用的情况也很明显如果企业有严格的财务审计要求就不能只靠一个简单的排行榜来做成本核算。排行榜适合“快速感知”和“行为引导”不适合替代正式的账单系统。正式系统需要准确的汇率结算、税费、折扣、退款、对账等能力那是另一个量级的事。3. 技术方案选型为什么用 Python Flask SQLite实现一个 TokenMaxxer 排行榜可以使用的技术组合非常多。最朴素的方式是用一个后端服务接收 API 调用记录把数据存进数据库再提供一个排行榜查询接口。前端可以是一个简单的 HTML 页面也可以是一个 React/Vue 应用。这里我选择 Python Flask SQLite理由有三个成本低Flask 是一个极简 Web 框架SQLite 是 Python 标准库自带的数据库不需要额外安装数据库服务。容易理解整个项目只有少数几个文件适合作为参考实现。可迁移数据库表结构设计好后后续换成 PostgreSQL、MySQL 的成本很低涉及的核心逻辑不变。如果你更熟悉 Node.js也可以把 Flask 换成 Express把 SQLite 换成 better-sqlite3整体思路完全一致。本文后面的代码会尽量把“业务逻辑”和“框架”解耦方便你移植。这个选型的前提是并发量不高。SQLite 适合单机、小规模写入如果你们团队一天只有几千条调用记录完全没问题。如果吞吐量到了每秒几十次写入建议换 PostgreSQL 或 MySQL。4. 环境准备与前置条件在动手之前先确认你的本地环境。4.1 必需的运行环境Python 3.9 或更高版本。pip 包管理器。一个能执行 Python 脚本的终端。不需要单独安装数据库Python 标准库自带的 sqlite3 模块即可。4.2 项目依赖本文示例只依赖一个第三方库Flask。你可以在项目目录里创建一个虚拟环境然后安装mkdir tokenmaxxer-demo cd tokenmaxxer-demo python3 -m venv venv source venv/bin/activate pip install flaskWindows 环境下激活虚拟环境使用venv\Scripts\activate安装完成后通过下面的命令确认 Flask 可用python -c import flask; print(flask.__version__)如果你看到版本号输出说明环境就绪。4.3 项目文件结构我们打算创建以下文件tokenmaxxer-demo/ ├── app.py # Flask 主应用 ├── schema.sql # 数据库表结构 ├── record_usage.py # 模拟客户端向服务端提交用量 └── templates/ └── index.html # 排行榜页面其中schema.sql负责初始化表结构app.py提供 API 和页面record_usage.py用来模拟不同用户提交调用记录templates/index.html负责展示排行榜。5. 数据库设计与核心逻辑数据库设计是整个 TokenMaxxer 最关键的部分。如果只做排行榜至少需要两张表用户表和使用记录表。用户表存储参与排行的成员信息-- 文件路径schema.sql CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime(now)) ); CREATE TABLE IF NOT EXISTS usage_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, model TEXT NOT NULL, input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0, total_tokens INTEGER NOT NULL DEFAULT 0, cost_usd REAL NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime(now)), FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE INDEX IF NOT EXISTS idx_usage_records_user_id ON usage_records(user_id); CREATE INDEX IF NOT EXISTS idx_usage_records_created_at ON usage_records(created_at);为什么把total_tokens作为字段存下来而不是每次查询时计算理由很简单历史记录的输入输出 Token 数不会变但如果你在代码里临时计算还需要写一个函数查询性能也不好。直接冗余存储total_tokens和cost_usd让排行榜查询时直接做 SUM 聚合速度最快。价格表不用建表可以放到一个 Python 字典里。这样价格更新时只需要改代码方便。示例价格如下MODEL_PRICES { gpt-4o: { input_per_1m: 2.50, output_per_1m: 10.00, }, gpt-4o-mini: { input_per_1m: 0.15, output_per_1m: 0.60, }, claude-3-5-sonnet: { input_per_1m: 3.00, output_per_1m: 15.00, }, }注意我只保留了示例用的价格模型实际价格以各家官网为准。你在部署时应把价格表维护在配置中心或数据库里并标注“价格最后更新时间”。6. 核心流程拆解从记录提交到排行榜展示整个流程可以拆成四个步骤。6.1 第一步创建或查找用户当record_usage.py提交一个用户名时服务端先判断用户是否存在。如果不存在就创建新用户。这比预先在页面里维护用户列表更灵活朋友之间互相拉人进来时不需要管理员手动加账号。6.2 第二步校验并计算费用服务端拿到请求里的model、input_tokens、output_tokens后先检查模型是否在价格表里。如果是不支持的模型直接返回 400 错误。然后根据价格表计算cost_usd input_tokens / 1000000 * input_per_1m output_tokens / 1000000 * output_per_1m这里必须保留足够的小数精度。费用最小单位是美元分甚至更低如果直接四舍五入到两位小数累计后会差距很大。SQLite 的 REAL 类型可以满足示例需求但生产环境建议用 NUMERIC 或 DECIMAL 类型。6.3 第三步写入记录并更新排行榜写入后不需要立刻单独维护一个“排行榜缓存表”。在数据量不大时每次都通过 SQL 聚合生成排行榜最简单可靠。具体做法是SELECT u.display_name, COUNT(r.id) AS request_count, SUM(r.input_tokens) AS total_input_tokens, SUM(r.output_tokens) AS total_output_tokens, SUM(r.total_tokens) AS total_tokens, ROUND(SUM(r.cost_usd), 4) AS total_cost_usd FROM users u LEFT JOIN usage_records r ON u.id r.user_id GROUP BY u.id ORDER BY total_cost_usd DESC;使用 LEFT JOIN能保证即使某个用户没有任何记录也会出现在榜单里费用显示为 0。这很重要否则新拉进来的朋友可能“消失了”体验不好。6.4 第四步前端展示前端只需要一个简单的 HTML 表格通过fetch调用/api/leaderboard接口然后把结果渲染成表格。不需要引入前端框架也不需要构建工具。对一个小工具来说原生 JavaScript 足够。7. 完整示例代码实现现在开始写完整代码。为了让你更容易理解我把所有核心逻辑集中在一个app.py中但会尽量保持结构清晰。7.1 Flask 主应用代码# 文件路径app.py import sqlite3 from flask import Flask, g, jsonify, render_template, request DATABASE tokenmaxxer.db MODEL_PRICES { gpt-4o: { input_per_1m: 2.50, output_per_1m: 10.00, }, gpt-4o-mini: { input_per_1m: 0.15, output_per_1m: 0.60, }, claude-3-5-sonnet: { input_per_1m: 3.00, output_per_1m: 15.00, }, } app Flask(__name__) def get_db(): if db not in g: g.db sqlite3.connect(DATABASE) g.db.row_factory sqlite3.Row return g.db app.teardown_appcontext def close_db(exception): db g.pop(db, None) if db is not None: db.close() def init_db(): db sqlite3.connect(DATABASE) with open(schema.sql, r, encodingutf-8) as f: db.executescript(f.read()) db.commit() db.close() def get_or_create_user(username, display_name): db get_db() user db.execute( SELECT id FROM users WHERE username ?, (username,) ).fetchone() if user: return user[id] cur db.execute( INSERT INTO users (username, display_name) VALUES (?, ?), (username, display_name), ) db.commit() return cur.lastrowid def calculate_cost(model, input_tokens, output_tokens): price MODEL_PRICES.get(model) if not price: raise ValueError(fUnsupported model: {model}) input_cost input_tokens / 1_000_000 * price[input_per_1m] output_cost output_tokens / 1_000_000 * price[output_per_1m] return input_cost output_cost app.route(/) def index(): return render_template(index.html) app.route(/api/usage, methods[POST]) def add_usage(): data request.get_json(forceTrue) username data.get(username) display_name data.get(display_name, username) model data.get(model) input_tokens int(data.get(input_tokens, 0)) output_tokens int(data.get(output_tokens, 0)) if not username or not model: return jsonify({error: username and model are required}), 400 if input_tokens 0 or output_tokens 0: return jsonify({error: tokens must be non-negative}), 400 try: cost calculate_cost(model, input_tokens, output_tokens) except ValueError as e: return jsonify({error: str(e)}), 400 user_id get_or_create_user(username, display_name) total_tokens input_tokens output_tokens db get_db() db.execute( INSERT INTO usage_records (user_id, model, input_tokens, output_tokens, total_tokens, cost_usd) VALUES (?, ?, ?, ?, ?, ?) , (user_id, model, input_tokens, output_tokens, total_tokens, cost), ) db.commit() return jsonify({ status: ok, cost_usd: round(cost, 6), total_tokens: total_tokens, }), 201 app.route(/api/leaderboard) def leaderboard(): db get_db() rows db.execute( SELECT u.username, u.display_name, COUNT(r.id) AS request_count, COALESCE(SUM(r.input_tokens), 0) AS total_input_tokens, COALESCE(SUM(r.output_tokens), 0) AS total_output_tokens, COALESCE(SUM(r.total_tokens), 0) AS total_tokens, COALESCE(ROUND(SUM(r.cost_usd), 4), 0) AS total_cost_usd FROM users u LEFT JOIN usage_records r ON u.id r.user_id GROUP BY u.id, u.username, u.display_name ORDER BY total_cost_usd DESC ).fetchall() board [dict(row) for row in rows] return jsonify({leaderboard: board}) if __name__ __main__: init_db() app.run(host0.0.0.0, port5000, debugTrue)代码里需要注意的点MODEL_PRICES是全局价格表实际项目应抽成配置或数据库表。get_db使用 Flask 的g对象管理 SQLite 连接避免每次请求重复建连。init_db在应用启动时读取schema.sql创建表和索引。/api/usage接口负责接收调用记录校验参数后计算费用并落库。/api/leaderboard接口返回按费用降序排列的排行榜。计算费用时使用1_000_000作为除数对应“每百万 Token”的计价方式。7.2 模拟客户端代码为了模拟几个朋友往排行榜里提交数据我写了一个简单的客户端脚本# 文件路径record_usage.py import json import random import requests API_URL http://127.0.0.1:5000/api/usage USERS [ {username: alice, display_name: Alice}, {username: bob, display_name: Bob}, {username: carol, display_name: Carol}, ] MODELS [gpt-4o, gpt-4o-mini, claude-3-5-sonnet] def random_record(): user random.choice(USERS) model random.choice(MODELS) input_tokens random.randint(500, 50000) output_tokens random.randint(200, 20000) return { username: user[username], display_name: user[display_name], model: model, input_tokens: input_tokens, output_tokens: output_tokens, } if __name__ __main__: for _ in range(20): record random_record() resp requests.post(API_URL, jsonrecord) print(resp.status_code, json.dumps(record, ensure_asciiFalse))这个脚本会随机生成 20 条模拟调用记录。你可以多运行几次让排行榜数据更丰富。运行前记得先启动 Flask 服务。7.3 前端排行榜页面templates/index.html是一个纯 HTML 页面通过浏览器加载后调用后端接口把排行榜渲染成一个表格!-- 文件路径templates/index.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 titleTokenMaxxer 排行榜/title style body { font-family: PingFang SC, Microsoft YaHei, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 16px; background: #f7f7f8; color: #1f2328; } table { width: 100%; border-collapse: collapse; background: #fff; box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08); border-radius: 8px; overflow: hidden; } th, td { padding: 12px 16px; text-align: right; border-bottom: 1px solid #ececec; } th { background: #2b3a4a; color: #fff; font-weight: 600; } td:first-child, th:first-child { text-align: left; } .rank { font-size: 18px; font-weight: 700; color: #d97706; } /style /head body h2AI 消费排行榜/h2 p按估算费用 USD 降序排列数据来自本地 TokenMaxxer 服务。/p table idboard thead tr th排名/th th用户/th th请求数/th th输入 Tokens/th th输出 Tokens/th th总 Tokens/th th费用 (USD)/th /tr /thead tbody idboard-body/tbody /table script async function loadLeaderboard() { const res await fetch(/api/leaderboard); const data await res.json(); const tbody document.getElementById(board-body); tbody.innerHTML ; data.leaderboard.forEach((item, index) { const tr document.createElement(tr); tr.innerHTML td classrank${index 1}/td td${item.display_name}/td td${item.request_count}/td td${Number(item.total_input_tokens).toLocaleString()}/td td${Number(item.total_output_tokens).toLocaleString()}/td td${Number(item.total_tokens).toLocaleString()}/td td$${Number(item.total_cost_usd).toFixed(4)}/td ; tbody.appendChild(tr); }); } loadLeaderboard(); /script /body /html这里的前端没有写自动刷新逻辑。你可以每隔 30 秒重新调用一次loadLeaderboard()但这只是演示。实际使用中如果由多人同时操作建议前端页面用setInterval定期刷新比如setInterval(loadLeaderboard, 30000);8. 运行结果与效果验证代码写完后按下面的顺序启动和验证。8.1 启动服务python app.py启动日志会显示类似内容* Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:50008.2 提交模拟数据打开另一个终端运行python record_usage.py如果一切正常会输出 20 行201和对应的 JSON 数据。看到201代表服务端成功创建了记录。8.3 查看排行榜接口在浏览器访问http://127.0.0.1:5000/api/leaderboard或者用curl查看curl http://127.0.0.1:5000/api/leaderboard返回内容类似{ leaderboard: [ { username: bob, display_name: Bob, request_count: 7, total_input_tokens: 182340, total_output_tokens: 88412, total_tokens: 270752, total_cost_usd: 0.9342 } ] }注意由于我们使用了随机数据每次运行结果都会不一样。只要接口正常返回 JSON并且按total_cost_usd降序排列就说明核心逻辑没问题。8.4 查看页面效果访问http://127.0.0.1:5000/会看到一个简洁的排行榜表格。这个页面会调用后端接口动态展示当前所有用户的数据。如果页面没有数据优先检查 Flask 服务日志看/api/usage接口是否真正写入了记录。9. TokenMaxxer 常见问题与排查方法实现过程中有几个问题比较典型这里整理成表格。问题现象可能原因排查方式解决方案排行榜金额为 0没有写入 usage_records或前端格式化出错查看数据库记录和接口返回确认record_usage.py提交成功检查cost_usd字段请求返回 400 Unsupported model模型名和价格表不一致打印提交的 model 值检查模型名拼写或补充价格表页面不显示数据后端未启动或端口不对访问/api/leaderboard看是否报错确认 Flask 服务运行在 5000 端口同一个用户有多行排行数据SQL 聚合条件写错检查 GROUP BY 字段确保 GROUP BY 包含u.id等主键字段SQLite 报 database is locked多线程写并发过高查看后端日志改用连接超时、WAL 模式或迁移到 PostgreSQL数据重复提交客户端脚本重复运行或点击了多次提交按钮查看 usage_records 记录数量增加幂等键 / 唯一约束或从前端禁用按钮这里特别提一下 SQLite 的 WAL 模式。如果你们团队用这个工具时并发写入量不大SQLite 默认模式通常没问题。但如果出现database is locked可以在初始化数据库之后执行PRAGMA journal_modeWAL; PRAGMA busy_timeout5000;在app.py的init_db()里加入这两行能明显改善并发写入体验。10. 最佳实践与工程建议如果你打算把 TokenMaxxer 从“本地小玩具”变成一个真正可用的团队工具下面这些建议值得参考。10.1 不要把 API Key 放在前端或客户端文中示例是“服务端接收别人提交的 Token 数据”但实际使用时更安全的做法是让 API 请求统一经过一个代理层。开发者在自己的后端服务中调用大模型 API拿到响应后把usage信息发送给 TokenMaxxer 服务。不要把模型 API Key 暴露给前端页面或直接发给朋友否则别人可以直接用你的 Key 消耗额度。10.2 用中间层自动记录而不是手工上报手工调用record_usage.py只是演示。真实场景里应该在调用大模型 API 的 HTTP 客户端里写一个拦截器或装饰器。每次请求成功拿到响应后自动从响应中的usage字段读取prompt_tokens、completion_tokens和model然后异步上报到 TokenMaxxer 服务。这样用户不需要手动上报数据也不容易漏。10.3 价格表要单独管理模型价格是变动的。建议把价格表放到环境变量、配置中心或数据库表model_prices中并保留更新记录。避免为了调价格而改代码重新部署。10.4 索引与清理策略如果usage_records表越来越大排行榜查询会变慢。建议给created_at加索引并定期把超过 90 天的明细归档到历史表。排行榜页面默认只展示当月或近 30 天数据避免每次都要聚合全量数据。10.5 设置预算告警排行榜的价值是“事后看见”但更高级的做法是在写入记录时判断用户累计费用是否超过阈值。一旦超过可以在接口返回里警告或者在页面顶部显示“该用户本月已超预算”。这个逻辑用 SQLite 做也很简单查询某个用户当月的 SUM 即可。10.6 时区和统计口径不同模型的计费统计口径可能不同。有的按请求发起时间算有的按响应完成时间算。建议在数据库里存 UTC 时间显示时再转换为本地时区。排行榜的“今天”“本周”“本月”等时间段也要统一按 UTC 计算否则容易出现跨时区误差。11. 总结与后续学习方向TokenMaxxer 这个思路最值得学习的不是它做了一个好看的网页而是它把 API 可观测性和游戏化激励结合起来。通过记录每次调用的输入、输出 Token 数再按模型单价折算成费用最后用排行榜展示一个原本看不见的浪费问题就变得非常直观。这个模式可以用在个人开发、小团队、开源项目甚至企业内部 AI 平台。如果你要动手实践建议按这个顺序推进先跑通本文的 Flask SQLite 示例理解数据表和接口然后把上报逻辑嵌入到你真正调用大模型的代码里最后根据你的使用场景增加模型价格管理、预算告警和数据清理策略。可以继续深入的方向还有很多支持更多模型、接入真实云厂商账单、用 WebSocket 实时推送新记录、把排行榜嵌入企业微信群机器人或钉钉群、增加按周/月筛选的时间维度。比起一开始就设计一个“功能全面”的系统先做一个最小可用版本并且真正跑起来是更符合 TokenMaxxer 这类工具气质的开发方式。希望这篇文章能给你一个清晰、可落地的起点。