上下文污点追踪。
等到你的智能体要判断该不该相信一句话时,这句话早已丢掉了唯一能一锤定音的那件事——它从哪儿来。上下文窗口是一根扁平的字符串:系统提示词、用户的请求,以及三十秒前抓回来的那个网页,抵达时是同一批毫无分别的令牌;于是所有那些「读一读文本、判断它是不是指令」的防御,都在猜一件本来只要记下来就好的事。污点追踪是另一条路:数据进来的那一刻就给它盖上来源标签,让标签随数据一路同行,然后在工具调用处、而不是在句子上,去检查这个标签。它是记账,不是分类——这正是它的失效方式可以被一条条列举出来、而不是只剩一个你只能估的漏报率的原因。
每个令牌都有来处。几乎没人把它记下来。
从一次出错的工具调用往回走,你会找到一条链:模型之所以发出 send_email(to=…),是因为上下文里有一段话这么说;那段话来自一次工具结果;那次工具结果来自一个别人掌控的 URL。每一跳在发生的当下都是可知的。事后却统统不可知,因为模型看到的那份记录是拼接出来的,而拼接会把来源扔掉。
- 可信,大致上。你的系统提示词、你的工具定义、你自己代码里的固定字符串。那些你愿意拿去执行的内容。
- 半可信。终端用户自己发的消息。它有权在该用户的权限范围内指挥智能体——也仅限于此,而这道区分正是多数技术栈揉作一团的地方。
- 不可信。每一份工具结果。抓回来的页面、检索到的文档、上周被另一个用户写入的一行数据、子智能体的返回值、智能体自己早先跑出的代码输出。它的定义性质不是「怀有敌意」,而是「其内容不由这段对话里的任何人决定」。
第三类比团队预想的要大,而且每加一个集成就又大一圈。它还包括那些感觉上很「内部」的面:一个工单系统、CRM 的备注字段、一行日志、一条日历邀请的标题。这能伸到多远,见把遥测当作不可信输入。
污点是记账。注入检测器是猜测。
这两种做法远看很像,失效的方向却正好相反。检测器——一个分类器、一组正则、或者再叫一个模型来回答「这是不是指令」——读的是内容,产出的是一个概率。它会漏掉自己没见过的措辞,也会把一份恰好带着祈使句的正当文档标红。它的错误率是攻击分布的性质,而攻击分布不归你管。
污点追踪根本不读内容。它记下的是:这根字符串来自 http_get,因此带上标签 untrusted;而且它会让这个标签穿过之后的每一次字符串操作、每一次摘要、每一次变量赋值,直到有什么东西显式地把它清掉为止。没有哪种措辞能击败它,因为措辞压根不是这个决定的输入。
两者都有位置,而次序很重要:污点决定一个不可信的值被允许做什么,检测器决定它看起来有多可疑。把污点用在那道你绝不能丢的管控上,把检测器用在分流与告警上。常犯的错是把检测器放到承重位置上,因为它是演示效果好的那一个——至于底下那个问题为何打不了补丁,见提示词注入。
标签必须在工具调用处检查,因为模型不是一道边界。
接下来是不太舒服的那部分。一旦一根不可信的字符串进了上下文窗口,模型此后生成的一切都在它的下游。模型内部并不存在什么机制,能阻止被污染的输入去影响一份号称干净的输出;指令层级是一种被训练出来的偏好,不是一次权限检查。所以诚实传播的污点会一跳之内蔓延到整个上下文,而一个朴素的实现会立刻宣布一切都不可信,然后拦下所有动作。
有两种结构能让它重新可用:
- 盖住参数,而不是盖住这一轮。真正划算的检查是按参数来的:这个
recipient的值被污染了,而send_email要求收件人未被污染。给一个怀有敌意的页面做摘要无害;让那个页面里的值抵达一次写操作的目的地字段,就不无害了。几乎每一次真实的智能体数据外泄都是这个形状——不可信内容进来,特权写操作出去。 - 把上下文切开,好让污点有地方停住。在第二个不持有任何凭据的上下文里解析那份不可信文档,只让它返回一个窄窄的结构化值;跨回来的只有这个值。这正是子智能体买到的东西,也正是「那件商品是隔离、而不是人设」的原因。
研究上的参照点是 Google DeepMind 的 CaMeL(Defeating Prompt Injections by Design,2025),它跑的正是这套切分:一个有特权的模型仅凭可信请求把计划写成代码,且从不看见数据;一个被隔离的模型解析不可信内容,且没有任何工具权限;一个定制解释器给每一个值挂上能力(capability)元数据,并强制执行数据流规则。在 AgentDojo 基准上,它在带可证明安全保证的前提下完成了 77% 的任务——这是一个真实的数字,也是一个真实的天花板,因为剩下那部分大多是控制流依赖于数据的任务。
它的代价,以及这周就值得动手的那个版本。
完整的那套构造很贵:你必须在看见数据之前就写好计划,而这恰恰排除了那种开放式探索——正是它当初让智能体显得诱人。多数团队不该从那里起步,而该从「先让标签存在」起步,因为今天它通常压根不存在。
- 在边界处盖章。每一份工具结果都带上一个溯源字段——工具、来源、信任级别——由派发器设置,而不是由工具作者设置。在生产流量跑了一年之后再把它补进消息格式,是一次永远不会发生的迁移。
- 把工具分一次类。每个工具要么是读方、要么是写方、要么两者皆是;而每个写方要声明它的哪些参数必须未被污染。这张表通常不到一百行,而它就是全部的执行面。
- 只拒绝那一条序列。当一次特权写操作的参数在同一次运行中可以回溯到不可信输入时,拒绝它,并升级给人来判断,而不是去猜。这一条规则不需要任何分类器,就能拦住那个外泄模式。
- 把标签记在调用旁边。只强制执行、却不记录的污点,会让你在事后无法回答「是哪个来源挪动了这个决定」——和轨迹描述的是同一个缺口。