# AI 代理更改功能旗标：控制每一次写入

更改功能旗标的 AI 代理必须被视为执行生产写入，因为事实就是如此。旗标可能位于部署流水线之外，但它可以在几秒内为客户启用代码、关闭支付路径、扩大实验范围，或禁用安全检查。

我经常看到的错误，是因为任务听起来无害，就直接让代理访问旗标提供商，例如：“为内部用户开启新流程”或“把发布比例降到零”。变更本身可能是正确的，危险之处在于缺少记录。发生故障后，团队需要知道代理请求了什么、谁接受了请求、提供商执行了什么，以及最终状态是否与请求一致。聊天记录承担不了这个责任。

## 旗标变更就是生产写入

更新功能旗标在运营层面与修改生产数据库设置或编辑在线路由规则没有区别。API 调用可能很小，但影响范围可能很广，而且会立即生效。

把旗标当作无害配置，往往会导致一种常见故障。代理发现错误率升高，找到与近期功能关联的旗标，然后将它设置为 `false`。这可能保护用户，也可能因为旗标使用了定位规则而不是简单的布尔值，或者因为代理选错了环境，导致一条无关的代码路径被禁用。如果没人能说清楚具体请求的变更，以及批准它的人是谁，团队在故障处理中就会争论历史，而不是恢复服务。

Martin Fowler 的 Feature Toggles 文章指出了一种经常被团队忽略的区别。发布开关、实验开关、运营开关和权限开关的生命周期不同，动态程度也不同。这种区别应该改变团队对代理操作的控制方式。用于关闭故障集成的运营开关，可能需要快速的人类审批。改变谁能访问受监管数据的权限开关，则需要严格得多的流程。把两者都叫作“旗标”，几乎无法说明风险。

旗标变更还绕过了工程师通常与代码部署联系在一起的控制措施。拉取请求可以展示同行评审，构建可以展示测试结果，发布可以展示制品版本。旗标提供商的调用可能只显示令牌名称和时间戳。如果令牌由自动化进程持有，甚至这个令牌名称也可能在多次运行之间共享。

在任何生产写入前，都应提出同一个问题：这次请求究竟会改变什么状态，凭谁的授权，以及我们如何证明最终状态？

读取与写入不同。代理可以检查旗标、环境和当前规则，用来准备建议。写入则会改变外部系统。不要因为集成起来方便，就把这两种权限捆在一起。

## 分开记录意图、授权、执行和观察到的状态

一个事件不足以描述一次功能旗标操作。可靠的记录应包含四类不同事实。把它们混在一起，会掩盖之后真正需要诊断的故障。

**意图**是代理请求发生的事情。它应写明稳定的旗标标识符、环境、请求的值或规则变更、原因和范围，而且必须在调用提供商之前捕获。

**授权**说明谁允许执行这个意图。人类审批必须绑定到精确的拟议变更，而不是“让代理管理旗标”这类模糊请求。审批者需要足够的上下文来做决定：目标环境、当前状态、拟议状态、受影响的用户群体、过期时间（如果有），以及触发请求的任务或事件。

**执行**记录发出的请求和提供商的响应。这能证明网关尝试执行已批准的操作，但不能证明目标状态已经存在。

**观察到的状态**来自写入后重新读取旗标。它可以发现格式错误的负载、部分更新、默认行为，以及请求发往错误项目或环境的情况。重新读取仍有局限。缓存的 SDK 客户端可能尚未获取变更后的配置，而重新读取也不能证明旗标背后的代码行为正确。这些属于不同的观察，应记录在部署遥测和应用监控中。

这种区别在回滚时尤其重要。假设代理请求 `checkout_v2=false`，人类批准了请求，提供商也返回成功。如果代理重新读取之前，第二名操作人员修改了旗标，简单的日志会讲出一个看似完整却错误的故事。正确的记录应说明：已批准的请求在 API 边界处成功，然后报告代理观察到的状态，包括提供商提供的版本或修订号。

不要让自由文本原因取代这些字段。“缓解结账错误”是有用的背景信息，但它不是目标、旧值、已批准的新值，也不是最终结果。

## 在调用提供商前捕获变更信封

代理应提交结构化变更请求，而不是自行拼接发往旗标提供商的任意 HTTP 请求。固定格式能让审核者读懂，也能让网关验证。

