Agent 工程体系 · 第 78/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。

Agent 数据隐私:最小采集、脱敏、保留、跨境、导出和删除

Agent 的隐私问题,不只是“模型会不会泄露聊天内容”。一个可调用工具、读取知识库、保存记忆、生成审计日志的 Agent,实际上会让同一份数据经过多个系统:

flowchart LR
    U[用户或业务主体] --> G[接入网关]
    G --> P[隐私策略与数据分类]
    P --> M[会话编排器]
    M --> D1[模型服务]
    M --> D2[工具服务]
    M --> D3[记忆存储]
    M --> D4[RAG与向量库]
    M --> L[审计日志]
    D1 --> O[输出过滤]
    D2 --> O
    D3 --> O
    D4 --> O
    O --> U

    X[导出/删除请求] --> I[身份核验]
    I --> T[数据资产索引]
    T --> Q[删除任务队列]
    Q --> D3
    Q --> D4
    Q --> L
    Q --> B[备份与副本]
    Q --> E[删除证据]

隐私保护的目标不是让系统“完全不接触数据”,而是让每一次接触都满足四个条件:

  1. 有明确目的
  2. 只处理完成该目的所需的数据
  3. 数据只在必要的范围和时间内存在
  4. 能够解释数据去了哪里,并能够导出或删除

这也是为什么 Agent 隐私必须覆盖采集、推理、工具调用、记忆、检索、日志、备份和供应商处理,而不能只配置一个“不要训练用户数据”的模型选项。OWASP 将个人信息、财务信息、健康记录、商业机密、凭据和法律文书等都列为可能导致敏感信息披露的对象,并指出系统提示词中的限制可能被提示注入绕过。(genai.owasp.org)


一、先确定隐私边界:什么是“数据”,谁是“主体”

1. 数据不是只有用户输入

在 Agent 系统中,至少存在以下数据类型:

数据类型 示例 常见存储位置
原始输入 用户问题、上传文档、语音转写 API 网关、对象存储
推理上下文 system prompt、历史消息、检索片段 编排器、模型供应商
工具数据 CRM 记录、订单、邮件、数据库结果 工具服务、缓存
记忆 用户偏好、长期事实、摘要 记忆库、向量库
派生数据 embedding、分类标签、风险评分 向量库、特征库
输出数据 回复、生成的文件、工具执行结果 会话库、文件存储
运维数据 IP、设备标识、trace、错误堆栈 日志平台、监控系统
审计数据 身份、指令、工具、决策、审批记录 审计库、不可变存储
备份数据 数据库快照、日志归档、磁带副本 备份系统、灾备区域

**派生数据仍然是隐私资产。**例如,删除了原始文本,却保留了可以区分用户兴趣、疾病、财务状况的 embedding,不能简单地说“原文已经删除,所以隐私风险消失”。

向量和 embedding 的生成、保存、检索过程都可能引入泄露、跨租户访问和 embedding inversion 风险。OWASP 明确要求向量存储具备细粒度权限控制和逻辑隔离,并建议记录检索活动。(genai.owasp.org)

2. 数据主体不一定等于登录用户

一次请求可能涉及多个主体:

员工 A 请求总结客户 B 的邮件
客户 B 是邮件内容中的数据主体
员工 A 是操作发起者
企业 C 是业务控制者或数据管理方
模型供应商 D 是外部处理方

因此,删除请求不能只用 user_id 过滤。至少还要考虑:

  • 谁发起了请求;
  • 请求针对哪个主体;
  • 该主体是否拥有数据控制权;
  • 数据是否包含第三方信息;
  • 是否存在保留义务、争议、审计或安全调查;
  • 是否已经进入模型训练、统计聚合或不可逆匿名化流程。

“用户删除自己的账户”与“删除所有与该用户有关的数据”不是同一个操作。前者是账户生命周期事件,后者是数据主体范围判定。


二、最小采集:不是少存字段,而是让数据集合服从目的

1. 最小采集的形式化定义

设:

  • PP 是业务目的,例如“回答订单状态”;
  • DD 是系统可采集的数据集合;
  • DPD_P 是完成目的 PP 所需的最小数据集合;
  • R(D)R(D) 是使用数据集合 DD 的隐私风险;
  • U(D)U(D) 是数据对任务质量的贡献。

