大多数AI助手的演示,都是把模型架在数据库上面。给智能体配一个“查询”工具,用户提问,模型写查询语句。演示时效果很好。到了生产环境,这意味着用户的判断和别人的数据之间,只隔着模型这一道关。

我正在把AI智能体做进The Arsenal,一个面向寿险代理人的平台。第一版故意做得很“无聊”:只读性能数据、回答问题,什么都不写。但即便是只读智能体,也坐在客户记录之上,所以在智能体成型之前,我按OWASP的LLM应用十大风险清单逐条过。这篇讲第二条——敏感信息泄露,以及由此得出的设计决策。

数据访问层是唯一的门

模型从不接触数据库。它只调用一小批窄接口,比如“取我本周的管道统计”。没有通用查询工具,没有裸SQL,没有例外。如果某个问题没有对应的函数,智能体就答不了——这正是设计意图。

所有安全问题都收敛到一个地方:数据访问层放行了什么。

身份来自会话,绝不来自模型

模型会索要线索ID、客户ID这类东西。那些是参数。真正承担安全职责的字段是“谁在问”,而模型从不提供它,由已认证的会话提供。数据访问层把调用者身份盖在每条查询上,并按该身份允许的范围过滤行。

如果模型能传“用户等于Alex”,它就能传任何人。所以它压根不该传用户。

字段只打一次标签,且默认关闭

一个KPI问题永远不需要客户的电话号码或出生日期。所以模型根本不该收到它们。它没见过的东西,泄不出去。

与其给每个函数手写白名单,不如让模式里的每一列都带一个标签:公开、生产、客户PII。每个函数声明自己的层级,数据层拒绝返回高于该层级的任何内容。真正要紧的规则是:有人新增一列却忘了打标签时怎么办——它会被当作PII,永不返回。忘打标签的结果是一个缺失的数字,而不是一次泄露。

模型说“不”不算控制

智能体会礼貌地告诉用户,电话号码不在范围内。那是用户体验,不是安全。模型拒绝是我要求的行为;数据层永不返回该字段才是保证。就算提示注入说服模型放弃拒绝,它依然什么都拿不到。

那些会泄露的小地方

  • 错误信息:用户看到通用提示,真实错误带着请求ID进服务器日志。
  • 存在性泄露:“我不能给你看Maria Lopez的记录”等于确认她是客户。越界查询返回的错误,和打错字时一模一样。
  • 对话记录与日志:两者都含客户数据,因此套用同样的访问控制,对话记录另加三十天清除。

这套设计没解决什么

它假设数据访问层本身是对的。在测试之前,这只是假设,所以审计阶段包括:以某个下级用户的身份登录,试着通过模型拉取另一个代理人的数据行。它也不覆盖第二版给智能体写入能力之后的情况——那会把上面每个问题重新打开。至于模型提供商自己的留存与训练条款,那是合同审查的事,不是改代码能解决的。

这是一个寿险平台里一个智能体的第一版。我正按同样的方式过完OWASP清单剩下的条目,边做边写。目标是恰好从保险起步的智能体安全,而不是保险安全。