下面的示例使用布尔旗标，但同样的结构也适用于 JSON 配置、百分比发布和定位规则。规则变更应与标量值变更分开处理。定位规则可能大幅改变曝光范围，其影响远超一个 true 或 false 值所暗示的程度。

```json
{
  "request_id": "ffchg_01J8KQ4W6D7P",
  "correlation_id": "incident_INC-1842",
  "operation": "set_boolean_flag",
  "flag": {
    "project": "storefront",
    "environment": "production",
    "name": "checkout_v2"
  },
  "expected": {
    "value": true,
    "revision": "481"
  },
  "requested": {
    "value": false
  },
  "reason": "Reduce checkout failures while payment timeout is investigated",
  "rollback": {
    "value": true,
    "expires_at": "2025-03-08T18:00:00Z"
  }
}
```

`expected` 部分可以防止无声覆盖。它的意思是：只有当前值和修订号仍与请求者检查时一致，才执行这次变更。如果代理读取旗标后，另一个人或自动化流程修改了它，就拒绝请求并显示新状态。此时不应盲目重试。代理需要重新发起确认，因为它采取行动的依据已经失效。

`request_id` 必须支持幂等。提供商收到请求后、调用方收到响应前，网络可能发生故障。如果没有幂等机制，代理重试可能生成重复审计事件，或者在提供商将更新建模为补丁时重复应用百分比规则。应保存请求 ID，并在收到重复提交时返回原始结果。

网关可以按以下形式返回结果：

```json
{
  "request_id": "ffchg_01J8KQ4W6D7P",
  "status": "applied",
  "provider_request_id": "p_9d3ab",
  "authorization": {
    "approver": "ops@example.com",
    "approved_at": "2025-03-08T17:18:32Z"
  },
  "observed": {
    "value": false,
    "revision": "482",
    "read_at": "2025-03-08T17:18:35Z"
  }
}
```

不要在这些记录中放入提供商令牌。包含凭据的请求日志最终会变成第二个秘密存储，而且通常访问控制更差，副本也更多。

## 审批必须说明影响范围

人类不能只凭旗标名称就批准一项安全变更。名称会变化，旗标可能远远超出最初的用途，而一个布尔值背后可能隐藏着规模很大的定位规则。

审批卡片或审核界面应并列显示当前表示和请求表示。对于百分比发布，应显示准确的旧百分比和新百分比、用户群体或分群、任何前置旗标以及环境。对于规则编辑，应以易读的规范形式显示完整的变更前规则和变更后规则。如果差异展示省略了看似重复的子句，就可能有人误把“区域 A 的员工”变成“所有用户”。

审批者还需要看到原因和过期时间。临时运营变更很容易变成永久变更，因为事件结束后所有人都转向了别的工作。过期时间不是神奇的回滚机制。它会为操作人员安排一个重新评估旗标的时间点，也会在操作记录中形成明确的后续承诺。

审批范围应匹配变更，而不是匹配代理。“在本次会话剩余时间内批准这个进程”适合重复读取，或适合一组预先定义的非生产操作。但对于每次操作都会改变不同客户群体的生产发布，这样的范围过大。

只要出现以下任一情况，我就会要求每次生产变更都单独审批：

- 变更影响运营控制、支付、身份验证、授权或数据保留。
- 请求修改的是定位规则、分群、前置条件或百分比，而不是单个布尔值。
- 操作目标是生产环境，或与真实客户流量相连的环境。
- 代理提出的值与已批准的回滚方案不同。

这不是审批表演。人类的工作不是重新输入请求，而是判断当前说明的范围和运营状况是否足以支持这次写入。如果审批卡片隐藏了影响范围，审批者只能机械地点头。

避免使用“允许代理管理功能旗标”这种长期审批。它之所以受欢迎，是因为可以减少打断。但它也会把之后的每次变更都变成未经审核的生产写入，包括代理被过期上下文或误导性的工具结果影响而做出异常操作的情况。

## 并发会让自动回滚默认处于不安全状态

代理只有在能证明自己正在撤销自己的变更时，才可以回滚旗标。常见的“失败时始终让代理回滚”建议忽略了并发操作，因此并不安全。