理想的数据选择可以写成:

D=argmaxDDavailable(U(D)λR(D))D^* = \arg\max_{D \subseteq D_{\text{available}}} \left(U(D) - \lambda R(D)\right)

约束条件是:

U(D)UminU(D) \geq U_{\min}

其中:

  • DD^* 是实际允许进入流程的数据;
  • UminU_{\min} 是业务可接受的最低效果;
  • λ\lambda 表示组织对隐私风险的敏感程度。

直觉是:不能为了追求“模型可能更聪明”而无限扩大数据范围。只要删除某字段不会使业务目的失败,就应当不采集,或者在进入模型前移除。

2. 推导一个完整例子

目标:客服 Agent 回答“我的订单什么时候发货”。

原始请求可能是:

{
  "user_id": "u-1001",
  "name": "张三",
  "phone": "13800138000",
  "address": "浙江省杭州市……",
  "order_id": "o-9001",
  "order_items": ["机械键盘"],
  "message": "我的订单什么时候发货?"
}

逐步判断字段:

  1. order_id 用于查询订单状态,属于必要字段;
  2. message 用于确认用户意图,属于必要字段;
  3. user_id 可能用于授权检查,但不一定需要交给模型;
  4. name 对“发货时间”没有贡献;
  5. phone 对查询订单状态没有贡献;
  6. address 对查询订单状态没有贡献;
  7. order_items 只有在回答商品相关问题时才需要。

因此,模型实际接收的数据可以是:

{
  "request_context": {
    "authorized_order_id": "o-9001"
  },
  "message": "我的订单什么时候发货?"
}

而数据库查询可以由受控工具完成:

SELECT order_id, shipping_status, estimated_ship_at
FROM orders
WHERE order_id = :order_id
  AND owner_user_id = :authenticated_user_id;

模型看到的是工具返回的最小结果:

{
  "order_id": "o-9001",
  "shipping_status": "已出库",
  "estimated_ship_at": "2026-09-03"
}

这里有一个重要边界:**最小采集不等于让模型直接查询数据库。**身份验证和数据授权应在工具服务中执行,而不是让模型自行决定用户是否有权限。OWASP 将过度代理能力归因于过多功能、过多权限和过高自治,并建议在下游系统实施完整授权,而不是依赖 LLM 判断是否允许操作。(genai.owasp.org)

3. 采集前应建立“字段—目的—去向”表

一条数据进入系统前,应至少能回答:

字段 目的 是否进模型 是否进记忆 是否进日志 保留期限
order_id 查询订单 脱敏后 允许记录引用 30 天
phone 身份核验 仅记录哈希 认证会话期间
message 理解问题 仅用户同意后 默认摘要 会话结束后 7 天
shipping_status 生成回复 允许记录枚举值 30 天
trace_id 故障定位 90 天

如果一个字段没有明确的目的、接收方和保留期限,它就不应默认进入全链路。


三、脱敏:改变数据的可识别性,不是简单替换几个字符

1. 脱敏的四种结果

脱敏是对原始数据进行变换,使接收方无法获得不必要的敏感信息。常见方式不同,安全性质也不同。

删除或截断

13800138000  ->  138****8000
浙江省杭州市西湖区某路  ->  浙江省杭州市

适合展示和日志,但不适合需要精确匹配的业务。

掩码

掩码保留部分结构:

身份证号:3301**********1234
订单号:o-9***

掩码不是匿名化。若其他字段能够缩小候选范围,掩码后的信息仍然可能识别个人。

令牌化

把敏感值替换为随机令牌:

张三       ->  <PERSON_1>
13800138000 -> <PHONE_1>

映射关系保存在独立的令牌保险库中:

token       original_value       purpose       expires_at
----------  -------------------  ------------  -------------------
<PHONE_1>   13800138000          order_query   2026-09-01T10:30:00Z

模型只处理令牌。只有确实需要发送短信的工具,才在受控边界内解析令牌。

哈希或加密摘要

phone_hash = HMAC-SHA256(tenant_secret, phone)

带密钥的 HMAC 适合做精确匹配或去重。普通 SHA-256 对低熵字段并不安全,因为攻击者可以枚举电话号码、邮箱和身份证号进行字典攻击。

