# 通过 AI 代理数据最小化，让工具调用更安全

AI 代理不应该因为客户记录中可能最终只需要一个事实，就接收整条记录。每次工具调用都需要更小的契约：要执行的操作、完成操作所需的最少字段，以及能够告诉代理发生了什么、又不会交出一大堆新客户数据的输出。

团队通常会在能力很强的代理和方便调用的内部 API 之间泄露数据。他们给代理一个通用的客户查询接口，返回完整响应，然后把结果称为有用上下文。这样一来，后续的每个提示词、转录、重试、调试记录和工具结果，都可能成为记录扩散的场所。

解决办法不是设计一个巧妙的脱敏提示词，而是一套设计纪律：在开放操作前逐一梳理每个操作，在执行边界强制使用窄请求，并让代理无法接触上游原始响应。

## 工具权限不等于代理可以查看整条记录

允许调用 API，与允许查看 API 能返回的每个字段，是两项不同的决定。团队之所以混淆它们，往往是因为服务账户可以读取宽泛对象，而代理工具只需要其中很小一部分。

设想一个需要判断是否发送付款提醒的代理。发送服务可能需要收件地址、模板标识符和发票引用。代理本身可能只需要 `eligible: true`、一个安全的显示名称和操作引用。它不需要付款历史、客户备注、税务信息，也不需要知道发送方在后台使用的地址。

宽泛的 `get_customer` 工具会制造一个看似方便、实则危险的入口。它一旦存在，提示词就会开始把它用于无关任务，因为调用它似乎比创建一个合适的操作工具更省事。接着，人们会用「不要暴露敏感字段」之类的说明来补救。说明不会从 JSON 响应中删除字段。

请把下面三个问题分开：

- 这个代理进程能否发起这项操作？
- 执行器需要接收哪些字段才能完成操作？
- 操作完成后，代理需要知道哪些事实？

每个问题都应产生自己的约束。访问控制回答第一个问题，请求验证回答第二个问题，响应整形回答第三个问题。一个适用于所有场景的 API 令牌和一个灵活的 JSON 端点，无法很好地回答其中任何一个问题。

当代理反复使用工具时，这种区分尤其重要。在演示中，一次宽泛查询看起来还能接受。但在真实运行中，代理可能在重试后再次调用它，在后续请求中引用它的输出，或把输出传给另一个工具。第一个多余字段最终会变成许多份多余副本。

## 围绕操作建立数据地图，而不是围绕数据库表建立

有用的数据地图应从一个动词和一个外部影响开始。「读取客户」并不是这里所说的操作。「确认发票是否逾期」和「创建运输标签」才是，因为它们都有明确的接收方、用途和预期结果。

为每个准备使用的工具先写一份小记录，再编写架构：

| 项目 | 示例：发送付款提醒 |
|---|---|
| 发起者 | 账单支持代理进程 |
| 影响 | 向符合条件的收件人发送一个已批准的模板 |
| 最少输入 | `invoice_ref`、`template_code` |
| 受信任查询 | 收件地址、语言偏好、资格规则 |
| 代理可见结果 | `sent`、`suppressed` 或 `needs_human_review` |
| 禁止输入 | 电子邮件地址、付款历史、账户备注、完整客户对象 |
| 禁止输出 | 收件地址、供应商原始响应、付款详情 |

受信任查询这一列承担了最困难的工作。它指出执行器可能需要、但代理不需要的信息。把查询移到边界之后。代理发送发票引用，受你控制的服务只有在检查操作后，才解析收件人。

不要把 `invoice_ref` 做成电子邮件地址的伪装副本，也不要让它成为包含客户姓名的复合标识符。不透明引用可以减少意外披露，但不会自动让系统变得私密。如果任何人都能用某个引用查询客户对象，它仍然需要授权、过期时间和受众限制。

欧盟《通用数据保护条例》在第 5(1)(c) 条中清楚地说明了相关原则：个人数据必须充分、相关，并限于实现处理目的所必需的范围。「处理目的」这几个字正是工程团队容易放松警惕的地方。目的不是「帮助代理完成工作」，而是边界上的具体操作，例如发送一条提醒或创建一个支持工单。

数据地图还会暴露那些不应向任一方向跨越边界的字段。自由文本备注应单独列出。它们经常包含粘贴的电子邮件、身份证件、凭据、健康信息，以及客户未经筛选的投诉。通用的 `notes` 字段没有明确范围，因此不应出现在常规工具请求中。

