# API 结果截断：阻止代理做出错误变更

代理不需要恶意工具，也可能做出有害变更。一个悄悄只返回部分答案的工具就够了。给代理一份看起来完整的搜索结果，再让它删除搜索没有找到的内容，它往往会从错误前提出发，做出逻辑上完全合理却错误的操作。

解决办法不是在系统提示词里反复告诉代理要小心。工具必须以稳定、机器可读的形式说明：请求的工作是否已经完成，省略了什么，为什么省略，以及调用方如何继续。如果工具无法说明这些情况，代理就不能把结果中的缺失当成执行大范围变更的许可。

## 部分数据和空数据表达的是不同结论

空结果表示工具在实际检查的范围内没有找到匹配项目。完整的空结果则表示工具检查了请求的全部范围，并且什么也没找到。这是两种不同的结论，但大多数 API 契约把它们都压缩成同一个 `[]`。

这种混淆会导致一种具体的错误推断：

1. 代理请求列出所有没有当前负责人的服务账户。
2. API 扫描完第一页、达到结果上限，或省略了令牌无权读取的记录后，返回一个空数组。
3. 代理得出结论，认为每个账户都有负责人。
4. 它根据这个结论修改相关的控制项、报告或清理任务。

代理不需要误解英语。工具返回的答案形式，让它以为服务器知道的内容比实际更多。

工具至少应区分四种状态。完整查询可能返回项目，也可能不返回项目。不完整查询也可能返回部分项目，或一个项目都不返回。只有把第三种状态当成值得注意，便会漏掉最危险的情况：空响应让代理相信问题不存在。

权限会让情况更糟。许多服务会有意隐藏无法访问的对象，返回空集合或经过筛选的集合，而不是权限错误。对于面向人工用户的界面，这种行为可能合理。但对于自主清理任务，除非 API 明确说明调用方能看到什么，否则它不能作为可靠依据。

不要用 `Some results may be missing` 这样的文字说明充当契约。它没有给代理提供可靠的分支，也没有给工程师提供可测试的条件。名为 `complete` 的布尔字段看起来很普通，而这正是它的价值所在。

## 成功的 HTTP 响应也可能是不完整的答案

HTTP 状态码描述的是客户端与服务器之间的交互。它们本身无法证明搜索、清单或导出覆盖了请求的全部范围。

RFC 9110 定义了 HTTP 状态码的语义。`200 OK` 表示请求按照对应方法的语义成功完成。它并不表示搜索覆盖了每一页、每个分区、每个权限域，或截止时间前的每条记录。团队经常从 `200` 中读出协议并未承诺的含义。

看一下这个响应：

```json
HTTP/1.1 200 OK
Content-Type: application/json

{
  "items": [],
  "next_cursor": null
}
```

它看起来像最终结果。但 `next_cursor: null` 只表示这套特定分页机制没有下一页。它没有说明后端是否有结果上限，搜索任务是否过期，某个数据源是否失败，记录是否因策略被排除，或 API 是否悄悄限制了回溯时间。

只有当契约说明 `complete` 覆盖了什么范围时，响应才能成为可用证据。对于账户搜索，它可能表示在指定快照下，调用主体能够看到的所有账户。对于代码搜索，它可能表示指定版本中所有已索引的文件，同时明确排除被忽略的文件和未索引的生成文件。范围必须足够具体，让调用方可以判断它是否符合拟执行的操作。

不要让所有部分响应都返回 `500` 来解决这个问题。部分结果本身可能有用。仪表板可以展示它们，代理可以总结它们，人也可以检查它们。真正的错误，是把部分结果呈现成一个需要完整性的权威答案。

当请求的操作承诺提供原子或完整答案，却无法兑现承诺时，应使用错误。当部分数据本身有合理用途时，可以返回成功响应，但必须明确说明不完整。客户端需要的是确定性的区分，而不是争论 `200` 看起来是否过于乐观。

## 将完整性元数据放在每个结果旁边

结果契约应把完整性作为结构化数据公开，无论项目列表是完整、较短还是为空。不要让调用方通过项目数量、缺失的响应头，或 `message` 字段中的一句话来推断。这个结构适合集合搜索：

```json
{
  "items": [
    {"id": "svc-184", "owner": null}
  ],
  "complete": false,
  "truncated": true,
  "incomplete_reasons": [
    {
      "code": "RESULT_LIMIT_REACHED",
      "message": "The query stopped after the configured result limit.",
      "limit": 1000
    }
  ],
  "next_cursor": "eyJvZmZzZXQiOjEwMDB9",
  "scope": {
    "resource": "service_accounts",
    "visibility": "resources readable by this credential",
    "snapshot": "2025-03-08T14:20:11Z"
  },
  "warnings": []
}
```