**加密不是脱敏。**加密数据只要密钥仍然可用,通常仍可恢复原文;它解决的是机密性,不自动解决最小化、用途限制或删除问题。

2. 脱敏必须在正确的边界发生

错误流程:

用户输入
  -> 原文写入日志
  -> 原文写入会话库
  -> 原文发送给模型
  -> 最后才在前端掩码

前端掩码只能改变用户看到的内容,不能阻止后端、模型供应商、日志系统或备份系统获得原文。

更合理的流程是:

接收请求
  -> 识别字段和敏感类型
  -> 授权与目的检查
  -> 令牌化/删除
  -> 记录最小审计摘要
  -> 调用模型或工具
  -> 输出检查
  -> 按不同保留策略写入不同存储

3. 一个可运行的 Python 令牌化示例

下面示例使用内存字典演示机制。生产环境应将令牌保险库替换为独立服务,并使用 KMS 管理加密密钥、限制解令牌权限和记录访问审计。

import re
import uuid
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone

PHONE_RE = re.compile(r"(?<!\d)1[3-9]\d{9}(?!\d)")

@dataclass
class TokenRecord:
    value: str
    purpose: str
    expires_at: datetime

class TokenVault:
    def __init__(self):
        self._records = {}

    def put(self, value: str, purpose: str, ttl_seconds: int = 1800) -> str:
        token = f"<PHONE_{uuid.uuid4().hex[:10]}>"
        self._records[token] = TokenRecord(
            value=value,
            purpose=purpose,
            expires_at=datetime.now(timezone.utc)
                      + timedelta(seconds=ttl_seconds)
        )
        return token

    def resolve(self, token: str, purpose: str) -> str:
        record = self._records[token]

        if record.purpose != purpose:
            raise PermissionError("token purpose mismatch")

        if record.expires_at <= datetime.now(timezone.utc):
            raise PermissionError("token expired")

        return record.value

def sanitize_message(message: str, vault: TokenVault) -> str:
    def replace(match):
        return vault.put(match.group(0), purpose="order_query")

    return PHONE_RE.sub(replace, message)

vault = TokenVault()
raw = "请查询 13800138000 的订单状态"
safe = sanitize_message(raw, vault)

print(safe)

预期输出类似:

请查询 <PHONE_7f3a12c9ab> 的订单状态

这个实现演示了三个关键点:

  1. 模型输入中不出现原始电话号码;
  2. 令牌绑定用途 order_query
  3. 令牌有过期时间。

但它仍然有边界:

  • 正则无法覆盖所有姓名、地址、证件和自由文本实体;
  • 令牌本身可能成为关联标识,不能无限期保存;
  • 如果模型输出令牌,输出层必须决定是否允许还原;
  • 如果日志记录了令牌保险库内容,脱敏仍然失败;
  • 如果同一个令牌跨不同租户复用,可能形成跨租户关联。

四、记忆、RAG 和 embedding:脱敏不能停在聊天输入

Agent 记忆通常分为三类:

  1. 短期记忆:当前会话上下文;
  2. 长期记忆:用户偏好、稳定事实、历史摘要;
  3. 外部知识:企业文档、数据库记录、工单、邮件和文件。

三者的隐私策略不同。

1. 不是所有对话都应写入长期记忆

例如:

用户:我这周在看胃病,帮我找附近医院。

如果系统把它自动总结为:

用户长期偏好:患有胃病,关注消化科医院。

就发生了语义升级:一次临时问题被转换成长期健康画像。

长期记忆写入至少应经过:

候选事实
  -> 判断是否稳定
  -> 判断是否必要
  -> 判断是否敏感
  -> 检查用户同意和用途
  -> 写入结构化记忆

比自由文本更容易治理的记忆结构是:

{
  "subject_id": "u-1001",
  "memory_type": "response_preference",
  "value": "prefer_concise_answers",
  "source_event_id": "evt-20260901-001",
  "purpose": "personalize_response",
  "consent_id": "consent-42",
  "expires_at": "2026-12-01T00:00:00Z"
}

这里保存的是偏好,而不是整段原始对话;保存了来源事件,因此后续可以追溯和删除。

2. embedding 不能当作匿名副本

下面这种设计存在明显问题:

