先看出事的那一刻
2026 年 7 月 27 日,自动驾驶知识站的 AI 导师准备上线。导师每回答一次,都要调用一次付费的大模型接口,所以 AI 给它加了护栏:每人每天 80 条、每分钟 6 条,在自己的数据库里计数。上线前,AI 还提了一句"80 轮/人/天是我随手定的",建议调低到 30;站长回复:"不用改配额,先上线吧。"
两个月后的一次代码审查,才把另一半问题摆了出来:注册只检查用户名和密码的长度,没有验证码、没有邀请码、不按 IP 限流,注册完立刻拿到登录凭证。每人每天 80 条的护栏,挡不住一个人注册一百个账号。
这节课要回答的是:给网站加上付费的 AI 功能之后,怎样才不会被刷爆账单?每人每天多少次、全站每天多少钱,由谁来卡?
装备几个词
为什么要懂
AI 替你做了
AI 能在几分钟里加好注册登录、每人每天的次数限制,还会顺手写一句“防止滥用”的注释。
留给你的
想清楚护栏挡的是谁:一个用得太多的人,还是一个能变成很多人的人;以及全站每天最多愿意花多少钱。
不懂的代价
没人来刷的时候一切正常;一旦被刷,账单按“账号数 × 每人上限”增长,而你要等账单到了才知道。
护栏挡的是谁
把可能的护栏排一排,对照这个导师:
| 护栏 | 挡住什么 | 这个导师有没有 |
|---|---|---|
| 每人每天上限(额度) | 一个人用得太多 | 有,80 条(按最近 24 小时算,不是按日历上的一天) |
| 每分钟上限(限流) | 一个人问得太快、脚本连发 | 有,6 条 |
| 注册门槛 | 一个人变成很多人 | 没有 |
| 按 IP 限流 | 同一台机器批量注册、批量调用 | 没有 |
| 全站每日预算 | 所有人加起来花得太多 | 没有 |
前两道都按账号计。只要账号可以免费、立即、无限地注册,它们就能被绕过去。粗略算一笔账:按当时估的价格,每条回答大约一两分钱,一个账号用满 80 条是一两块钱;一百个账号,就是每天一两百块。而全站没有任何一个数字,会在花到某个金额时把门关上。
幸运的是,没有发现被滥用。但这个缺口从上线那天起一直开着。
超过上限的时候,接口说了什么
超过上限时,导师在页面上显示"今天的对话次数已用完(80 次),明天再来",或者"问得太快了,稍等几秒再发"。人看得懂,程序却看不出来:这两句话是放在流式输出的内容里送回去的,接口的状态码是 200。
标准的做法是返回 429(请求过多),并告诉对方多久之后可以重试。这样监控能统计出"有多少请求被拦下",调用方也知道该等一等。另外,"明天再来"也不太准确:计数是按滚动的 24 小时算的,不是按日历上的"明天"。
深潜检查和记账,最好一步完成+
计数的代码先查"这个人今天用了几条",没超就再写一行记录。这是两次独立的数据库操作,中间有一个空隙:几个请求同时到达时,它们可能都在对方写入之前完成了检查,于是一起通过。
对一个只有自己在用的网站,这个空隙无关紧要;对一个真会被脚本连发的接口,就要用事务或者数据库自带的计数功能,把"检查"和"记账"合成一步。
找茬
下面是 AI 导师的注册接口、护栏配置、计数和超限处理的真实代码(节选)。导师每回答一次都要花钱。找出让护栏可以被绕过、或者让超限这件事被掩盖的地方。
这段 JS 里埋了 5 处问题。点击你觉得有问题的行,至少找出 3 处再揭晓。
·第 2 行中危
注释说是真防线,防线只查了长度
注释写着"服务端才是真防线",可这道防线只检查用户名 2 到 20 个字、密码至少 6 位。没有验证码、邀请码,也不按 IP 限流:注册一百个账号,和注册一个一样容易。
该问的话:注册有哪些门槛?一台机器一分钟内能注册多少个账号?
·第 8 行高危
注册完立刻发放登录凭证
注册成功就直接返回可用的登录凭证,每个新账号立刻拥有一份全新的"每天 80 条"。按账号计的护栏,从这一行开始就可以被批量绕过。
该问的话:一个人能不能通过批量注册,拿到多份每日额度?
·第 12 行高危
只有按账号的上限,没有全站总预算
上面的注释把它称作"主护栏",可它护的是账号,不是钱。全站没有任何一个数字,会在花到某个金额时把门关上。
该问的话:全站每天最多会花多少钱?超过之后会发生什么?
·第 16 行低危
先查、再记,中间有空隙
检查次数和写入记录是两次独立的数据库操作。同时到达的几个请求,可能都在彼此写入之前通过了检查,一起超出上限。
该问的话:“检查次数”和“记下这一次”是在同一个事务里完成的吗?
·第 19 行中危
超限的消息藏在一个 200 里
超限的提示放在流式输出的内容里送回,接口状态码是 200。监控和日志统计只看状态码,看不出有多少请求被拦下。而且计数按滚动 24 小时算,"明天再来"并不准确。
该问的话:被限流的请求,返回的状态码是多少?监控能统计出来吗?
查看代码与答案(5 处问题)
1 // server/auth.mjs(注册校验,节选)
2 /** 注册与登录的输入校验(前端也会校验,但服务端才是真防线) */
3 if (typeof name !== "string" || name.trim().length < 2 || name.trim().length > 20) return "用户名需 2-20 个字符";
4 if (typeof password !== "string" || password.length < 6) return "密码至少 6 位";
5
6 // server/index.mjs(注册接口,节选)
7 const u = await createUser({ name: name.trim(), password, roleScene, projectType });
8 return c.json({ token: issueToken(u.id), user: { … } });
9
10 // server/config.mjs(护栏配置,节选)
11 // 服务端确定性护栏——主护栏。不依赖端点的用量上报
12 messagesPerDay: 80, // 每用户每日对话轮次
13
14 // server/db.mjs(计数,节选)
15 const { rows } = await q(`SELECT count(*) … FROM usage_log WHERE user_id = $1`, [userId]);
16 await q("INSERT INTO usage_log (user_id) VALUES ($1)", [userId]);
17
18 // server/tutor/service.mjs(超过上限时,节选)
19 yield { type: "error", reason: quota.reason, message: `今天的对话次数已用完(${quota.limit} 次),明天再来。` … };- 第 2 行 · 中危 · 注释说是真防线,防线只查了长度:注释写着"服务端才是真防线",可这道防线只检查用户名 2 到 20 个字、密码至少 6 位。没有验证码、邀请码,也不按 IP 限流:注册一百个账号,和注册一个一样容易。 该问的话:注册有哪些门槛?一台机器一分钟内能注册多少个账号?
- 第 8 行 · 高危 · 注册完立刻发放登录凭证:注册成功就直接返回可用的登录凭证,每个新账号立刻拥有一份全新的"每天 80 条"。按账号计的护栏,从这一行开始就可以被批量绕过。 该问的话:一个人能不能通过批量注册,拿到多份每日额度?
- 第 12 行 · 高危 · 只有按账号的上限,没有全站总预算:上面的注释把它称作"主护栏",可它护的是账号,不是钱。全站没有任何一个数字,会在花到某个金额时把门关上。 该问的话:全站每天最多会花多少钱?超过之后会发生什么?
- 第 16 行 · 低危 · 先查、再记,中间有空隙:检查次数和写入记录是两次独立的数据库操作。同时到达的几个请求,可能都在彼此写入之前通过了检查,一起超出上限。 该问的话:“检查次数”和“记下这一次”是在同一个事务里完成的吗?
- 第 19 行 · 中危 · 超限的消息藏在一个 200 里:超限的提示放在流式输出的内容里送回,接口状态码是 200。监控和日志统计只看状态码,看不出有多少请求被拦下。而且计数按滚动 24 小时算,"明天再来"并不准确。 该问的话:被限流的请求,返回的状态码是多少?监控能统计出来吗?
快测
1. 导师限制每人每天 80 条。下面哪种情况,这道护栏挡不住?
你可能是这么想的:计数按最近 24 小时算,不是按日历上的一天。过了零点,前一晚的记录还在窗口里,后面的请求照样会被拒绝。
对了。每个新账号都有一份全新的额度。按账号计的护栏,挡不住“一个人变成很多人”。
你可能是这么想的:计数是按账号记的,不管请求从几台设备发来,加起来超过 80 条,第 81 条起就会被拒绝。
护栏按什么计数,就只挡得住那一种滥用。
查看选项与答案
- A. 一个账号在午夜前问满 80 次,零点后再问 80 次——计数按最近 24 小时算,不是按日历上的一天。过了零点,前一晚的记录还在窗口里,后面的请求照样会被拒绝。
- B. 一个人注册了 50 个账号,每个账号都各问满 80 次(正确)——每个新账号都有一份全新的额度。按账号计的护栏,挡不住“一个人变成很多人”。
- C. 同一个账号在手机和电脑上同时提问,合计 100 次——计数是按账号记的,不管请求从几台设备发来,加起来超过 80 条,第 81 条起就会被拒绝。
护栏按什么计数,就只挡得住那一种滥用。
2. 请求被限流时,接口最好返回什么?
对了。人看得懂,程序也看得懂:监控能统计出被拦下的请求,调用方也知道该等一等。
你可能是这么想的:程序和监控只看状态码,会把它当成一次成功的请求,统计不出有多少请求被拦下。
你可能是这么想的:500 表示服务器自己出了故障。限流是服务器按规则拦下的,并不是故障。
状态码是写给程序看的:被拦下就说被拦下。
查看选项与答案
- A. 返回 429(请求过多),并告诉对方多久之后可以重试(正确)——人看得懂,程序也看得懂:监控能统计出被拦下的请求,调用方也知道该等一等。
- B. 返回 200,在流式内容里写“今天的次数已用完”——程序和监控只看状态码,会把它当成一次成功的请求,统计不出有多少请求被拦下。
- C. 返回 500,因为请求是被服务端拒绝的,属于服务端的问题——500 表示服务器自己出了故障。限流是服务器按规则拦下的,并不是故障。
状态码是写给程序看的:被拦下就说被拦下。
3. 给一个付费的 AI 功能设护栏,最该先定下来的是?
你可能是这么想的:重要,但挡不住批量注册:账号可以免费、立即、无限地注册,每个新账号都有一份新额度。
你可能是这么想的:便宜的模型只是让账单涨得慢一点,挡不住被刷。
对了。这是最后一道闸:账号再多,也花不过这个数。
先给总花费定一个上限,其余的护栏才有兜底。
查看选项与答案
- A. 每个账号每天最多能问几次,先把单个用户管住——重要,但挡不住批量注册:账号可以免费、立即、无限地注册,每个新账号都有一份新额度。
- B. 选用最便宜的模型,先把单次调用的成本压下来——便宜的模型只是让账单涨得慢一点,挡不住被刷。
- C. 全站每天最多花多少钱,到这个数就关上门(正确)——这是最后一道闸:账号再多,也花不过这个数。
先给总花费定一个上限,其余的护栏才有兜底。
判断时刻
AI 导师每回答一次要花一两分钱。现在注册不设限、每人每天 80 条。你只打算自己用,偶尔给几个朋友试试。
你会怎么改?
考察:缩小入口
最直接:能用的人你心里有数,按账号的护栏也就重新有了意义。代价是想试用的人得先找你要邀请码。
考察:对陌生人开放
适合真的想让陌生人来用的时候。能挡住大部分脚本,挡不住有心人;还多了一个验证码服务要接入、要维护。
考察:事后才知道
最省事,但提醒是事后的:它到的时候,钱已经花出去了,也说不清是谁花的。可以作为补充,不能当作护栏。
护栏要按“谁能进来”和“总共花多少”两层来设。先把门缩小,再给全站定一个花钱的上限,按账号的次数限制才真正有意义。
三个选项各自的代价
- A. 关掉公开注册,改成邀请码——考察缩小入口:最直接:能用的人你心里有数,按账号的护栏也就重新有了意义。代价是想试用的人得先找你要邀请码。
- B. 保留公开注册,加验证码、按 IP 限流——考察对陌生人开放:适合真的想让陌生人来用的时候。能挡住大部分脚本,挡不住有心人;还多了一个验证码服务要接入、要维护。
- C. 什么都不改,在模型平台后台设一个预算提醒——考察事后才知道:最省事,但提醒是事后的:它到的时候,钱已经花出去了,也说不清是谁花的。可以作为补充,不能当作护栏。
护栏要按“谁能进来”和“总共花多少”两层来设。先把门缩小,再给全站定一个花钱的上限,按账号的次数限制才真正有意义。
带走
下次让 AI 做这件事时,问它
- 这个付费功能谁能用?注册有没有门槛:验证码、邀请码、按 IP 限流?
- 一共有几道护栏:每人每天、每分钟、全站每日预算?各自在哪里计数?
- 超过限制时,接口返回什么状态码?用户看到什么?监控看得见吗?
- 按最坏情况估算:一个人批量注册账号、每个都用满上限,一个月会花多少钱?
自己验证
- 用一个新注册的账号连续请求,确认第几次开始被拦下、返回的状态码是什么。
- 在数据库里看一眼计数表:每次请求都记下了吗?记在谁的名下?
- 到模型平台的后台看实际用量和账单,和自己的计数对一对。
护栏先问谁能进来,再问总共能花多少。