字段的确切名称没有它们的含义和一致性重要。`complete` 是决策字段。`truncated` 描述了一种重要的不完整来源，但不应成为万能字段。权限筛选不是截断，联邦搜索超时也不是分页。如果一个标志承载了所有含义，调用方就会失去安全恢复所需的原因。

将 `warnings` 与 `incomplete_reasons` 分开。警告可以告诉调用方出现了已弃用字段、某个值被标准化，或请求的排序退回了默认值。不完整原因表示响应不足以支持对请求范围中未返回部分做出结论。这个区别决定了代理能否继续操作。

也不要只使用一个简单的 `has_more` 标志。它通常只回答一个狭窄的分页问题。看到 `has_more: false` 后，代理可能合理地认为集合已经结束，即使服务器端上限或无法访问的分片阻止了完整扫描。`has_more` 可以保留，但不能承担完整性的全部责任。

对于单资源读取，也要采用同样的原则。响应中省略的字段应说明原因：调用方没有请求它们、调用方无权访问、数据源失败，还是该值确实不存在。省略 JSON 字段很简洁，但含义不明确。

## 分页需要稳定边界，而不是更大的页面

只有当 API 让继续获取变得可靠，并说明哪些变化会使继续过程失效时，分页对代理才是安全的。提高页面上限只是推迟问题，并没有消除问题。

偏移分页尤其容易导致错误结论。代理读取第 0 到第 99 条记录，删除或创建一个对象，然后读取第 100 到第 199 条。如果底层顺序发生变化，它可能跳过一条记录，或处理同一条记录两次。对于提示性报告，这或许可以接受。对于变更计划，后果可能很严重。

游标分页通常更好，因为服务器可以在有序结果集中编码当前位置。但它仍需要契约。应说明游标是否冻结了快照、有效期多长，以及改变筛选条件、排序顺序或授权是否会使游标失效。游标过期时，不要悄悄重新开始扫描并返回合并后的答案。应返回明确的不完整状态，或要求客户端重新开始。

有用的集合响应应给调用方足够的信息，让它能够有意识地完成操作：

```json
{
  "items": ["item-001", "item-002"],
  "complete": false,
  "next_cursor": "cD0y",
  "page": {
    "returned": 2,
    "requested_size": 2,
    "ordering": "id ascending",
    "snapshot": "search-7f9c"
  },
  "incomplete_reasons": [
    {"code": "MORE_PAGES_AVAILABLE"}
  ]
}
```

调用方应持续获取，直到收到 `complete: true`，而不是仅仅因为收到了一页较短的结果就停止。短页可能有很多原因。有些 API 返回短页，是因为某个分区暂时稀疏，内部工作线程提前停止，或服务按字节而不是对象数量限制响应大小。

不要让语言模型通过文字记住这个循环。把分页行为放进工具实现中。高级的 `search_all` 工具可以收集各页、保留快照、限制自身工作量，并报告是否到达终止状态。如果达到自身上限，必须返回 `complete: false`，并说明是客户端上限导致了这种情况。

最后一种情况经常被忽略。工程师正确地给 API 加上了元数据，却又构建了一个 `max_pages=10` 的代理封装，并丢弃它在第十页停止这一事实。此时，封装层成了不完整性的来源。最外层工具契约必须负责公开这个信息。

## 时间限制、失败分片和权限需要各自的原因

搜索可能已经完成 HTTP 请求，但其中一部分工作尚未完成。分布式服务通常会把查询分发到多个索引或租户。如果一个数据源超时，而服务仍返回其他数据源的匹配项，结果可能有用，但它是不完整的。

用程序可以据此分支的代码表示原因。人类可读的文字应放在旁边，而不能取代代码。代码应少而稳定，并且有文档说明。例如：

- `MORE_PAGES_AVAILABLE` 表示调用方可以请求下一页。
- `RESULT_LIMIT_REACHED` 表示服务在耗尽匹配项前执行了上限。
- `TIME_BUDGET_EXCEEDED` 表示搜索在计划的全部工作完成前停止。
- `SOURCE_UNAVAILABLE` 表示某个已识别的数据源没有响应。
- `VISIBILITY_RESTRICTED` 表示调用方的授权排除了请求范围的一部分。