原始文档 -> embedding -> 向量库
原始文档删除

删除原文后,embedding 仍可能:

  • 被相似度检索返回;
  • 通过元数据暴露文档范围;
  • 在多租户过滤错误时泄露给其他用户;
  • 被尝试反演出部分源信息;
  • 在备份和索引副本中继续存在。

正确的删除对象应是一个数据资产集合

subject_id
  -> 原始消息
  -> 摘要记忆
  -> 文档分块
  -> embedding
  -> 检索缓存
  -> 生成文件
  -> 训练/评测数据副本
  -> 审计引用
  -> 备份索引

向量库的检索条件必须同时包含租户、主体和授权域:

SELECT chunk_id, content
FROM document_chunks
WHERE tenant_id = :tenant_id
  AND subject_scope @> :authorized_scopes
  AND deleted_at IS NULL
ORDER BY embedding <=> :query_embedding
LIMIT 5;

实际数据库语法会随 PostgreSQL 扩展或向量数据库产品而变化;这里重要的不是具体操作符,而是权限过滤必须参与检索条件,而不是检索后再过滤。否则未授权数据可能已经进入模型上下文。


五、保留:每条数据都要有终止条件

1. 保留期不是“数据库永不过期”

保留期是数据从产生到应当删除、匿名化或转入受控归档的时间范围。它必须绑定具体目的:

会话原文:用于提供当前服务,保留 7 天
故障日志:用于排查服务问题,保留 30 天
安全审计:用于追踪高风险操作,保留 180 天
发票记录:依业务和适用规则确定
备份副本:随备份轮换周期清除

“为了以后可能有用”不是充分的保留目的。

一个保留策略可以表达为:

data_class: conversation_content
purpose: provide_service
retention:
  active: 7d
  archive: 0d
  backup: backup_cycle
deletion:
  mode: hard_delete
  propagate_to:
    - primary_db
    - cache
    - vector_store
    - object_store
    - analytics
    - backup

2. 用状态机处理保留和删除

stateDiagram-v2
    [*] --> ACTIVE: 写入
    ACTIVE --> EXPIRED: 到达保留期
    ACTIVE --> DELETE_REQUESTED: 收到删除请求
    DELETE_REQUESTED --> DELETING: 身份核验通过
    DELETE_REQUESTED --> REJECTED: 无权/范围不明
    DELETING --> DELETED: 全部目标完成
    DELETING --> PARTIAL_FAILURE: 某副本失败
    PARTIAL_FAILURE --> DELETING: 重试或人工处理
    ACTIVE --> LEGAL_HOLD: 合法保留条件成立
    LEGAL_HOLD --> ACTIVE: 保留条件解除
    LEGAL_HOLD --> DELETING: 解除后执行删除

关键是不要把 DELETE FROM conversations 当成删除完成。删除完成应满足:

Deleted=sSin-scopeDeletionConfirmed(s)\text{Deleted} = \bigwedge_{s \in S_{\text{in-scope}}} \text{DeletionConfirmed}(s)

其中 Sin-scopeS_{\text{in-scope}} 是受影响的存储系统集合。只要主库已删、缓存未删,或者原文已删、向量未删,整体状态就不是 DELETED,而是 PARTIAL_FAILURE

3. 软删除和硬删除的区别

软删除通常是设置:

deleted_at = CURRENT_TIMESTAMP

优点是可恢复、事务成本低;缺点是:

  • 查询错误可能重新返回数据;
  • 搜索索引仍可能命中;
  • 备份中仍保留原文;
  • 数据库管理员仍可读取。

硬删除是物理移除记录及相关对象。它更接近真正删除,但需要处理:

  • 级联引用;
  • 异步索引;
  • 缓存;
  • 对象存储版本;
  • 消息队列中的待处理任务;
  • 备份和灾备副本。

生产系统通常使用“软删除隔离 + 异步硬删除”的组合,但必须定义最终完成条件,而不是把软删除状态永久保留。


六、跨境:首先是数据流路由问题,其次才是加密问题

跨境处理是指数据从一个司法辖区、区域或受约束的数据域传输到另一个区域,或者被位于另一地区的供应商、人员、模型服务和运维系统访问。