考虑下面的顺序。10:00 时，当前值为 `true`，修订号为 481。代理获得批准，将它设置为 `false`，提供商记录修订号 482。10:06，一名值班工程师发现了另一个症状，有意将旗标设置为 `true`，修订号为 483。10:08，代理的监控条件触发，它执行原定的回滚，将值恢复为 `true`。

在这个具体例子中，重复设置同一个值似乎没有问题，但如果对象是定位规则，同样的模式就可能造成损害。值班工程师可能刚刚修改规则，将某条路径限制到一个租户。代理却因为记得变更前的快照，恢复了旧的宽泛规则。它在没有看到人工干预的情况下覆盖了这次有意的修改。

回滚请求应包含原始操作创建的修订号，并将其作为预期状态。只有在提供商仍报告该修订号，或仍保存代理写入的完全相同的规范配置时，网关才应执行回滚。如果条件不满足，应返回 `needs_review`，同时提供当前配置。代理可以向人类解释冲突，但不应自行解决冲突。

使用带有父操作信息的回滚负载：

```json
{
  "request_id": "ffrb_01J8KR0Y8J2M",
  "operation": "rollback_boolean_flag",
  "parent_request_id": "ffchg_01J8KQ4W6D7P",
  "flag": {
    "project": "storefront",
    "environment": "production",
    "name": "checkout_v2"
  },
  "expected": {
    "value": false,
    "revision": "482"
  },
  "requested": {
    "value": true
  }
}
```

有些提供商不公开修订号，也不支持条件更新 API。在这种情况下，对于存在竞争的生产旗标，你无法让自动回滚达到足够安全的程度。读取当前状态，展示差异，并要求人类批准恢复写入。承认这个限制，比假装时间戳可以提供并发控制更好。

还要区分功能旗标回滚和用户状态恢复。关闭旗标可能阻止进一步曝光，但不会撤销数据迁移、队列中的工作，也不会删除功能运行期间创建的记录。如果旗标控制写入，审批上下文应明确说明这一点。

## 限制操作、目标和凭据

代理应只能请求一组有限的旗标操作，而不是获得提供商的管理员权限。提供商令牌或 API 凭据必须留在代理上下文之外。

先列出允许的操作。`get_flag` 和 `list_flag_metadata` 是读取操作。`set_boolean_flag` 是范围较窄的写入操作。`set_rollout_percentage`、`replace_targeting_rule`、`create_flag`、`archive_flag` 和 `edit_segment` 各自都有更大的影响，应当作为不同的操作处理。不要暴露通用的 `PATCH /flags/{id}` 操作，然后指望代理始终谨慎。通用补丁接口会暴露审核者没有预料到的字段。

接着限制目标。如果提供商支持，将凭据绑定到项目和环境。在操作网关中，为具体任务维护旗标标识符和操作的允许列表。负责 `checkout_v2` 的发布代理不需要访问整个组织中的每个旗标。

如果操作请求使用了未批准的操作、未知环境、缺少预期修订号的目标，或未获允许的旗标，应该在到达提供商之前失败。这种验证必须是确定性的。“只做安全变更”之类的自然语言策略只是给代理留下一句话去解释，并没有提供可执行的边界。

Sallyport 将 HTTP 凭据保存在加密保险库中，并在不把秘密传给代理的情况下执行 API 请求。这种模式很适合此类场景，因为代理可以请求操作，而凭据仍留在控制 Mac 上。

将提供商凭据与代理自身的身份分开。提供商审计记录可能只能看到一个服务账户，但你的操作记录可以识别代理会话、来源任务以及批准这次精确写入的人。分离身份也让撤销更实际：你可以停止某一次代理运行，而不必轮换合法人工流程使用的凭据。

不要为了“这次事件”就把 API 令牌放进代理配置文件。代理比团队预想得更容易把上下文复制到对话记录、Shell 历史、生成的补丁和工具参数中。之后轮换令牌，并不能删除这些副本。

## 用故障而不是成功路径测试控制流程

只有在假设失效时也能正常工作，功能旗标集成才算准备就绪。成功路径，也就是读取值、批准、更新值，几乎不能证明什么。

在非生产环境中进行受控测试，并故意制造以下结果：