不要用普通成功响应掩盖 `VISIBILITY_RESTRICTED`。安全团队有时倾向于返回无法区分的响应，因为不想透露某个对象是否存在。这种担忧合理。API 可以报告权限限制使清单无法完整，而不必指出隐藏对象的名称。但它不能让调用方把部分清单误认为完整清单。

同样的规则适用于速率限制和配额。如果 API 在耗尽预算前读取了请求的前一部分，应同时报告已返回的数据和预算条件。重试可能在稍后完成，但重试是新的尝试。除非 API 提供稳定快照，或任务允许数据漂移，否则代理不能把两次尝试合并成完整结论。

截止时间既应是输入，也应是输出。当代理请求大范围清单时，让它设置工作时间预算，并收到已经完成的工作量。这能让取舍变得可见。十秒的侦察搜索可以作为人工审核前的准备，但对于删除搜索未发现的所有资源来说，它的证据很弱。

## 缺失内容不适合支持破坏性变更

代理可以安全地使用部分数据来起草报告、找出候选项，或请求人工检查一小组目标。它不应使用部分数据来推断某个资源未被使用、没有负责人、重复存在或可以安全删除。

区别在于结论的方向。找到一个 `owner: null` 的记录，是关于该记录的正面证据，但仍要考虑字段的新鲜度。没有找到未分配记录，则是关于整个搜索范围的全称结论。全称结论需要对定义范围进行完整覆盖。

这种故障经常伪装成效率提升。团队给代理一个名为 `list_inactive_projects` 的工具，然后让它归档所有返回的项目，甚至更糟，让它归档第二个列表中没有出现的每个项目。工具有最大结果数量。几个月后，一个大型组织超过了这个上限。没有人修改代理提示词，但代理的含义已经从「对清单执行操作」变成了「对清单中任意的前缀部分执行操作」。

设计操作工具时，应要求证据，而不是接受一段叙述。归档操作可以要求调用方提供先前完整清单选出的 ID，以及将选择结果绑定到读取操作的快照令牌。如果清单不完整，工具就拒绝操作。这样，安全检查位于模型无法用模糊说法绕过的位置。

对于无法使用快照令牌的操作，应要求提供明确范围，并在执行时重新检查每个目标。这不能证明原始搜索是穷尽的，但能防止一份过时列表授权无关的变更。操作范围应足够小，让审核者能够理解目标集合。

常见的替代方案是告诉代理：「除非确定，否则永远不要删除任何东西。」这听起来合理，实际却很容易失效。确定性只是提示词中的一个词，`complete: false` 则是工具可以强制执行的条件。

## 工具模式应迫使代理面对不确定性

MCP 工具或任何面向代理的封装，都应返回类型明确的外层结构，而不是一段吸引人的说明文字。模型可以阅读文字，但周边软件需要能够验证、记录、拦截和测试的字段。

一种实用的响应类型可能如下：

```json
{
  "status": "partial",
  "data": {
    "repositories": [
      {"id": "repo-a", "default_branch": "main"}
    ]
  },
  "completeness": {
    "complete": false,
    "reasons": ["TIME_BUDGET_EXCEEDED"],
    "continuation": {
      "kind": "retry_with_deadline",
      "minimum_seconds": 30
    }
  },
  "warnings": [
    {
      "code": "STALE_INDEX",
      "message": "Search index may lag the source repository."
    }
  ]
}
```

不要对这个响应使用 `status: "success"`。它会鼓励简单客户端丢弃元数据。`partial` 告诉调用方，它收到了有用但带有限制的数据。如果协议必须使用单一成功状态，就让 `complete` 成为必填字段，并要求能够执行操作的客户端在变更前检查它。

继续操作的字段应描述真实的恢复路径。下一页适合使用 `next_cursor`，速率限制适合使用 `retry_after`，服务器上限可能适合使用 `narrow_query`。不要提供一个只会重复相同查询、寄希望于环境自行改变的继续方式。

代理指令应建立一组简短而严格的规则：

- 只有当 `complete` 为 true 时，代理才能把空集合作为不存在的证据。
- 代理可以使用部分响应，提出范围受限的只读调查。
- 代理在请求批准任何依赖该结果的操作前，必须说明 `incomplete_reasons`。
- 代理不能自行补造缺失的继续令牌，也不能在没有结果的情况下声称重试成功。

这些规则很短，因为细节由数据携带。工具选择不报告的信息，提示词无法补回来。

## 警告需要负责人和失效路径