以下情况都可能属于跨域风险,需要在组织规则和适用法律下判定:

  • 国内服务调用境外模型 API;
  • 境外供应商在境外保存请求和响应;
  • 境外客服或工程师访问日志;
  • 备份复制到另一地区;
  • 监控平台收集原始请求;
  • 模型供应商进行跨地区故障排查;
  • 通过第三方 SaaS 处理包含个人信息的附件。

TLS 保护传输,不等于没有跨境。
数据库加密,不等于没有跨境。
脱敏,也不等于天然不受跨境规则约束。

跨境网关应在调用前做数据域判断:

ALLOWED_REGIONS = {
    "cn-mainland": {"cn-mainland"},
    "global-public": {"cn-mainland", "us", "eu"},
}

def can_route(data_class: str, source_region: str, target_region: str) -> bool:
    return target_region in ALLOWED_REGIONS[data_class]

实际系统中不能只用静态集合,还应检查:

  • 数据分类;
  • 主体所在区域;
  • 业务租户策略;
  • 供应商处理位置;
  • 是否允许供应商保留;
  • 是否允许用于训练或改进服务;
  • 是否支持导出和删除;
  • 备份区域;
  • 运维和技术支持访问区域;
  • 合同和组织审批状态。

更稳妥的架构是:

原始敏感数据
  -> 区域内工具服务处理
  -> 返回最小化、令牌化结果
  -> 区域内或获准区域的模型推理

例如,订单 Agent 不需要把姓名、手机号和完整地址发送给模型。区域内授权服务先完成身份校验和订单查询,模型只接收:

{
  "shipping_status": "已出库",
  "estimated_ship_at": "2026-09-03"
}

跨境策略是组织合规要求的工程实现,不应被误写成某个框架的保证。NIST AI RMF 的定位是帮助组织管理 AI 对个人、组织和社会的风险,并明确其为自愿使用的框架;其生成式 AI Profile 用于识别生成式 AI 的特有风险和管理措施。它提供风险管理方法,不替代具体司法辖区的法律判断。(nist.gov)


七、导出:导出的不是一张聊天表,而是主体可理解的数据包

数据导出是把与某个主体有关、且在导出范围内的数据,以可读、可处理的形式交付给有权请求者。

一个 Agent 的导出包至少可以包含:

manifest.json
conversations/
memories/
documents/
generated_files/
consents/
tool_actions/
audit_references/

但审计日志不能无条件原样导出。日志可能包含:

  • 其他用户的信息;
  • 内部系统提示词;
  • 访问令牌;
  • 安全规则;
  • 第三方商业机密;
  • 不应向请求者暴露的检测结果。

因此导出过程通常分为:

  1. 身份核验;
  2. 确认主体和范围;
  3. 查询数据资产索引;
  4. 去除无关第三方数据;
  5. 脱敏内部字段;
  6. 固定一致性快照;
  7. 生成可读和机器可读格式;
  8. 记录导出事件;
  9. 设置下载链接过期时间;
  10. 删除导出临时文件。

导出的时间点也很重要。若导出任务运行期间仍有并发写入,结果可能不一致。常见做法是使用数据库快照或事件序号:

snapshot_event_id = 98120

读取所有 event_id <= 98120 的数据
导出完成后,再提示 98121 之后产生的数据未包含在本次快照中

不能把“当前查询返回的几张表”冒充完整导出。记忆、向量、对象存储和生成文件往往不在同一个数据库中,必须依靠数据资产索引关联它们。


八、删除:可追溯删除需要知道“删了什么、删到哪里、还剩什么”

1. 删除请求的生命周期

sequenceDiagram
    participant S as 主体
    participant API as 隐私 API
    participant IAM as 身份服务
    participant IDX as 数据资产索引
    participant Q as 删除队列
    participant DB as 主库/记忆库
    participant V as 向量库
    participant OBJ as 对象存储
    participant BK as 备份系统
    participant AUD as 审计系统

    S->>API: 提交删除请求
    API->>IAM: 核验身份和权限
    IAM-->>API: 核验通过
    API->>IDX: 查询主体关联资产
    IDX-->>API: 返回资产清单和版本
    API->>Q: 创建删除任务
    Q->>DB: 删除消息、记忆、索引
    Q->>V: 删除文档块和 embedding
    Q->>OBJ: 删除文件和对象版本
    Q->>BK: 标记备份删除/等待轮换
    Q->>AUD: 写入最小删除证据
    Q-->>API: 返回任务状态
    API-->>S: 异步任务编号