## 严格架构会在执行前拦截便利字段

架构应拒绝操作不需要的字段。静默忽略额外属性看似宽容，但会在测试期间掩盖泄露，也会让开发者误以为字段已经传递成功。

假设代理需要请求退款审核。下面的请求契约只允许案例引用和选定原因，并拒绝客户控制的金额、地址和任意备注。

```json
{
  "name": "request_refund_review",
  "description": "Create a review task for an existing support case.",
  "input_schema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["case_ref", "reason_code"],
    "properties": {
      "case_ref": {
        "type": "string",
        "pattern": "^case_[A-Za-z0-9]{16}$"
      },
      "reason_code": {
        "type": "string",
        "enum": ["duplicate_charge", "service_not_received", "other"]
      }
    }
  }
}
```

带有 `customer_email`、`shipping_address`、`amount` 或 `conversation_text` 的请求，应在验证阶段失败，并返回类似下面的明确结果：

```json
{
  "error": "invalid_request",
  "message": "Unexpected property: customer_email"
}
```

这个错误会告诉代理遵守契约，也会告诉开发者不需要的字段已经抵达边界。错误中不要回显被拒绝的值。很多泄露都来自错误处理器，因为它们为了调试而序列化整个失败请求。

仅靠架构验证无法保护服务端字段。请求可能带有格式看似正确、却属于其他账户的 `case_ref`，也可能将有效的 `reason_code` 与发给另一代理的操作引用配在一起。执行器必须将引用绑定到签发者、目标操作和有效期限。应把操作引用视为一次性取件凭证，而不是公开的数据库主键。

当代理起始上下文不受信任或过于宽泛时，使用请求构建器。构建器应提取允许的字段并创建新对象。不要拿一个大对象，只删除几个已知危险键。黑名单脱敏会在出现新字段、嵌套对象改变结构，或开发者使用不同名称的别名调用工具时失效。

```python
ALLOWED_REASONS = {"duplicate_charge", "service_not_received", "other"}

def build_refund_request(case_ref, reason_code):
    if not isinstance(case_ref, str) or not case_ref.startswith("case_"):
        raise ValueError("invalid case_ref")
    if reason_code not in ALLOWED_REASONS:
        raise ValueError("invalid reason_code")
    return {"case_ref": case_ref, "reason_code": reason_code}
```

这个小函数可以防止一种常见故障：由于客户端库接受任意关键字参数，开发者便把整个 `case` 字典传给它。明确返回对象看起来很无趣。在客户数据边界上，无趣是好事。

## 工具输出也需要自己的契约

原始工具输出就是代理上下文，即使工具输入从未包含客户数据。应把每个响应都视为代理可以引用、保留、转换或发送给其他系统的材料。

创建运输后，供应商响应可能包含收件人的完整地址、电话号码、承运商账户信息、标签数据、路由详情和内部诊断字段。代理通常只需要运输引用，以及是否可以告诉客户订单正在发往途中。

分别定义面向代理的响应和面向服务的结果：

```json
{
  "status": "created",
  "shipment_ref": "ship_Q7J4K2P8",
  "customer_message_allowed": true
}
```

执行服务可以在运营人员需要的地方保存或传递详细的承运商响应。但不应因为调试方便就把响应返回给代理。如果操作人员需要回执，应为他们提供受保护的界面。不要把代理转录当成故障排查数据库。

错误响应也需要同样处理。上游 API 可能返回被拒绝的街道地址、账户号码或请求中被引用的一段内容。应将其转换为代理可见的范围受限错误代码，例如 `recipient_unavailable`、`reference_invalid` 或 `provider_retryable`。受保护的诊断详情应放到面向操作人员的系统中。

Model Context Protocol 规范将工具定义为带有结构化输入和结果的可调用函数。这种结构为开发者提供了强制响应类型的清晰位置。返回散文式转储或任意 JSON 的工具，等于放弃了这一优势。窄响应架构也更容易测试代理行为，因为下一步决定只能依赖已知字段。

除非你能明确说明 `details` 的完整结构和敏感性规则，否则不要返回名为 `details` 的字段。模糊的逃生口会永久存在。有人会在事故期间把原始结果放进去，然后忘记删除。

## 脱敏、化名和保密是不同的控制措施

把姓名替换成令牌，并不意味着数据就适合流转。能够与客户数据库关联的稳定令牌，在大多数实际威胁模型中仍然属于个人数据。只限于一个调用者且有效期很短的一次性操作引用，失败模式要窄得多。

