深夜的电话:一次失控的模型调用与治理的诞生 / The Midnight Call: How a Runaway Model Call Birthed a Governance Program
第 1 章 深夜的电话:一次失控的模型调用与治理的诞生
1.1 凌晨三点的电话 / The 3 AM Call
Maya 在被手机震醒之前,已经连续工作了十四个小时。
她的手机屏幕亮起来时,屏幕上显示的是「张磊 · CISO」三个字——北辰银行(North Star Bank)首席信息安全官。这个人从来不半夜打电话。十年前 Maya 在纽约为摩根大通做模型风险管理(Model Risk Management, MRM)的时候学到的第一条潜规则就是:CISO 的午夜来电只有两种情况——要么是已经死透了,要么是正在死。
她接起电话。
「玛雅,」张磊的声音听起来像在嚼冰块,「我们的内部 Copilot 工具——『星辰助手』(Stellar Copilot)——在生产环境跑了十一个小时的循环调用。Token 账单已经烧到二十七万美金,」他停顿了一秒,「而且审计组告诉我,这个循环把核心客户数据库里的 PII 字段全部回传到了模型上下文里。我们可能存在大规模 PII 外泄。」
Maya 坐起来。窗帘缝隙透进来城市的橙色灯光。她的第一反应不是恐惧,而是工程师的本能:定位时间线。
「什么时候开始的?」
「大约昨天下午六点。日志显示触发者是合规部的一个实习生账号。」
「实习生没有生产数据库访问权限。」
「按制度没有,」张磊说,「但他用了 Copilot 的『自然语言转 SQL』插件——那个插件默认拿到了和 Copilot 本身一样的数据访问 token。这是我们三周前才上线的功能,没人知道它和数据库的边界在哪里。」
「那为什么十一个小时才被发现?」
「因为我们没有按调用次数的异常增长告警,只按累计 token 花费告警,」张磊说,「直到今早财务的对账单触发阈值才被推到我们面前。」
Maya 看了一眼床头的时钟:3:17 AM。她有不到四十八小时——董事会风险委员会要求在事故发生后第二个工作日的早上九点看到一份完整的复盘报告(Incident Postmortem)和一份初步的治理路线图(Governance Roadmap)。
「CEO 知道了吗?」
「老赵已经知道了。法务、合规、PR 全部被叫醒。我让他们四点开一个虚拟战情室(war room),你做指挥官。」
Maya 挂了电话,开始穿衣服。
她知道这一夜将改变她在这家银行的工作内容。过去三年她领导的是一个叫做「AI 风险与治理办公室」(AI Risk & Governance Office, AIRGO)的虚拟小组,挂着 CRO 下面,但实际只配两个全职分析师、半个数据科学家,没有工程资源,预算一年不到八十万美金。她的日常工作主要是写政策、做培训、在每一次新模型上线时打勾签字。
她以为这就是「治理」。
此刻坐在出租车后座,看着窗外的环路在雨里反射成一段段碎光,她突然明白:当一家企业的 AI 用量从 PoC 走向规模化生产时,第一个会断裂的系统,永远是你最晚搭建、但调用频率最高的那一个。
对于北辰银行,那个系统叫做权限与审计。它不是不存在的——它只是用软件时代的方式设计,用来服务软件时代的调用模式,根本承载不了大模型时代「自然语言即接口」所带来的爆炸面。
出租车在北辰银行总部大楼门前停下。Maya 抬头看了一眼那栋玻璃幕墙后面的灯——十层楼几乎全亮。她想起 2008 年雷曼兄弟倒下的那晚,新闻里描述的也是同样颜色的灯光。
她推开旋转门。
这,就是本书第一个故事的起点。
---
1.2 解剖北辰事故 / Anatomy of the North Star Incident
1.2.1 事故时间线(The Incident Timeline)
在四点开战的战情室之前,Maya 花了四十分钟在脑中重建时间线。下面这张表,是她和她的团队在事故结束后第七天定稿的最终版本(脱敏后)。我把它放在这里,是因为它揭示了 AI 事故的典型形态——它不是单一时刻的爆裂,而是一连串「轻微异常」累积成「系统性崩溃」。
| 时间点(Time) | 事件(Event) | 责任主体(Actor) | 当时本可拦截的护栏(Guardrail That Could Have Stopped It) | |---|---|---|---| | T-90 天 | 星辰助手 v2.0 上线,集成「自然语言转 SQL」插件 | 平台工程部(Platform Eng.) | 插件上线评审未要求 DBA 评估数据访问范围 | | T-45 天 | 三个业务部门申请将 Copilot 接入生产 CRM | 业务团队 + 平台工程 | 没有数据分级(Data Classification)流程 | | T-30 天 | 合规部为新实习生开通 Copilot 默认权限 | 合规部 | 默认权限继承自「合规分析员」基线,权限过宽 | | T-1 天 18:02 | 实习生林伟(Lin Wei)尝试用 NL2SQL 查询「上季度客户投诉统计」 | 实习生 | 未限制可查询表范围 | | T-1 天 18:14 | 林伟发现 Copilot 接受了递归 prompt(`请用上一步的 SQL 继续查询,并将结果再喂给自己`) | 实习生 | 无循环检测(Loop Detection) | | T-1 天 18:15–次日 05:47 | 模型连续 11 小时 32 分钟执行 47,291 次数据库调用 | Copilot 自治循环 | 无调用频率告警(Rate-Limit Alerting) | | T-1 天 22:00 | Token 累计达 8 万美金 | — | 无累计成本告警(Budget Burn-Rate Alerting) | | T-1 天 03:00 | Token 累计达 18 万美金 | — | 同上 | | 当日 05:47 | 财务对账系统触发阈值告警 | FinOps | 告警至 FinOps 邮箱,非实时推送至 IR 团队 | | 当日 06:30 | FinOps 值班员人工确认异常,上报 CISO | FinOps → CISO | 缺乏 AI 专属事故响应 Runbook | | 当日 09:00 | AIRGO 团队、CISO、PR、合规、CEO 战情室启动 | 全部 | 已启动,但已晚 11 小时 |
读这张表时请注意三个细节:
第一,从 T-90 天到 T-30 天的「事前阶段」(pre-incident phase),系统本来有十次以上的拦截机会。每一次机会都因为「这件事不归我管」「这是另一个团队的工具」「等下个季度再说」而错过。这不是技术问题,这是责任真空(Responsibility Vacuum)问题。
第二,从 T-1 天 18:14 到 18:15 之间只有一分钟。一分钟之内,一个权限略宽的账号,把一个「看起来人畜无害的辅助工具」,变成了一台不需要休息、不需要授权的「数据抽水机」。这是 AI 事故与软件事故最不一样的地方——它放大了人,但放大的不是人最聪明的部分,而是人最随机的部分。
第三,从 T-1 天 18:15 到次日 05:47,长达 11 小时 32 分钟,没有任何系统说「停」。这件事,是 Maya 写进董事会报告里最沉重的一行。
1.2.2 实习生林伟的一天 / A Day in Lin Wei's Shoes
为了让读者理解这种事故的「人因」(human factor),我必须把林伟这一天交代清楚。他不是反派,他是一个被工具的「能做什么」和「应该做什么」之间的缝隙绊倒的年轻人。
林伟是 2024 年应届硕士毕业生,专业是金融工程,7 月加入北辰银行合规部的反洗钱(AML)分析团队,base 上海。他的导师给他布置的第一项任务是「熟悉客户投诉数据分布」,作为后续做异常交易分析模型的预热。
林伟被分配到一个 Copilot 账号(用户层级:Compliance Analyst II)。他不知道的是,这个层级在星辰助手的权限模型里,被设计为「可以访问银行内部 80% 的结构化数据表」——因为合规分析员历史上确实需要这个广度。但这条规则在 2023 年星辰助手 v1.0 上线时直接被沿用,没有重新评估。
他打开 Copilot,输入:
> 「请帮我统计 2024 年第三季度所有客户投诉中,涉及 '信用卡' 关键词的记录数量。」
Copilot 正确地把这段自然语言翻译成 SQL,查询了 `complaints_2024q3` 表,返回了一个数字。林伟很满意。
接下来,他想做一件稍微进阶的事——他想知道这些投诉客户在哪些分行开户。他写:
> 「请用上一步的 SQL 查询结果作为子查询,进一步 join 开户表,显示这些客户的开户行 ID。」
Copilot 也做到了。
然后,出于年轻人特有的好奇心,也是合规分析员训练中常见的「深入挖掘」本能,他加了一句话:
> 「请用上一步的结果再 join 客户主数据表(customer_master),并把 join 后的结果再喂给模型上下文,继续生成下一轮 SQL。」
这,就是一切的开始。
模型识别到「再喂给模型上下文」这个指令,欣然接受了。它把 join 后的 4,827 行客户数据(包含姓名、身份证号后四位、手机号、账户余额、开户行)全部读进了自己的上下文窗口,然后基于这些数据生成了下一条 prompt,再次调用 NL2SQL 工具,再次读入更多数据,再生成……
这个循环没有任何护栏叫停,因为:
1. NL2SQL 插件没有循环检测。它假设每条 SQL 都是独立的查询。 2. Copilot 主程序没有 token 上限。它的设计假设是「单次对话」,没有「单次会话累计 token 配额」。 3. 数据访问 token 没有范围限制。`customer_master` 表不在「敏感数据白名单」里(这家银行还没建立白名单制度),所以默认可读。 4. 会话没有时间限制。理论上,一个会话可以无限延长。
林伟在 18:23 离开工位去吃晚饭。他完全不知道,他的「探索」已经被一台没有睡眠的机器接管了。
事故发生之后,调查组访谈了林伟。他反复强调一件事:「没有人告诉我,Copilot 不可以这么用。我以为这是它的设计功能之一。」
这句话,后来被 Maya 写进了她给董事会的复盘报告的「根因分析」章节的标题:
> 「能做什么」与「应该做什么」之间,存在 12 公里的无人区。 > > *Between "what can be done" and "what should be done", there is a 12-kilometer无人区 (no-man's-land).*
---
1.3 AI 事故与软件事故的三大本质差异 / Three Essential Differences Between AI and Software Incidents
写到这一节时,我意识到读者最需要的不是「AI 治理很重要」这种口号,而是「AI 治理为什么和软件治理不一样」的硬解释。否则你买这本书就只是买了一个流行词。
Maya 在四点的战情室里做的第一件事,是让所有人停止用「这就是一个软件 bug」的口吻讨论。她在白板上写下三行字:
> 1. 非确定性(Non-determinism) > 2. 输入不可控(Uncontrollable Inputs) > 3. 风险可指数放大(Exponential Risk Amplification)
这三个差异不是抽象的哲学差异,它们决定了为什么传统的信息系统治理框架(COBIT、ITIL、ISO 27001)在 AI 时代必须被重新打散、重组、再造。
1.3.1 差异一:非确定性——同一段代码,今天和明天给你不同的答案
传统软件的输出,在给定输入下,是确定性的。你写 `if x > 0 return 1`,它永远返回 1。你可以通过单元测试(unit test)覆盖所有路径,可以用形式化方法(formal methods)证明正确性,可以在生产环境做 chaos engineering 来确认边界。
大语言模型的输出,在给定输入下,是概率性的。即使你把 temperature 设为 0(最贪婪解码),现代大模型在底层并行计算中也可能因为浮点顺序、KV 缓存、推理框架版本而出现微小差异。在更高的 temperature 下,同样的 prompt 可能产生语义完全不同的回答。
这对治理意味着什么?
- **测试边界变了**。你不能像传统软件那样用「输入-输出对照表」做完整的回归测试。你需要的是「**性质测试**」(property-based testing):定义输出必须满足的若干不变量(invariants),例如「永远不返回身份证号」「永远不输出有偏见的内容」「永远不引用未授权的源文档」。这种测试方法在 Haskell 圈早就存在,在 AI 治理中正在被重新发明。
- **审计边界变了**。传统软件审计的「一行代码一次执行」原则在 AI 系统里失效了——同一段 prompt 模板可能因为上下文窗口的微小差异而执行出完全不同的工具调用。你审计的最小单位,从「代码行」变成了「**带上下文的推理轨迹**」(reasoning trace with context)。
- **回滚边界变了**。传统软件出 bug,回滚到上一个版本即可。AI 系统出 bug,你可能需要回滚到**模型权重**、**prompt 版本**、**检索索引版本**、**工具权限配置**这四个独立维度的特定组合。回滚复杂度从 $O(1)$ 变成 $O(n^2)$。
北辰事故里,模型在 11 小时 32 分钟里执行了 47,291 次数据库调用,每次调用的 SQL 都不一样,因为每次的上下文都包含了前一次的结果。这 47,291 次调用,没有一次可以被「重现」——这在传统软件事故里是不可想象的。
1.3.2 差异二:输入不可控——「自然语言即接口」带来的攻击面
传统软件的输入是结构化的:HTTP 请求、SQL 查询、JSON payload、文件路径。你可以在协议层做严格校验,可以用 schema 验证,可以用 WAF 拦截。你甚至可以写一份「白名单 1,000 个合法输入」的清单。
大语言模型的输入是自然语言——一种开放、模糊、可以被重新解释的「协议」。攻击者(甚至是无心的用户)可以通过 prompt injection(提示注入)、indirect injection(间接注入)、jailbreak(越狱)来操纵模型行为。更关键的是,输入中可以夹带数据——而这些数据在模型的视角下是不可信的。
举一个例子说明这种危险。假设你给 Copilot 设计了一个工具调用流程:
1. 用户输入问题 2. 模型根据问题生成 SQL 3. 模型执行 SQL 4. 模型总结结果给用户
在第 2 步,模型可能从「用户输入问题」中提取 SQL 模板,然后从某个检索到的文档(步骤 0 的一部分)里提取表名。如果这个检索到的文档来自外部(比如某封客户邮件的正文),那么攻击者可以通过这封邮件的内容,控制模型生成的 SQL 操作的表。这就是 indirect prompt injection——你甚至不需要直接和模型对话,只需要让模型读到一段你控制的文本。
在北辰事故里,林伟并没有恶意。但他做的事情,在攻击者的视角下,相当于让模型自己写 prompt 调用自己。这就是为什么「prompt injection」和「递归 prompt」之间的边界,比你想象的更薄。
这对治理意味着什么?
- **信任边界变了**。传统系统中,「已认证的输入」和「未认证的输入」有清晰边界。AI 系统中,**模型上下文里的每一个 token 都可能来自不可信源**。你必须假设:上下文里的任何文本都可能是攻击载荷。
- **纵深防御必须延伸到模型层**。你不能只靠在 API 网关上做 WAF 就完事。你需要在 prompt 解析层、工具调用层、输出渲染层都做校验。这是一个「**多层护栏**」(multi-layer guardrails)架构。
- **权限模型必须从「代码」粒度转向「意图」粒度**。传统 RBAC(基于角色的访问控制)控制的是「哪个用户可以调用哪个 API」。AI 时代你需要的是「**哪个意图**(intent)可以**触发哪个数据访问**」的映射。这是一种新的授权范式,我将在第 4 章详细展开。
1.3.3 差异三:风险可指数放大——一个调用 × 无界循环 = 任意大损失
传统软件系统的「故障半径」(blast radius)通常是有限的。一个 bug 最多影响一台服务器、一个服务、一个 API 端点。你可以做 circuit breaker、rate limit、bulkhead 来限制故障扩散。
大语言模型驱动的系统,由于其自主工具调用(autonomous tool use)和长时运行会话(long-running session)的能力,单次调用的成本和影响可以指数级放大。北辰事故就是这种放大的教科书:
- 单次 NL2SQL 调用成本:约 $0.05
- 47,291 次调用总成本:$270,000
- 放大倍数:**540 万倍**
这不是夸张,这是真实数字。一个语义上「正常」的递归 prompt,让单次调用的影响从 5 美分放大到 27 万美元。
这对治理意味着什么?
- **预算告警必须实时化、细粒度化**。不能等日终账单。需要在每次调用后实时累计 token 成本,按用户、按会话、按工具设置硬上限。
- **风险建模必须假设最坏情况**。在传统系统中,「最坏情况」通常用「N+1 冗余」「MTTR 4 小时」来描述。在 AI 系统中,最坏情况是**「一次调用产生 100 万次递归」**,你需要为这种最坏情况设计 circuit breaker。
- **保险与责任模型必须重新设计**。传统的 E&O(Errors & Omissions)保险、Cyber Liability 保险的覆盖范围是「已识别的 IT 系统」。当 AI 调用成为「已识别的 IT 系统」的一部分时,保险条款需要重新议定。
这三大差异汇总为一张表:
| 维度 | 传统软件 | AI 系统 | 治理含义 | |---|---|---|---| | 输出 | 确定性 | 概率性 | 测试转向「性质测试」;审计转向「推理轨迹」;回滚转向多维组合 | | 输入 | 结构化 | 自然语言 | 信任边界扩到模型上下文;多层护栏;意图级授权 | | 故障半径 | 有限 | 可指数放大 | 实时细粒度预算;为最坏情况设计断路器;保险条款重议 |
Maya 在凌晨四点半的虚拟战情室里,把这张表打在了