删除接口不应同步等待所有系统完成,因为向量索引、对象存储和备份通常是异步系统。接口返回的应是:

{
  "request_id": "del-20260901-001",
  "status": "accepted",
  "scope": "subject:u-1001"
}

之后通过任务状态查询:

{
  "request_id": "del-20260901-001",
  "status": "partial_failure",
  "completed": [
    "conversation_db",
    "memory_db",
    "vector_store"
  ],
  "pending": [
    "backup_2026_08_31"
  ],
  "last_error": "backup provider accepted tombstone; physical expiry follows rotation policy"
}

2. 删除与备份

备份有三种处理方式:

方式一:立即改写备份

安全性最好,但对不可变备份、磁带和对象锁定存储通常不可行。

方式二:加密擦除

每个租户或数据域使用独立数据密钥。删除时销毁密钥,使备份中的密文不可恢复。

这要求:

  • 密钥确实只用于该数据范围;
  • 没有明文副本;
  • 密钥备份也被纳入删除策略;
  • 密钥销毁过程有独立审计证据。

方式三:备份轮换后过期

保留备份中的数据直到备份自然过期,同时在在线系统设置删除标记,恢复时不得把已删除数据重新导入生产。

这不是“已经立刻物理删除”,而是“在线副本立即隔离,备份副本按轮换周期完成清除”。产品文档和用户响应必须如实区分这两种状态。

3. 删除证据不能重新制造隐私泄露

审计系统需要证明删除发生过,但不应保存被删除的原文。可保存:

{
  "request_id": "del-20260901-001",
  "subject_ref": "hmac:u-1001",
  "asset_type": "conversation",
  "asset_count": 42,
  "deletion_started_at": "2026-09-01T03:00:00Z",
  "deletion_completed_at": "2026-09-01T03:00:08Z",
  "result": "completed",
  "executor": "privacy-worker-v3",
  "evidence_hash": "..."
}

subject_ref 可以使用带密钥的 HMAC,而不是直接写入用户 ID。证据哈希用于证明记录未被篡改,但哈希本身不应被误解为原数据已经不可识别。


九、一个最小数据模型

以下 SQL 展示了数据资产索引、保留期、删除状态和来源关联。示例使用 PostgreSQL 风格语法:

CREATE TABLE privacy_assets (
    asset_id         UUID PRIMARY KEY,
    tenant_id        TEXT NOT NULL,
    subject_ref      TEXT NOT NULL,
    asset_type       TEXT NOT NULL,
    storage_system    TEXT NOT NULL,
    storage_key       TEXT NOT NULL,
    purpose          TEXT NOT NULL,
    classification    TEXT NOT NULL,
    region            TEXT NOT NULL,
    consent_id        TEXT,
    created_at        TIMESTAMPTZ NOT NULL,
    retain_until      TIMESTAMPTZ,
    deleted_at        TIMESTAMPTZ,
    deletion_status   TEXT NOT NULL DEFAULT 'active',
    source_event_id   TEXT,
    metadata         JSONB NOT NULL DEFAULT '{}'
);

CREATE INDEX privacy_assets_subject_idx
ON privacy_assets (tenant_id, subject_ref, deletion_status);

CREATE INDEX privacy_assets_expiry_idx
ON privacy_assets (retain_until)
WHERE deletion_status = 'active';

删除任务应以资产为单位执行,而不是只删除业务主表:

UPDATE privacy_assets
SET deletion_status = 'deleting'
WHERE tenant_id = :tenant_id
  AND subject_ref = :subject_ref
  AND deletion_status = 'active';

任务处理完成后:

UPDATE privacy_assets
SET deletion_status = 'deleted',
    deleted_at = CURRENT_TIMESTAMP
WHERE asset_id = :asset_id
  AND deletion_status = 'deleting';

如果底层系统失败,不要直接写成 deleted

UPDATE privacy_assets
SET deletion_status = 'failed',
    metadata = metadata || jsonb_build_object(
        'last_error', :error,
        'retryable', :retryable
    )
WHERE asset_id = :asset_id;