脱敏会从对象中删除已知值。当内部系统必须向操作人员展示记录时，它很有帮助，但不适合成为代理的主要边界。字段名称会变化，内容会移动到嵌套结构中，自由文本也会包含固定脱敏列表无法可靠发现的信息。

化名会用一个标识符替代直接标识符。当接收方无法解析映射关系时，它可以减少暴露。但如果同一代理能用该标识符调用宽泛查询工具，或者令牌出现在不相关的多个工具中，或令牌本身带有含义，化名就会失效。`acme-health-urgent-001` 并不会因为没有电子邮件符号就变得不透明。

保密来自于把解析数据和凭据留在受信任的一侧。代理发送受限指令，服务只针对获准的操作解析受保护信息。这一区分可以防止一种常见错误：把「已清理」的客户对象交给代理，然后以为工作已经完成。

还应把数据最小化与授权分开。获得适当授权的代理仍可能接收过多数据。反过来，未经授权的调用者可能只得到很少的信息，但这些信息仍可能造成伤害。两项控制都要执行，并分别测试。

## 宽泛凭据会削弱窄架构的可信度

如果代理持有可以直接调用底层 API 的凭据，再完美的请求架构也无法弥补问题。只要代理能读取密钥，或从运行环境使用密钥，它就可以绕过精心设计的工具，向供应商请求更丰富的响应。

应把凭据放在操作执行的位置，而不是模型推理的位置。执行器验证窄请求后，再注入授权请求头或 SSH 身份。代理看到的是结果，不是密钥、占位符，也不是环境变量中的副本。

RFC 6749 描述的 OAuth 2.0 使用作用域限制授予客户端的访问权限。作用域很有用，但许多实现把作用域当成一张宽泛的部门通行证。`customers.read` 作用域仍可能允许读取完整客户记录。应将凭据作用域与面向具体操作的端点和响应过滤结合起来，否则作用域只是在限制代理可以搜索哪一批大型数据集合。

按影响拆分权限。可以创建草稿的代理不应同时拥有发送草稿的权限。可以请求退款审核的代理不应拥有发起退款的权限。这种拆分可以减少这样的压力：为了让一个工作流运行起来，就添加一个通用的管理员凭据。

对于使用 Sallyport 的 macOS 团队，应用会把 API 和 SSH 凭据保存在加密保险库中，并执行 HTTP 或 SSH 操作，而不是把凭据公开给代理。只有当调用本身保持范围受限时，这种安排才有帮助。受保护的令牌仍然可能授权过于宽泛的请求。

## 支持工作流能展示数据如何扩散

一次常见泄露往往始于一个合理请求：让支持代理准备一份失败配送的回复。第一种实现会开放 `get_order(order_id)`，返回订单、客户资料、配送地址、付款状态、支持历史和承运商事件。代理其实只需要承运商事件，以及发送一条已批准更新的权限。

代理调用查询并接收完整记录。接着，它把选定详情传给消息起草工具。此时提示词已经包含地址和历史，尽管两者都不会影响消息。一次工具调用失败，又把整个提示词写入错误日志。工程师为了诊断问题，将错误复制到工单中。原本一次宽泛响应，变成了四个独立的保留问题。

应以不同方式构建工作流。给代理一个接受 `order_ref` 的 `assess_delivery_update` 操作。执行服务验证调用者权限，在内部获取订单，读取承运商状态，应用联系规则，然后只返回下面的内容：

```json
{
  "status": "contact_allowed",
  "event_code": "delivery_delayed",
  "approved_template": "delivery_delay_notice",
  "order_ref": "ord_9VJ3R6M1"
}
```

代理可以判断情况是否需要发送已批准的消息。另一个 `send_approved_delivery_update` 操作接受 `order_ref` 和 `approved_template`。它不接受电子邮件地址或自由文本正文。服务会在验证同意状态和订单状态后，解析收件人并渲染模板。

这种设计看起来更受限制，因为它确实如此。限制正是目的所在。代理无法随意把客户资料改用于其他任务，后续工具也不会意外收到这些数据。

不要用「工具需要灵活性」这种笼统说法反驳。灵活性应留在受信任的应用代码中，这样你才能测试、审查并审计它的数据处理方式。通过宽泛的请求和响应对象把灵活性交给代理，会把成本转移到每个提示词和每个下游系统。

## 批准界面无法检查每个隐藏字段