当每个响应都发出含糊的提醒时，警告就会变成背景噪声。警告应具体、可追溯，并且能够促成行动。如果一条警告从不改变调用方的下一步选择，它通常应该成为文档，或直接删除。

例如，`STALE_INDEX` 应指出被索引的数据源，并在可能时提供观察到的版本或更新时间。这样，代理可以在修改代码前检查权威数据源。`PARTIAL_FIELD_SET` 应说明服务器省略了哪些字段，以及调用方能否请求这些字段。`DEFAULT_SCOPE_APPLIED` 应说明服务器选择的范围，因为默认范围经常导致意外的大范围操作。

不要无意中把警告变成阻断条件。调用方需要清晰的严重程度规则。完整性元数据决定结果能否支持关于整个范围的结论。警告决定可信度、新鲜度或解释方式。工具可以返回 `complete: true`，同时附带数据过时警告。这样的结果可能完整列出了索引中的每个项目，但仍不适合需要实时状态的变更。

给警告设置稳定代码，并针对这些代码测试消费者。不要只断言友好的提示文字。文字会随着作者改进措辞而变化，决策规则不应变化。

还要决定警告发布后由谁负责。如果运维团队连续六个月在每次调用中看到同一条警告，他们就会停止阅读。应修复底层条件，在适当情况下将其提升为硬错误，或在它不再影响决策时删除。永久亮着的黄灯会教会人和代理忽略黄灯。

## 测试必须覆盖危险的空响应

大多数测试套件都会覆盖正常结果页和服务器错误，却跳过最容易导致错误推断的响应：`items: []` 加上不完整状态。

为每个原因代码编写契约测试。验证 API 在列表有数据和为空时都返回元数据，SDK 能保留这些信息，代理封装不会把它压扁成文字。任何一层的回归，都可能把诚实的服务器响应变成误导性的工具结果。

在测试夹具中加入类似这样的用例：

```json
{
  "case": "empty first page with more pages",
  "response": {
    "items": [],
    "complete": false,
    "truncated": false,
    "incomplete_reasons": ["MORE_PAGES_AVAILABLE"],
    "next_cursor": "cursor-2"
  },
  "expected_agent_decision": "continue_search"
}
```

然后测试在这个夹具之后发起变更请求。预期决定应是 `refuse_or_request_review`，而不是 `perform_cleanup`。把策略写进测试名称。否则，未来维护者可能把这个防护当成过度谨慎的边缘情况，为了让自动化演示更顺畅而删掉它。

还要测试变更期间的分页。在两页之间插入、删除并重新排序记录。让游标过期。先让一个分片返回数据，再让另一个分片失败。在扫描过程中撤销一项权限。工具应保留有文档说明的快照，或报告它无法声称结果完整。只使用静态模拟数据库的测试，无法发现生产环境中出现的那些谎言。

属性测试在这里很有帮助。生成大于所有配置上限的集合，改变页面大小，并断言一个不变量：只有在已核对声明快照中的每个项目后，客户端才能将集合标记为完整。这个测试不需要语言模型，它属于普通的接口正确性测试。

## 人工审批应揭示缺失的证据

只有当审批界面展示了要求人做出的实际决定时，人工控制才有用。「允许代理操作」不是审批，而是在要求人接受一串不透明的假设。

工具报告数据不完整时，应显示拟执行的操作、目标范围、证据不完整的原因，以及恢复选项。一个有用的提示会说明清单在返回 842 个资源后超时，并询问是使用更长的截止时间重试、只对返回的 ID 执行操作，还是放弃变更。审核者随后才能做出真正的取舍。

Sallyport 在执行 HTTP 和 SSH 操作时，可以让凭据留在代理进程之外，Activity 记录则可以展示产生的调用。当审核者需要重建一次错误决定时，这种隔离和追踪很有用。但它们不能把含义模糊的 API 答案变成证据，因此工具响应仍必须携带完整性状态。

通过只在关键的不确定性出现时请求审批，避免审批疲劳。对于获取有文档说明的下一页这类例行继续操作，工具应自行处理，不要反复打断人工。遇到策略边界时则应停止，例如快照过期、可见性受限、操作依赖缺失内容，或拟执行的变更超出了已收集证据的范围。

第一项工程任务很小：找出所有可能返回列表、聚合结果或搜索结果的 API 封装，然后在其最外层响应中加入明确的完整状态。先从空结果和有上限的搜索开始。正是在这些地方，自信的代理最容易制造出看起来干净、实际错误的答案。