一个常见错误是先删除业务数据,再删除资产索引。这样一旦索引删除失败,系统将失去“还有哪些副本”的清单。更安全的顺序是:

建立资产索引
  -> 执行删除
  -> 验证底层删除
  -> 更新资产状态
  -> 生成删除证据

资产索引本身也包含主体引用,因此它需要独立的保留和访问控制。


十、并发、重试和故障路径

1. 删除与新写入并发

假设用户在 t1 发起删除请求,删除任务在 t2 开始;但另一个会话在 t2t3 之间继续写入一条新的长期记忆。如果删除任务只扫描一次,就会遗漏这条记忆。

解决方法之一是引入主体删除屏障:

ACTIVE
  -> DELETE_REQUESTED
  -> 禁止新增长期记忆和新建导出副本
  -> 扫描并删除现有资产
  -> 验证没有新写入
  -> DELETED

或者使用事件序号:

delete_start_event = 98120
delete_barrier_event = 98135

删除任务必须处理:

event_id <= 98120:请求前已有数据
98120 < event_id <= 98135:删除期间产生的数据
event_id > 98135:删除完成后才允许产生,或拒绝写入

2. 重试必须幂等

删除任务可能因为网络错误被执行多次,因此底层删除接口应满足:

删除已不存在的对象 -> 成功
重复设置 deleted_at -> 成功
重复写入删除证据 -> 使用 request_id 去重

不要使用“删除成功后再发送一次不可重放的破坏性命令”而没有幂等键。

3. 删除失败的诊断顺序

当系统报告“已删除,但检索仍能召回”时,按以下路径排查:

  1. 主库是否仍有原始记录;
  2. 会话缓存是否仍有旧响应;
  3. 文档分块是否仍存在;
  4. embedding 是否仍在向量库;
  5. 检索过滤是否遗漏 deleted_at
  6. 搜索索引是否尚未刷新;
  7. 是否存在异步队列中的旧任务;
  8. 是否从备份恢复过旧数据;
  9. 模型是否只是生成了相似内容,而不是检索到了原文;
  10. 日志或评测集是否仍包含原始片段。

“模型还记得”是一个含糊诊断。必须先确认内容来源是:

当前上下文
还是 RAG 结果
还是长期记忆
还是缓存
还是日志注入
还是模型自身的训练知识

应用层可以删除前四类在线资产,但不能把供应商模型的通用参数记忆、训练数据处理和删除能力假设为应用方自动可控。是否进入训练、能否删除或排除,取决于供应商条款、产品配置和具体合同,不能仅凭 API 调用结果推断。


十一、审计记录如何支持隐私,而不是扩大泄露面

隐私治理需要审计,但审计日志本身是高价值数据。Agent 审计至少要记录:

谁:
  subject、actor、service identity、tenant

做了什么:
  指令、工具调用、数据库操作、审批结果

对什么做:
  数据资产引用、资源 ID、权限域

为什么做:
  purpose、policy version、consent reference

结果如何:
  success、failure、denied、partial

何时何地:
  timestamp、region、trace_id

是否可验证:
  event hash、链式序号、签名或不可变存储引用

但不应默认记录完整 prompt、完整工具响应和完整模型输出。更小的审计记录可以是:

{
  "event_type": "tool_call",
  "actor_ref": "user-hmac:...",
  "tool": "order.read_status",
  "resource_ref": "order-hmac:...",
  "purpose": "provide_service",
  "policy_version": "privacy-policy-2026-09",
  "input_digest": "hmac:...",
  "result": "allowed",
  "region": "cn-mainland",
  "trace_id": "tr-001"
}

这里的 input_digest 用于关联和完整性验证,而不是让审计人员恢复输入。需要重现故障时,应使用有权限的短期调试捕获,并经过审批、脱敏和自动过期,而不是永久保存所有原文。

不可抵赖记录也不等于必须公开原文。不可抵赖关注的是:

  • 谁产生了事件;
  • 事件是否被篡改;
  • 事件顺序是否可信;
  • 系统是否能证明某次授权或删除确实发生。

它不要求审计日志包含最多的数据。


十二、常见误解和反例

误解一:模型供应商承诺“不训练”,所以可以发送原文

