中间件应仅拦截 /api/chat、/api/image/generate 等真实调用 ai 服务的路由,排除 /health、/auth/login 等非计费路径及 options 预检请求;优先通过路由名判断而非 url 路径;计费统计宜置于 api 网关后、业务服务前;需为不同服务商实现统一 billableresponse 接口以适配 token 提取逻辑。

中间件要拦截哪些请求路径才合理
只对真正调用 AI 接口的路由做计费统计,比如 /api/chat、/api/image/generate 这类明确由前端发起、后端转发给第三方 AI 服务的请求。别把健康检查 /health、登录 /auth/login 或静态资源也包进去——这些不产生费用,统计反而污染数据。
常见错误是直接在全局中间件里无差别记录所有请求,结果发现账单对不上,因为日志里混进了大量非计费调用。
- 用 Laravel 的
$request->route()->getName()或 ThinkPHP 的Route::current()->getName()判断路由名更可靠,比靠 URL 路径匹配容错性高 - 如果用了 API 网关层(如 Kong),计费统计应放在网关后、业务服务前,避免重复或遗漏
- 务必排除 OPTIONS 预检请求——它们不触发实际 AI 调用,但会被某些框架自动捕获
如何从请求和响应中提取计费关键字段
不同服务商返回结构差异大:openai 在响应头带 x-ratelimit-remaining 和 x-ratelimit-reset,但计费核心得看响应体里的 usage;anthropic 返回 content 字段不带 token 数,得靠 usage.input_tokens 和 output_tokens;qwen(通义千问)则把 usage.total_tokens 放在顶层。
不能写死解析逻辑,否则换一个服务商就得改中间件代码。
- 定义统一抽象接口
BillableResponse,要求各服务商适配器实现getInputTokens()、getOutputTokens()、 - 中间件里不做 JSON 解析,只把原始响应体传给对应适配器——避免中间件承担格式转换职责
- 若响应失败(HTTP 4xx/5xx),仍需记录失败事件:有些服务商按“调用次数”收费(如早期 Azure OpenAI 某些 SKU),不是只算成功请求
计费数据写入时为什么不能直接 INSERT
高并发下多个请求几乎同时完成,直接往数据库 INSERT 计费记录,容易因主键冲突或唯一索引报错(比如同一秒内多个请求用了相同 trace_id)。更糟的是,如果中间件里还顺手更新用户余额表,事务锁表会拖慢整个 API 链路。
计费是事后审计行为,不是业务强依赖流程。
- 用 Redis 的
LPUSH把原始计费事件(含 timestamp、model、tokens、cost、request_id)暂存为队列,格式用 JSON 字符串 - 单独起一个 Laravel 的
artisan schedule:run或 Swoole Worker 定时消费队列,批量写入 MySQL 或 ClickHouse - 避免在中间件里调用
sleep()或重试逻辑——超时会传染到前端,必须保证中间件执行时间
怎么验证中间件没漏记也没多记
最有效的办法是拿真实流量跑 A/B 对照:一边走老逻辑(SDK 内部手动埋点),一边走新中间件,对比两套数据的 total_tokens 总和与请求量。偏差超过 0.5% 就得查。
容易被忽略的是异步调用场景——比如用户上传文件后,后端用 dispatch(new AiImageJob()) 异步生成图,这种请求不在 HTTP 生命周期内,中间件根本捕获不到。
- 对异步任务,要在 Job 的
handle()开头显式调用BillingRecorder::recordAsync(...),传入 job_id 替代 request_id - 在中间件里加个开关配置
billing.enabled,上线初期先设为 false,用日志输出模拟计费数据,确认字段齐全再开写入 - 每条计费记录必须带
source字段(值为middleware或job),方便后续归因
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