批准可以防止未经授权的操作，但无法可靠地管住过大的载荷。人们看到简短摘要，在时间压力下做判断，然后批准操作。如果服务在请求中隐藏了不必要的地址或历史记录，批准并没有减少披露。

当批准成为工具设计的替代品时，它还会带来糟糕的激励。开发者不断添加字段，因为「用户会批准每次调用」。很快，批准卡片包含太多细节而无法阅读，或包含太少信息而无法判断。人们会批准看似无害的重复操作，却注意不到某次请求新增了字段。

在批准界面中展示操作及其范围受限的参数，但要在界面出现前执行允许列表。批准者应该选择是否授权「为 ord_9VJ3R6M1 发送已批准的配送更新」，而不是手动检查序列化的客户记录。

对于每次执行都值得人工关注的影响，使用逐次调用批准。不要把它当成个人数据扫描器。看到批准界面的人既没有时间，也没有上下文判断每个嵌套字段是否必要。

一个好的检查很简单：把批准者从故事中拿掉。工具契约是否仍会阻止不必要的数据离开受信任服务？如果答案是否定的，说明边界做得还不够。

## 审计记录应证明操作，而不应变成另一个数据仓库

当代理代表客户采取行动时，你需要证据。但要获得这些证据，并不需要保存一份代理可读取的完整载荷永久档案。

记录操作名称、发起进程或会话、时间戳、结果、授权决定和关联引用。如果需要证明载荷完整性，应对规范化且受保护的表示计算加密摘要，而不是保存表示本身。记录被接受的字段名称，不要记录敏感值。

例如，一条审计事件可以采用以下结构：

```json
{
  "action": "send_approved_delivery_update",
  "session_ref": "sess_4KH8N2",
  "order_ref_digest": "sha256:8e4c...",
  "accepted_fields": ["order_ref", "approved_template"],
  "outcome": "sent",
  "authorized_by": "per_call"
}
```

摘要不会神奇地消除隐私风险。如果输入来自一个很小的已知集合，攻击者可以猜测值并比较哈希。操作人员需要获取详情时，应使用受保护的内部引用，并限制保存原始记录的系统的访问权限。绝不要把电子邮件地址的普通哈希当作匿名数据。

将运行诊断与代理自己的工具结果分开。支持人员可能需要在短时间内受保护地访问上游错误正文，但代理不需要。这样分离也能让删除和保留策略更清晰，因为原始记录不会在每本日志中累积。

Sallyport 会从一个不可写的加密哈希链式审计日志中记录代理会话和单次调用。其 `sp audit verify` 命令可以在没有保险库密钥的情况下离线验证链条。完整性验证只能回答记录是否被修改，事件架构仍然决定记录本身是否包含过多客户数据。

## 用有意过度共享来测试边界

只测试有效的正常调用，无法发现最容易导致意外暴露的行为。应测试代理提交完整对象时会发生什么，上游服务返回意外字段时会发生什么，以及请求中途发生异常时会发生什么。

使用包含容易识别的虚假敏感值的测试样例，然后断言这些值不会越过代理边界。样例应包含嵌套字段和自由文本，因为扁平示例会让脱敏代码轻松过关。

```json
{
  "case_ref": "case_Ab92Kx71LmQ4Rt8P",
  "reason_code": "duplicate_charge",
  "customer": {
    "email": "test.person@example.invalid",
    "address": "17 Example Lane",
    "payment_note": "card ending 4242"
  },
  "conversation_text": "Customer says their medical appointment depends on delivery."
}
```

预期结果应是验证失败，并且只指出意外属性的名称。然后检查四个位置：代理可见响应、应用日志、错误跟踪记录和审计事件。开发者经常验证了请求，却忘记异常中间件已经记录了原始正文。

也要为输出添加契约测试。模拟一个包含完整客户对象的上游响应，并断言工具只发出文档规定的字段。每当供应商客户端发生变化时，都要运行这项测试。SDK 升级可能增加响应字段，即使没有人修改代理提示词。

最后运行一次转录测试。给代理一个正常任务，捕获代理收到的所有工具输入和结果，然后搜索测试样例中的值。这项测试可以发现架构测试遗漏的意外提示词插值和调试文本。

通常最先需要重新设计的是宽泛的查询工具。用一个会产生真实外部影响的操作，或一个返回单一范围受限判断的操作替代它。如果新契约看起来过于窄小、不够方便，这往往说明系统一直依赖代理携带它根本不需要的数据。