“不用于训练”只覆盖某一类用途,不能自动解决:

  • 供应商日志保留;
  • 人工支持访问;
  • 区域位置;
  • 备份副本;
  • 请求内容泄露;
  • 工具服务和应用日志中的副本。

误解二:把邮箱替换成 <EMAIL> 就完成了隐私保护

如果系统仍然保存:

用户 ID + 时间 + 完整地址 + 唯一订单 + 特殊病史

攻击者可能通过组合字段重新识别主体。脱敏应按场景评估重识别风险,而不是只统计替换了多少个正则匹配。

误解三:只删除会话表就完成删除

Agent 的原文可能已被:

摘要任务
embedding 任务
缓存
分析平台
导出文件
备份快照

复制。删除必须基于资产索引和数据血缘。

误解四:把个人数据导出后打包下载

如果压缩包没有短期有效期、访问控制和导出审计,导出动作本身会制造新的泄露副本。导出文件应具有自己的 asset_id 和保留期限。

误解五:让模型决定是否可以读取数据

模型可以提出工具调用,但不应成为最终授权者。恶意文档、邮件或网页中的间接提示注入,可能诱导 Agent 读取并外传本不应访问的数据。过度代理的根因正包括过多权限和过高自治。(genai.owasp.org)


十三、生产验收应验证行为,而不是只检查配置

至少应设计以下测试。

最小采集测试

输入包含姓名、电话、地址和订单号,断言:

模型请求中没有姓名、电话、地址
工具请求中只有经过授权的订单号
日志中没有原始手机号

跨域路由测试

将数据分类设为 restricted-cn,模拟目标模型区域为 us,断言:

请求在路由层被拒绝
没有产生供应商请求
拒绝事件进入审计日志

RAG 隔离测试

租户 A 写入文档,租户 B 使用相同关键词检索,断言:

召回结果为空
模型上下文不包含租户 A 的 chunk
向量数据库审计记录显示过滤条件包含租户 B

删除传播测试

创建同一主体的数据副本:

主库、缓存、记忆库、向量库、对象存储、日志引用、备份索引

发起删除后逐项验证。任何一个系统仍能返回原文,都不能标记为 completed

并发测试

删除进行期间持续写入记忆,断言:

新的长期记忆写入被拒绝,或被纳入同一删除批次
删除完成后不存在可检索的新增资产

恢复测试

从删除前备份恢复到隔离环境,执行删除墓碑重放:

restore -> load deletion tombstones -> delete matching assets -> verify

如果恢复流程会把已删除数据重新送入生产,说明删除策略只覆盖了在线系统,没有覆盖灾备生命周期。


十四、边界:什么必须由组织策略和法律确定

工程系统可以保证:

  • 某字段没有发送给模型;
  • 某个租户无法检索另一个租户的向量;
  • 删除任务覆盖了资产索引中的存储系统;
  • 删除失败会进入重试和告警;
  • 导出包不包含内部凭据;
  • 跨区域调用在网关被阻断。

工程系统不能单独决定:

  • 某类数据在某司法辖区是否可以跨境;
  • 某个保留期限是否满足法定义务;
  • 某种处理是否需要同意;
  • 某次安全调查是否构成合法保留;
  • 供应商是否可以将请求用于训练;
  • 模型参数中的信息是否具备可执行删除能力。

因此,Agent 隐私基线需要把政策配置和技术控制绑定起来:

法律/合同判断
  -> 数据分类与目的
  -> 路由策略
  -> 模型和工具权限
  -> 保留期限
  -> 导出范围
  -> 删除目标
  -> 审计证据

如果只做加密,不做数据资产索引,无法解释数据在哪里;如果只做脱敏,不做权限控制,令牌和派生数据仍可能泄露;如果只做删除接口,不处理并发、缓存、向量和备份,删除就只是一个数据库操作。

Agent 数据隐私的核心不是“把所有数据保护起来”,而是把数据的必要性、可见性、流向、生命周期和可撤销性设计成系统状态。最小采集减少进入系统的数据,脱敏减少单个组件能看到的数据,保留策略限制数据存在的时间,跨境控制限制数据流向,导出保证主体能够理解系统保存了什么,删除则把这些约束落实到每个副本和每条数据血缘上。


系列导航与关联阅读

官方资料

本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。