首页 / 文章

V Vanity
先玩一下
这篇讲的是项目实现;如果你更想先看它怎么动,打开 AI 治理网关实验:调请求速率、看 429 和预算拦截,或者把一条 DELETE 送进护栏。

1为什么不是“接个模型 API”

“接个模型 API”本身不难。难的是把它放进企业环境之后,三件会立刻发生的事:

这个项目把三件事收进同一条请求路径:模型侧管路由、限流、缓存和成本;数据侧管“模型写出来的 SQL 到底有没有资格被执行”。两边共用鉴权、配额和审计,所以“这次调用花了多少钱”和“这次为什么被拒”能在同一张表里回答。

2一条请求经过什么

请求路径是刻意排成一条直线的:

request_id → edge(日志/指标) → body_limit → auth(key→租户)
  → policy(令牌桶 / 并发 / 日预算 / 月预算)
  ├─ POST /v1/chat/completions → 路由打分 → 熔断中的上游不参与 → 上游
  └─ POST /v1/nl2sql → 目录 + 少样本提示 → 模型给出候选 SQL
       → 护栏 review_sql → 执行器(mode=ro + 超时 + 行数硬截 + 结果集掩码)
       → 审计落库

被护栏拒绝不是 HTTP 错误:链路完整跑完了,网关给出了一个明确判定,所以状态码是 200、判定在 verdict 里。把它做成 4xx 会让“模型写错了 SQL”和“你没权限”共用一个信号,排障时很难分清。

3护栏:执行 AST 重新生成的那一串

护栏的核心断言只有一句:被执行的字符串永远是 AST 重新生成的那一个,不是模型给的那一个。 所有规则都在判定“要不要给这条 AST 生成执行文本”,而不是扫描原文里的危险关键字。关键字黑名单可以被注释、大小写、等价写法绕过,重新生成不行。

规则按优先级跑,两个顺序上的反直觉之处都是被语料逼出来的:

规则做什么为什么排在这里
GRD-000可解析没有 AST,后面任何规则都无从检查
GRD-001单语句多语句是注入的常见形状
GRD-002只允许 SELECT写语句在这里就被挡下
GRD-004表白名单先于标识符解析:目录里没有 sqlite_master,先跑列解析会把“表不在白名单”误报成“列解析失败”
GRD-003标识符可解析目录是唯一真相,不借第三方库的错误文案分类
GRD-005列白名单含 SELECT * 展开;id_card 这种不暴露列到这里才被拒
GRD-006函数白名单宁可漏放几个合法函数,也不放任何能落盘/读文件的入口
GRD-007禁止加锁与写出FOR UPDATE / SELECT INTO 是锁定读,不是普通 SELECT
GRD-008OFFSET 上限防止用深分页把数据库拖住
GRD-009RLS 谓词注入行级权限在这里变成执行计划的一部分
GRD-010外层行数上限执行器再兜一层硬截
GRD-011语句超时AST 层判断不了“跑多久”,交给执行器的 progress handler

放行时返回的 canonical_sql 才是执行文本。模型原文只作为审计证据保留,不直接进数据库。

4行级权限与列级掩码

行级权限不是“在 WHERE 里碰巧写了 dept_id”,而是护栏按别名给每一个引用了权限键表的 scope 注入谓词。比如租户的 rls_dept_ids = [1],对 payroll_runs 会生成:

SELECT id, base_salary
FROM payroll_runs
WHERE dept_id IN (:rls_0)
LIMIT 200;

参数 :rls_0 的值来自租户行,模型碰不到。没有部门列的表(比如考勤)沿外键路径受限:attendance.employee_id → employees.id,再对 employees.dept_id 注入谓词。所以用户把两张表写成逗号交叉连接、或者写错 ON 条件,都拿不到别人的行。

列级掩码作用在结果值上,是纯函数、可被审计重放,而不是拼进 SQL。并且沿表达式叶子继承:改名、substr、GROUP_CONCAT、MAX 都拿不到原值。实验里那条 SELECT substr(phone, 1, 3) FROM customer_pii 就是在演示这一点。

5预算、限流与 key 泄露

每个租户有独立的令牌桶、并发上限和日/月预算。默认演示租户:60 次/分、突发 10 个令牌、日预算 $0.20、月预算 $2.00。请求先过速率与并发,再过预算;超了分别返回 GW-RATE-LIMITED(429)和 GW-BUDGET-DAILY(402)。

关键点是:key 被复制后,调用方和正常用户共享同一套桶与预算。攻击者能把额度打满,但不能凭空多出一份额度。实验里把速率拉到 120 次/分,60 秒内会同时看到限流和预算拦截——这就是这层闸门存在的意义。

本机压测(2026-09-21,无真实厂商 key,1200 条请求)两次独立跑出同一金额:计费 599 条、限流 601 条、花费 $0.004293。这不是生产容量结论,只说明计费与限流两个计数器在同一批请求上能对齐。

6实测、边界与可复现

466
passed(2026-10-23)
18
skipped(条件不满足的继承闸门)
0
failed
73s
本机全套墙钟

本地已经完成的是:功能闭环、测试全绿、文档与 Dockerfile 收尾。没有完成的是“对外营业”那一半:

这个项目真正值得讲的一点不是“用了多少库”,而是把安全边界写进了执行路径:SQL 不是被扫描后放行,而是被重新生成后执行;权限不是文档约定,而是注入到 AST 里的谓词;花费不是账单出来才知道,而是随响应返回。

如果你想把这三层亲手点一遍,去 AI 治理网关实验;想看完整测试与边界,项目的 README 和 docs/SECURITY.md 是权威口径。