1. 代理读取旗标后修改它，然后提交原始请求。网关必须拒绝过期的预期修订号。
2. 模拟响应丢失后，两次提交同一个请求 ID。第二次调用必须返回第一次记录的结果，而不是发起新的变更。
3. 拒绝审批。提供商不应收到任何写入，审计记录应显示拒绝，而不是含义不明的超时。
4. 代理完成操作后让人类编辑旗标，然后尝试自动回滚。回滚必须停止并等待审核。
5. 在请求等待期间锁定或撤销代理会话。操作必须在使用凭据前失败。

这些测试会暴露一个隐蔽的设计问题：很多团队只记录成功的变更。失败和被拒绝的请求同样重要。被拒绝的请求说明代理尝试了自己无权执行的范围。过期写入被拒绝说明系统阻止了一次覆盖。两类记录都能解释为什么提供商状态没有在操作人员预期的时间发生变化。

也要测试规范化。两个 JSON 定位规则可能含义相同，只是字段顺序不同。如果比较并设置代码比较原始 JSON，就会产生虚假的冲突。如果标准化过度，又可能漏掉语义差异。应为提供商模型选择一种规范表示，保存这种表示，并用字段重排、省略默认值以及等价的分群引用进行测试。

最后，测试提供商返回成功但重新读取失败的情况。记录 `execution=accepted` 和 `observed=unknown`，不要把整个请求标记为成功。代理执行依赖性变更前，必须有人检查提供商状态。

## 审计轨迹必须经得起争议事件

一条有用的功能旗标记录必须回答一个怀疑性问题：“我们怎么知道这份变更记录事后没有被编辑过？”普通应用日志通常足以支持调试，但当许多人都能访问日志系统后，它们很少能回答这个问题。

写入只能追加的操作事件，并记录序列号、时间戳、请求信封、授权结果、执行结果和观察到的状态。通过请求 ID 和父请求 ID 关联相关事件。哈希链可以让之后的篡改变得可见：每个事件都包含自身内容的摘要，以及前一个事件的摘要。验证时按顺序检查整条链。

哈希链不会让日志自动变成事实。它无法证明审批者理解了请求，也无法找回根本没有写入的记录。但只要保存链并独立验证，就能发现事后的删除或修改。这才是准确的说法，而且已经比不说明机制就声称日志“不可变”有用得多。

将旗标提供商的原生历史作为辅助证据，而不是唯一记录。提供商公开请求 ID 时，应将它们匹配起来。把操作的 `correlation_id` 与事件记录或部署变更关联起来。有人询问旗标为什么发生变化时，你应该能追溯决策，而不是靠聊天消息和人的记忆重新拼凑。

Sallyport 会从加密哈希链审计日志中投影会话记录和单次调用记录，`sp audit verify` 可以在不使用保险库密钥的情况下离线检查链。这让团队能够验证其控制操作记录在内部仍保持一致，即使保险库处于锁定状态。

将被拒绝的操作和过期写入冲突纳入日常运营工作。它们不是噪音。冲突数量上升，可能说明多个自动化流程同时管理相同的旗标。针对目标的拒绝请求反复出现，可能说明代理任务范围过大，或定义不够明确。

## 先让代理生成变更提案

最安全的运营模式很简单：让代理检查、诊断并起草变更，然后要求受控的写入路径来执行。提案应足够详细，让另一名工程师无需阅读完整代理对话记录就能批准。

对于每个生产请求，都要求代理说明它观察到的当前状态、准确的目标状态、预期修订号、变更的帮助、可能受到影响的对象以及回滚条件。如果它无法提供这些事实，就还没有获得写入权限。

不要要求一篇很长的文章，要一份完整的请求。两者差别很大。冗长的文字经常掩盖这样的问题：代理从未检查目标环境，或者从未获取当前规则。结构化信封会立即暴露遗漏。

采用这种规范的团队会发现，许多拟议变更其实不需要执行。代理可能发现旗标已经是目标值，故障用户群体与规则不匹配，或者真正的解决办法是回滚部署。先读取并记录预期状态，可以阻止代理为了完成任务而进行形式上的写入。

功能旗标是控制线上行为的快速开关。只有当系统记录代理的请求，将人类决策绑定到精确变更，阻止过期覆盖，并验证提供商保存的状态时，才应让 AI 代理使用这个控制权。否则，你最方便的生产开关就会连接到一个没人能完整解释的账户。
