最近帮某公司搭客服工单Agent的时候,我碰到了一件让整个项目组后背发凉的事:一个只读工单查询Skill在联调阶段跑得好好的,结果日志采集一开,测试同事随手把一个真实客户的手机号和身份证号写进了对话里,日志系统原样落盘,链路追踪里也全量记录了。更麻烦的是,这个Skill会把工单备注拉进上下文再交给大模型做摘要,摘要把备注里的敏感信息又"复述"了一遍。那一刻我意识到,Web开发者转型AI安全,传统的输入校验、输出编码、日志脱敏三板斧,在Agent场景下根本不顶用,必须重新设计一套专门针对Agent Skills的敏感数据脱敏架构。
这篇文章我就按自己的实操过程来讲,从"为什么Web那套防泄漏思路会失效"到"数据到底从哪些口子漏出去",再到我最终落地的一套四层脱敏架构,以及最折磨人的"一致性脱敏"设计。很多细节是踩坑踩出来的,希望能给同样从Web后端转向AI安全的同学一条能直接参考的路径。
1. 为什么Web开发者的传统防泄漏思路在Agent场景下会失效
1.1 Web时代的三板斧,为什么到Agent这里就断了
做Web后端的人对数据防泄漏都有自己的一套惯性打法:入口做参数校验,业务层做权限控制,出口统一封装响应,日志框架里配好脱敏拦截器。这套组合拳在传统请求-响应模型里非常成熟,核心思想是"在数据跨信任边界的地方设卡"。你对接的用户输入再脏,只要在Controller入口和数据库访问层之间把该滤的滤掉、该转义的转义,基本就稳了。
但Agent(智能代理)应用的链路完全不是这个形态。一次对话任务里,用户输入不是直接打到你的业务接口,而是先进入大模型的上下文窗口。大模型通过ReAct循环(推理+行动)自主决定调用哪些Agent Skills,每个Skill拿到的参数可能是模型从用户话里抽取出来的,也可能是上一个工具返回结果里继续加工出来的。数据在这个循环里被反复搬运、改写、拼装,你根本没法用"入口和出口"这种线性模型去框它。
我习惯用一个类比来解释这个差异:Web应用的防泄漏像是给一条河流装闸门,水从源头流向大海,你在关键节点拦一下就行;Agent的数据流更像一锅反复煮沸的汤,料在同一个锅里被舀起来、倒回去、调味、再舀起来,你光在锅边放个滤网是罩不住的。因为敏感数据一旦进了上下文,它就可能以各种变形出现,还可能作为某个工具调用的参数被带出去。
1.2 真正棘手的三类泄漏场景
我在设计和复盘这个架构时,把Agent场景下的敏感数据泄漏归纳成了三类,这三类基本涵盖了绝大多数翻车事故:
第一类是上下文递归污染。用户在一开始输入了手机号,大模型为了保证后续任务连贯,会把这个手机号原文牢牢攥在上下文里。接下来Agent要查订单、查物流、填售后表单,这个手机号就被反复复制到各个Skill的入参里。更隐蔽的是,即使第一层脱敏把手机号替换成了占位符,模型也可能在后边的推理过程中靠语义猜测重新"脑补"出真实值——如果原始值残留在某次工具返回里的话。
第二类是工具间拼接泄漏。一个Skill拿到的字段单独看一点不敏感,比如一个纯数字ID、一个城市名、一个时间戳,但两个Skill的数据拼在一起,就能定位到一个具体的人。这类泄漏最阴险,因为传统Web防泄漏只看单个请求字段的正则规则,根本不会去拼装检测。
第三类是模型"复读机"效应。很多AI安全培训材料都会强调,大模型在生成摘要、回复邮件草稿时,会倾向于原样复述输入里的电话号码、身份证片段,这在客服Agent场景里极其常见。模型不知道自己该不该保留这些信息,它只知道"忠实回答"。所以只在输入侧做脱敏是不够的,输出侧必须再做一次校验。
1.3 从"拦水"到"改造水",思维必须升维
我之前带过的几个从Web转过来的同事,最容易犯的错是照搬OWASP里的输入校验清单。他们会写一堆正则来匹配手机号、身份证,在Agent入口处把匹配到的内容全部打码,然后觉得任务完成了。结果一上线就发现两类新问题:一是脱敏后的占位符被模型当成普通文本来"润色",变成了***,导致下游Skill没法还原;二是某些Skill本身目的就是查真实数据(比如按账号查余额),把真实参数脱敏了,业务根本跑不通。
这个教训让我想明白一件事:Agent场景下的脱敏不能等价于"隐藏",它更像是对数据进行一次"语义安全的翻译"。你在系统内部维护一套可靠的映射关系,让模型在脱敏数据上完成推理,等真正需要执行外部操作时再还原成真实值——这就是后面要展开的一致性脱敏架构的核心思想。Web开发者的价值在于,我们已经很习惯"数据在哪里流动、哪里是信任边界"这套思考方式,缺的只是把线性管道换成循环系统的架构能力。
2. Agent Skills在哪个环节会"漏"数据——先摸清敌情
2.1 一次Skill调用里,数据到底经过哪些位置
动手写脱敏代码之前,我先干了一件事:把Agent的一次完整调用链路的每个环节掰开揉碎,给每个可能出数据的地方编号。很多团队一上来就急着写脱敏函数,结果漏掉了关键出口,属于典型的敌情不明、火力全开。
我以一个典型的"工单详情查询"Skill为例,完整链路大致这么走的:
- 用户输入原始文本,进入Agent编排层,会话上下文记录原始内容。
- 大模型对用户意图做解析,从文本里抽取结构化参数(比如工单号、客户手机号),生成调用Skill的请求。
- Skill执行器收到请求,做参数校验,然后构造对外部业务系统的API调用,这个调用包含URL路径、Query参数、请求头和请求体。
- 外部系统返回业务JSON,Skill执行器解析结果,把关键字段回填到上下文,交给大模型。
- 大模型基于上下文生成回复,回复内容输出到用户界面。
- 全程的调用链日志、Token用量、输入输出快照,被异步采集到日志平台。
这六个环节里,几乎每一个都是敏感数据的出口。但它们的泄漏风险并不一样,对脱敏策略的要求也完全不同。
2.2 六类数据出口的风险等级评估
我把这六个环节按风险做了个分级表,这也是我当时给团队做安全评审时用的底稿:
| 环节位置 | 典型敏感数据 | 泄漏风险 | 脱敏策略复杂度 |
|---|---|---|---|
| 用户原始输入进入上下文 | 手机号、身份证、住址、情感化自述 | 高(源头污染) | 中:先识别再替换为结构化占位符 |
| 模型抽取出的Skill入参 | 账号、手机号、姓名 | 极高(参数直接打到外部系统) | 高:必须保证同值同占位符且必要时可还原 |
| 外部系统API请求报文 | 真实账号、鉴权头、业务字段 | 极高(跨系统泄漏) | 高:按接口契约区分必真字段与可脱敏字段 |
| 外部系统返回的业务JSON | 数据库原始字段、关联用户信息 | 极高(模型会二次引用) | 高:需要递归遍历JSON做字段级脱敏 |
| 模型生成的回复内容 | 模型可能复述敏感原文 | 中(面向用户可见) | 低:输出侧做同规则二次匹配 |
| 日志与链路追踪快照 | 全部入参、返回body、中间态 | 极高(运维可视) | 中:必须延迟到日志侧,顺序不能错 |
这个表里最关键的一行是第三行"外部系统API请求报文"。因为很多业务场景下,Agent必须用真实数据去调接口(比如查余额时账号必须是真实账号),这就意味着我们不可能在源头把所有数据都打码。这里要引入一个新的概念:脱敏不是全有或全无,而是要区分"模型可见的上下文"和"外部系统可执行的真实参数"。
2.3 每个出口需要什么强度的脱敏
根据出口的用途,我把脱敏强度分成三档,这是整个架构设计的基础:
第一档是不可逆脱敏强度,用于日志、调试快照、数据分析场景。这些数据一旦离开系统就不该再有还原能力,直接用单向散列或者随机化替换,配上盐值防止碰撞猜测。
第二档是条件可逆脱敏强度,用于模型上下文与Skill参数回填。系统内部维护映射表,模型在整个推理周期内看到的都是占位符,占位符之间保持一致性。只有当某个Skill真正需要执行外部操作时,执行器才通过映射表把必要字段还原成真实值。
第三档是仅展示脱敏强度,用于模型最终回复。这层要注意的是,模型可能在回答里复述原始敏感数据,也可能复述占位符,需要有一套相对宽容的匹配规则,把两种形态都识别出来再打码,同时不能影响回复的正常阅读。
这三个强度必须分开落地,混在一起就会出现"为了日志安全把业务接口也搞挂了"或者"为了业务可用让日志裸奔"的问题。后面章节的四层架构,其实就是把这三种强度用可配置的方式分布到链路不同位置上去了。
3. 分层脱敏架构:从输入清洗到日志收口的四道防线
3.1 第一道防线:输入侧预处理,先分类再替换
整个架构的第一层在Agent编排器的入口,也就是用户消息进入上下文之前。这一层要完成的最核心任务是"识别并分类敏感字段"。识别用三种手段叠加:正则表达式匹配常见格式(手机号、身份证、银行卡、邮箱)、开源PII识别框架做命名实体识别、自定义词典匹配内部业务标识符(比如工号前缀+数字)。
我提供的思路是用一个统一的预处理管道,每个字段先做类型判断,再按类型选择替换策略。这里给出一个核心的Python实现片段,重点不在具体包名,而在于"识别-分类-替换-映射"这条链路你应该自己掌控:
import re import hashlib from typing import Dict, Tuple # 注意这里只演示核心逻辑,实际要引入PII识别框架与自定义词典做增强 PHONE_RE = re.compile(r'(?<!\d)1[3-9]\d{9}(?!\d)') IDCARD_RE = re.compile(r'[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]') EMAIL_RE = re.compile(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}') class SensitiveMasker: def __init__(self, salt: str): self.salt = salt self.mapping: Dict[str, str] = {} self.counter = 0 def _tokenize(self, matched: str, field_type: str) -> str: # 同一敏感值在本次会话中映射到同一个占位符,保证一致性 if matched in self.mapping: return self.mapping[matched] self.counter += 1 raw_digest = hashlib.sha256( (self.salt + matched + field_type).encode("utf-8") ).hexdigest()[:10] token = f"<{field_type.upper()}_{raw_digest}>" self.mapping[matched] = token return token def mask_text(self, text: str) -> Tuple[str, Dict[str, str]]: # 按优先级替换:手机号 > 邮箱 > 身份证,避免子串冲突 masked = EMAIL_RE.sub(lambda m: self._tokenize(m.group(), "EMAIL"), text) masked = PHONE_RE.sub(lambda m: self._tokenize(m.group(), "PHONE"), masked) masked = IDCARD_RE.sub(lambda m: self._tokenize(m.group(), "IDCARD"), masked) return masked, self.mapping这段代码里隐藏着一个非常容易踩坑的细节:正则的替换顺序。身份证正则和手机号正则如果互换顺序,当文本里同时出现一串相似的连续数字时,先匹配的手机号逻辑可能吞掉身份证片段。所以我在实现里先做邮箱(前缀最明确),再做手机号,最后做身份证。实际项目里你还会遇到地址、地名、发票号这类模糊实体,必须靠词典和模型识别兜底,不能只信正则。
3.2 第二道防线:工具调用侧,参数级别的最小化脱敏
如果说入口侧是"大扫除",工具调用侧就是"精准打击"。因为当大模型已经把参数抽取好,准备调用Skill时,这些参数是直接要去拼接外部请求的。这里我做了两件事:
第一件事是参数级二次校验。对模型抽取出来的结构化参数,逐个跑一遍同样的识别规则。为什么要二次校验?实战中发现,大模型在路由到Skill时可能把上下文中残留的原始手机号直接塞进参数里,即便入口侧已经替换过。一次典型的翻车是:入口侧明明把138xxxx0001换成了<PHONE_1>,但模型在Function Call参数里写的是从系统提示词里翻出来的另一段原始文本。这种"模型自我考古"的行为防不胜防,只能在参数出口再堵一次。
第二件事是必真字段与可脱敏字段的区分。我维护了一份Skill接入清单,每个Skill声明哪些字段必须真实传输、哪些可以脱敏。比如"查询余额"这个Skill,账号字段必须真实;"生成客户日报"这个Skill,客户姓名完全可以用占位符替代。这份清单相当于传统Web开发里的接口白名单,只是对象从URL变成了Agent Skill的入参声明。
3.3 第三道防线:输出侧检查,防止模型"复读机"
模型生成回复之后,很多人以为万事大吉,直接就把文本推给前端了。我在实际测试中发现,模型在复述用户原话时会带入敏感内容,尤其是当用户问"我刚才报的手机号是多少"这种问题时,模型会忠诚地从上下文里取出原文。
第三层做的事就是在模型输出原文推到用户面前之前,跑一遍和入口侧完全相同的匹配规则。注意,一定要用同一套占位符表,否则会出现一种很滑稽的情况:模型原文里的<PHONE_1>被输出侧正则当成一种无意义标签而忽略,用户就直接看到了带占位符的回复,体验很差。正确的做法是:先把已存在的占位符还原成"展示用掩码"格式,再把识别到的原始敏感值按同一规则变成掩码。
def mask_output(text: str, mapping: Dict[str, str]) -> str: # 先处理占位符,再处理遗漏的原始值 for raw_value, token in mapping.items(): text = text.replace(token, mask_display(raw_value)) text = PHONE_RE.sub(lambda m: mask_display(m.group()), text) text = EMAIL_RE.sub(lambda m: mask_display(m.group()), text) return text def mask_display(value: str) -> str: if len(value) <= 3: return "*" * len(value) return value[:3] + "*" * (len(value) - 3)这层还有一个容易被忽略的细节:模型可能不是原样复述,而是换了一种格式,比如把138xxxx0001报成138 xxxx 0001,或者把身份证号分段念出来。输出侧检查需要加一层轻量的归一化逻辑,把常见分隔符(空格、横线、换行)先去掉再做匹配,否则等于漏防。
3.4 第四道防线:日志与链路追踪脱敏,顺序错了全白搭
日志这层是整个架构里最让我后怕的环节。很多Agent框架默认会做全链路Token统计,把每次请求的输入输出快照都记下来,这些快照里包含了最原始的上下文。如果你在日志采集中转层之前没做脱敏,那前面三道防线全白做了,因为运维、研发、数据分析师全都能在日志平台里看到原始敏感数据。
第四层的关键不是"怎么脱敏",而是"在哪里脱敏"。有一个血泪教训是:我在接入日志时让日志采集器直接监听消息总线,而消息总线上的消息体在入口脱敏层之前就已经发布了。结果就是入口侧脱敏做得再漂亮,日志里还是有原文。
正确的架构顺序是:消息体只有在经过入口脱敏、参数校验、输出校验这三层处理之后,才允许被发布到日志总线。并且日志侧的脱敏策略必须使用最高强度的不可逆散列,不能复用条件可逆的那张映射表——日志系统里面一旦存在可以还原的映射关系,就等于给数据泄漏留了一扇后门。按我的落地经验,这一步在架构评审时会被安全团队反复拷问,你最好提前想清楚回答。
4. 一致性脱敏:让Agent在大模型和业务系统之间"对得上话"
4.1 为什么不能一次性随机替换
如果只是写一个打码函数,这篇内容早该结束了。真正让我花了最多时间的是"一致性"这三个字。
假设用户说完手机号之后,Agent需要连续调用两个Skill:第一个Skill用手机号查用户ID,第二个Skill用用户ID查订单列表。如果脱敏模块对同一个手机号每次生成不一样的随机掩码,那第一个Skill在映射表里记录的是<PHONE_A>,第二个Skill拿到的是<PHONE_B>,两边根本对不上,业务直接断链。
你会说,那把映射表共享不就行了?对,但共享映射表也会出问题:如果两个不同的原始值被映射到了同一个占位符,模型就分不清谁是谁,推理结果全乱套。所以一致性脱敏的第一原则是:同一个原始值,在整个会话生命周期内,必须且只能映射到同一个占位符;不同原始值之间,占位符碰撞概率必须降到零。
4.2 三种一致性映射方案怎么选
我在设计时对比了三种方案,各有适用场景:
第一种是单次会话内存映射表。适合短对话、Skills之间依赖较强的场景。优点是实现简单、查询快;缺点是Agent重启或对话上下文切换后映射就丢了,无法支撑跨会话场景,比如用户第二天回来继续追问昨天的上下文。
第二种是HMAC散列确定性映射。用HMAC-SHA256(盐 + 原始值)生成定长Token,天然满足"同值同Token、不可逆、无碰撞隐患"。这是我推荐的方案。它的好处是不需要维护分布式映射存储,任何脱敏节点只要拿到同一个盐,就能算出和入口侧一致的Token;缺点是Token是定长的,不可读,且无法还原。
第三种是加密可逆映射,本质是给敏感值做字段级加密,密钥由密钥管理服务统一托管。适用于必须还原真实值的场景,比如某个Agent内部需要真实调用支付接口。但加密值很长也很丑,不适合直接作为模型上下文的占位符,所以通常的做法是加密值和短Token同时存在:模型上下文里放短Token,执行器真正调API时才通过Token查短Token到密文的映射,再解出真实值。
我最终在项目里采用的是"HMAC散列为主、加密映射为辅"的混合方案。模型上下文和日志里是HMAC Token,既保持一致性又不可逆;只有极少数必须执行真实操作的Skill,才走一张独立配置的Token-Vault映射表做临时解密。
4.3 占位符格式不能随便定,模型要看得懂
脱敏后的占位符不仅仅是"给系统内部看的Ticket",它本身会进入大模型的输入,模型要能从占位符中推断出字段语义。我用的是<类型_哈希>格式,比如<PHONE_abc123def4>。这种做法有两个好处:
一个是语义清晰。模型看到<PHONE_xxx>,知道这段内容代表"一个被脱敏的手机号占位符",在生成回答时不会把它当成乱码去修改。
另一个是便于追踪。哈希前缀可以和映射表里的记录做关联,调试时能快速定位到具体脱敏链路。不建议直接用***这种纯星号,因为它没有任何语义,模型在后续推理中大概率会补全或改写,造成下游解析失败。
这里给出HMAC方案的实现片段,方便你直接抄:
import hmac import hashlib SALT = b"your-project-salt-change-in-prod" def build_token(field_type: str, raw_value: str) -> str: msg = f"{field_type}:{raw_value}".encode("utf-8") digest = hmac.new(SALT, msg, hashlib.sha256).hexdigest()[:12] return f"<{field_type.upper()}_{digest}>"这个函数是纯函数,不依赖任何状态,所以无论部署多少个Skill执行节点,任何一个节点都能对同一个手机号产生同一个Token。这也是我强烈建议用HMAC而不是用随机序列的原因——分布式环境下,共享映射表的维护成本会高到你怀疑人生。
4.4 还原的边界:什么时候必须解密,什么时候绝不能解密
最后再聊聊一致性脱敏里最敏感的还原边界问题。脱敏是手段,还原是能力,能力意味着责任。我把还原分成了两类:
必须还原的场景:某个Skill对外发起真实请求之前,执行器通过Skill清单上的"真实字段白名单",把HMAC Token临时映射回真实值,用真实值构造请求。这里的关键词是"临时",也就是说解密动作必须在内存里瞬间完成,不允许落盘、不允许进日志、不允许进链路追踪。
绝不能还原的场景:日志系统、审计系统、调试快照。我在架构里强制做了一个隔离:日志链路拿到的Token直接在HMac层就截断了还原能力,Token-Vault服务不向日志系统开放任何读取接口。换句话说,一个有日志平台权限的开发人员,即便看到了所有Token,也无法反查出对应的真实手机号。
这个边界在答辩时一定要讲透。很多方案一上来就是"全量可逆加密",看起来很安全,实际上等于把保险柜钥匙挂在保险柜旁边。真正可靠的架构必须按场景收窄还原能力,宁可牺牲一点点调试便利,也要确保敏感数据只在真正需要的地方存在真实形态。
5. 从Web安全迁移过来的实操清单与踩坑记录
5.1 能直接迁移的经验:信任边界、数据分级与最小权限
转型AI安全并不意味着Web安全经验作废。恰恰相反,我觉得Web后端出身的人做Agent安全有天然优势,因为我们早就习惯了"信任边界"的思考方式。在Web里,浏览器是不可信端,API网关是边界;在Agent里,大模型是一个"能力很强但不可信的执行者",Skill边界、上下文边界、日志边界都需要重新画。
数据分类分级可以直接照搬。我给项目组定的规范很简单:凡是能唯一定位到自然人的字段全部视为L3级,包括手机号、身份证、银行卡、详细住址;凡是能缩小范围但无法单独定位的字段视为L2级,比如工单号、设备型号、城市名;其余是L1级。脱敏策略的强度完全由分级决定,L3级在日志里强制不可逆散列,L2级在上下文中允许结构化泛化,L1级正常透传。
最小权限原则也直接能用。每个Skill接入时声明自己的数据需求,能传ID的绝不传手机号,能传手机号的绝不传身份证。我在一次架构评审里就抓出过一个反面案例:某开发为了方便,把客户完整通信档案整体塞进了Skill入参,只是为了生成一句"客户反馈已收到"的自动回复。这种大而全的传输习惯在Agent场景里是灾难,因为模型可能为了一个简单任务在内部反复检索所有字段。
5.2 必须重新学的:模型行为不可控,脱敏链路的顺序就是生命线
转型过程中最需要重新建立的一个认知是:大模型的行为不是确定性的。传统Web安全里,一个过滤器只要写好正则和逻辑,它的输出是可预测的;但Agent里,同一个输入,模型可能这次正常抽取参数,下次就给你整出个意外格式。这意味着脱敏链路必须要能容忍模型的"自由发挥"。
我在测试中就踩过一个典型坑:入口侧把手机号换成了<PHONE_a1b2c3d4e5>,但模型在做Function Call参数填充时,把占位符直接当作普通文本传给了Skill,Skill没做还原就去查数据库,查了个寂寞。后来我改了策略,Skill执行器在调用外部系统之前,只要发现参数是Token格式,就先去Token-Vault做一次强制还原。这样模型传错格式不要紧,执行器兜底修正,链路依然能跑通。
另一个要重新学的点是"脱敏链路的顺序就是生命线"。我前面说过日志采集器的顺序问题,这里再补一个:输入侧脱敏和输出侧脱敏必须共享同一套规则和映射表,但两者的执行时机绝对不能耦合。有人在入口脱敏成功后,顺手把映射表清空了,结果模型输出侧想还原占位符时找不到映射,直接给用户展示了<PHONE_xxx>这种鬼东西。这种低级错误藏得很深,一个简单的并发请求就能触发,建议在集成测试里专门覆盖"入口脱敏->长对话->输出展示"的完整流程。
5.3 实测中反复踩过的四个坑
具体踩坑的记录我整理成了一张清单,每条都是我或队友用线上教训换来的:
| 坑 | 根因 | 最终处理方式 |
|---|---|---|
| 正则把"京州路10号"中的"10"关联成了手机号片段 | 正则边界没写严谨,缺少对完整连续号码的要求 | 引入词边界断言,并用PII识别框架做二次确认 |
模型把<PHONE_a1b2>改写成<PHONE 123> | 模型对占位符格式做了"润色" | 强制在系统提示词里声明占位符是不可修改的保留字段 |
| 外部API返回的JSON嵌套深层仍有原始手机号 | 只对顶层字段做了脱敏,递归遍历遗漏 | 输出侧改用递归JSON遍历,逐层匹配脱敏 |
| 日志平台能看到脱敏前快照 | 日志采集器接的是脱敏前的消息总线 | 把日志采集点移到所有脱敏层之后,并加广播开关 |
第一条和第三条是纯技术细节,第二和第四条则涉及Agent链路的架构顺序。细看你会发现,这四个坑如果只靠写代码时的注意,根本防不住,必须靠端到端的自检清单在发布前逐项过一遍。我后来把这套清单固化成了自动化集成测试的一部分,每次Agent有新的Skill上线,就跑一遍全链路泄漏扫描。
5.4 分级上线的建议与灰度策略
把这个架构第一次落地到真实业务时,我不建议直接铺到所有Agent Skills上。合理的做法是分级灰度:先挑一个只读类、无真实写操作、敏感数据密度低的Skill试点,跑通全链路脱敏和日志隔离之后,再逐步扩展到写操作型Skill。写操作型Skill的脱敏还原链路要比只读型复杂得多,因为还原时效性要求高,还需要考虑幂等和审计。
我当时的试点选择是工单查询Skill,因为它的特征是"参数单一、返回字段固定、不触发外部写操作"。试点期间单独开一台Agent实例,出口流量标注灰度标签,日志单独脱敏入仓。两周跑下来,专门盯三类指标:脱敏耗时的P99、占位符还原失败率、日志扫描的敏感值命中率。这个阶段能暴露至少一半的架构问题。
等到写操作型Skill接入时,还有一个建议:所有真实值还原动作必须加审计事件。因为一旦Skill执行器具备还原能力,就意味着内部人员理论上可以通过伪造Token请求还原出真实数据,所以每一次还原都要记录操作者身份、Skill名称、时间戳和还原字段类型。这段审计日志本身不允许存储真实值,只存"谁在什么时候对什么Token做过还原请求"。
最后再说几句掏心窝的话
如果你也是从Web后端转AI安全,我最大的建议是:别把AI安全想得太玄,它只是把信任边界换了个更复杂的形状。Web时代你防的是用户输入到服务器之间这条线,AI时代你要防的是用户在对话、模型在推理、工具在执行这三方之间不断循环的那张网。你现在掌握的输入校验、权限最小化、数据分级、日志隔离这些基本功,到了Agent场景全部用得上,只是需要给它们换一套编排载体。
我自己踩过的最重要的一课是:脱敏方案在架构图里再完美,也不如把它跑在一个真实Skill上,然后把日志打开,拿真实数据的影子样本扫一遍。你会发现那些预想不到的泄漏出口,往往不是模型太聪明,而是你的链路顺序和边界划分在某一个节点上偷了懒。早点跑通一条完整链路,比憋一个大而全的安全平台有意义得多。