# 面向 AI 代理的 API 分页：有边界的发现

没有页面预算的列表接口调用，不算发现。它只是启动了一个没有明确终点的远程过程，然后希望账单、速率限制和结果集都不要出问题。另一种错误同样危险：获取第一页，看到一个看似合理的答案，就悄悄把它当成整个系统。

分页会改变代理能够诚实声称的内容。返回某条记录，能证明这条记录存在，却不能证明不存在其他记录。某条记录缺失时，除非代理能说明检查过的范围、依赖的排序，以及服务器为何表示遍历已完成，否则这个结果几乎不能证明什么。

我见过代理把“查找过期访问令牌”这样无害的请求变成数千次调用，只因为没人告诉它发现应该在哪里结束。我也见过一页库存清单导致代理尝试清理位于第二页的账户。解决办法不是设计更聪明的提示词，而是给代理一份有边界的遍历契约，并让最终报告明确显示这个边界。

## 发现调用需要明确预算

每次分页发现调用都需要代理不能悄悄扩大的一组限制。在第一次请求前，先定义最大页面数、最大返回记录数、截止时间和速率限制额度。具体数值取决于任务，但限制本身不能省略。

把发现和操作分成两个阶段。发现阶段，代理收集标识符和事实。某一页出现可疑对象，并不意味着代理可以删除、轮换或修改它。证据足够后，代理可以提交范围明确的计划，或开始一个经过单独授权的操作阶段。

一份实用的契约包含四部分：

- 接口和所有过滤条件，包括 API 允许时使用的排序方式。
- 页面大小，以及页面数或记录数上限。
- 由 API 定义的完成条件，例如不存在下一个游标。
- 提前停止条件，例如找到指定对象，或用尽分配的预算。

不要把记录上限和页面上限混为一谈。如果服务允许每页返回 100 条记录，而代理的记录上限是 500，五次调用可能就够了。如果服务因为过滤条件或权限降低了实际页面大小，同一个任务可能需要更多调用。好的实现会在每次响应后同时检查这两个限制。

例如，代理要查找名为 `billing-service` 的代码仓库。如果接口的过滤条件和排序方式足以保证结论可靠，收到精确匹配后就可以停止。代理要找出所有没有分支保护的仓库时，则不能在第一个匹配项出现后停止。这个任务需要完整枚举，或明确标注为部分结果。

“全部”这个词应当有代价。只有遍历到服务器的终止条件，且没有触发页面、记录、时间或错误预算时，代理才可以使用它。如果其中一项限制被触发，报告必须写明“部分扫描”，并指出边界。

## 页面大小是成本控制，不是完整性设置

`limit`、`per_page` 或 `page_size` 参数告诉服务一次响应尝试返回多少条记录，并不代表代理需要检查整个集合的多少内容。设为最大值可以减少部分往返，但也可能让单个响应昂贵到发生超时、超过上下文预算，或让重要细节淹没在大量无关数据中。

从能够支持决策的最小页面开始。如果代理只需要一个确切账户，获取 20 条简略记录通常比请求 1,000 个完整对象更好。如果必须建立库存清单，先确认接口返回的是范围有界且有用的表示，再使用更大的受支持页面大小。

字段和页面大小同样重要。许多 API 提供 `fields`、`include`、`expand` 或类似机制。发现阶段应请求标识符、名称、状态、时间戳，以及驱动决策的那个属性。只有需要进一步检查的候选项，才获取完整详情。这样可以减少流量，也能让模型少接触容易误读的附带文本。

如果服务支持游标分页，可以使用这样的请求形式：

```http
GET /v1/projects?state=active\u0026limit=50\u0026sort=id HTTP/1.1
Authorization: Bearer injected-by-gateway
Accept: application/json
```

并期待将记录与延续状态分开的响应结构：

```json
{
  "data": [
    {"id": "prj_104", "name": "billing-service", "state": "active"}
  ],
  "next_cursor": "eyJvcmRlciI6ImlkIiwicG9zIjoiMTA0In0"
}
```

代理应记录它请求了 50 条记录，但实际收到了一条。不能根据数组较短就推断遍历完成。在这个例子中，唯一有用的完成信号是 `next_cursor` 不存在，或文档规定它为 null。

不要制定“始终使用 100”这样的武断规则。不同 API 支持的最大页面大小不同，有些服务还会分别按展开后的子对象数或响应字节数计算限制。只有任务确实受益于最大页面时，才请求文档规定的最大值。对于大范围扫描，中等页面大小通常能提供更好的检查点、更容易的重试，以及人类更容易审计的报告。

## 将游标视为不透明的服务器状态

游标不是换了个高级名字的偏移量。它的含义由服务器控制，代理必须逐字节复制到下一次请求中。游标可能编码排序位置、快照标识符、权限边界或签名。它看起来像 Base64，并不代表代理可以解码、编辑或自行生成。

安全循环很简单：使用稳定的过滤条件和排序参数请求第一页，保存该响应返回的游标，然后用同一个查询和这个游标提交下一次请求。除非 API 文档明确说明，否则保留所有原始参数。在请求之间改变过滤条件，可能使游标失效，或者更糟，产生看似合理但不连续的结果集。

```text
request = { state: "active", limit: 50, sort: "id" }
seen_ids = set()
pages = 0

while pages < 10 and len(seen_ids) < 500:
    response = GET /v1/projects with request
    record response status, request, and response cursor

    for item in response.data:
        if item.id in seen_ids:
            report "duplicate record encountered" with item.id
            stop or apply the provider's documented recovery method
        seen_ids.add(item.id)

    pages += 1
    if response.next_cursor is absent:
        report "complete"
        break

    request.cursor = response.next_cursor
else:
    report "partial: traversal budget reached"
```

重复检查不是装饰。代理遍历集合时，集合可能发生变化，也可能存在有缺陷的分页实现。重复记录不一定意味着服务已经失败，但它意味着代理不应继续假装自己得到了干净的枚举结果。如果服务提供快照令牌、`as_of` 参数或有文档说明的一致性模式，对于会导致重要操作的任务，应使用它们。

游标过期也需要明确规则。有些服务让游标很快失效，有些服务把游标绑定到会话，或者在查询变化时使其无效。收到游标过期响应时，代理应保留错误信息，然后从稳定检查点重新开始，或将扫描结束为不完整。不能通过猜测新的游标来跳过中间部分。

重启也可能制造完整性的假象。如果第一次遍历和重启之间记录发生变化，合并后的列表可能存在遗漏或重复。报告中应说明重启，以及用于恢复的条件，例如 `created_at >= last_observed_timestamp`。如果没有稳定的恢复方法，应报告集合在遍历期间发生变化，不要把结果用作删除清单。

## 集合发生变化时，偏移分页会漂移

偏移分页使用类似 `offset=200\u0026limit=50` 或 `page=5\u0026per_page=50` 的数字。它容易编写脚本，也容易解释，因此仍然很常见。但当代理遍历期间有新记录加入或旧记录消失时，它会变得不可靠。

假设第一页按最新优先顺序返回记录 1 到 50。在代理请求第二页之前，又加入了 10 条新记录。此时 `offset=50` 会从新插入记录之后开始，并与代理已经看过的对象重叠。如果第一页中的记录消失，同一个偏移量又可能跳过向前移动的对象。代理不能只靠去重 ID 修复问题，因为去重只能发现重复，发现不了遗漏。

如果 API 允许稳定排序，应选择带有确定性决胜条件的排序。单独使用 `created_at` 通常不够，因为多个记录可能拥有相同时间戳。如果服务有文档说明，可以使用 `created_at,id` 这样的排序，让代理记录高水位标记并更谨慎地恢复。如果 API 只有偏移量，没有快照或稳定排序，对多页结果得出的结论应保持保守。

对于需要完整答案的任务，可以按可信度从高到低采用以下方法：

1. 请求服务提供快照、导出任务，或文档说明能够保持稳定视图的游标。
2. 将查询限制在不可变的时间范围内，并使用有文档说明的稳定排序。
3. 执行第二次扫描并比较标识符，然后报告任何不一致。
4. 请人工批准一个范围更窄、定义更明确的范围，而不是进行大范围修改。

不要把糟糕的接口变成破坏性工作流。代理仍然可以使用偏移页面进行抽样、定位特定对象或生成部分库存清单。但它不应把不稳定的偏移扫描当成已经找到所有匹配凭据、项目或用户的证明。

## 完成状态必须来自协议，而不是猜测

不同 API 会在不同位置表达分页状态。JSON 请求体可能包含 `next_cursor`、`has_more` 或下一页 URL。其他 API 使用 HTTP `Link` 标头。RFC 8288 定义了 Web Linking，以及用于表示 `next` 等关系的 `rel` 参数。这个标头提供的是关系，不保证响应体中一定存在熟悉的游标字段。

代理需要针对接口的完成规则。把规则写在请求定义旁边。例如：“当 `next_cursor` 为 null 时完成。”或者：“当不存在带有 `rel="next"` 的 Link 关系时完成。”不要写：“返回少于 100 条记录时完成。”这个捷径会在过滤页面、权限裁剪、服务上限，以及有意返回不均匀页面的 API 中失效。

典型的 Link 标头可能如下：

```http
Link: </v1/events?limit=100\u0026cursor=a6f3>; rel="next",
      </v1/events?limit=100\u0026cursor=first>; rel="first"
```

代理只能选择自己理解的关系。不能直接拼接整个标头，不能假定 `first` 链接就是可以安全重启的检查点，也不能从缺少 `last` 链接推断不存在最后一页。API 自己的文档决定分页契约，RFC 8288 只描述链接关系如何通过 HTTP 标头传递。

有些 API 会返回 `has_more: true`，同时页面为空。遇到权限过滤、并发删除或索引延迟时，这种情况并没有听起来那么荒谬。如果 API 文档说明了这种行为，只要预算允许，就应继续使用延续令牌，并记录这个空页面。如果文档没有说明，则应停止并标记分页响应不一致。因为 `has_more` 一直为 true 就无限继续，是编程错误，不是坚持不懈。

还要区分终止响应和成功的 HTTP 状态。`200 OK` 只说明这次请求成功，并不表示集合已经结束。`404` 可能表示游标过期、接口不匹配，或资源已经消失。应在运行记录中保留状态、响应体和最后一个游标，让人能够判断实际发生了哪一种情况。

## 部分结果需要边界声明

代理报告发现结果时，应像谨慎的操作人员记录事件一样，说明请求了什么、观察到什么，以及没有检查什么。多数糟糕的代理报告都在最后一部分失败。它们列出发现，却省略了导致结果不完整的停止游标、上限或错误。

使用人类无需重建会话就能采取行动的报告格式：

```text
Scope: GET /v1/projects?state=active\u0026sort=id
Requested page size: 50
Pages fetched: 10
Records received: 487
Completion: partial
Stop reason: page budget reached
Last continuation cursor: eyJvcmRlciI6ImlkIiwicG9zIjoiNTg3In0
Observed finding: 12 projects matched the review rule
Uninspected scope: records after the last continuation cursor
Action taken: none
```

最后一行很重要。发现运行应说明是否改变了任何内容。审查报告的人不应需要猜测代理只是列出对象，还是已经对对象采取了行动。

如果服务把游标视为 bearer 能力，或游标内容可能暴露账户结构，就不要在聊天记录中暴露敏感游标。将准确令牌保存在受保护的运行元数据中，然后在面向人的报告里使用指纹或经过编辑的前缀。代理仍需要足够状态来恢复或审计遍历，但人们不需要让延续令牌散落在工单和终端中。

这里的措辞会影响风险。“在检查的前 500 条记录中没有发现匹配项”是准确的。只有在稳定程度足够的视图下完成完整遍历后，“不存在匹配记录”才准确。听起来像吹毛求疵，但清理或合规决策可能正依赖这一区别。

## 速率限制和重试需要单独的停止规则

分页会放大速率限制错误，因为一次请求变成了循环。收到 `429 Too Many Requests` 时，代理不应拿同一个游标反复冲击接口。服务器提供 `Retry-After` 时应遵守它，并将等待时间计入运行截止时间，在重试预算用尽时停止。

对于超时或 `5xx` 响应等临时故障，应在前进之前重试同一页。如果 API 为列表调用支持幂等性或请求标识符，应按照文档使用。对于不确定的响应，不能仅仅因为请求可能已经成功，就推进到下一个游标。那会造成无声遗漏。

一个有边界的重试策略可以规定：

- 临时传输或服务器故障后，当前页面最多重试两次。
- 速率限制时遵守 `Retry-After`，前提是剩余截止时间允许等待。
- 如果授权状态没有变化，不要重试身份验证或授权失败。
- 遇到格式错误的分页数据、重复游标或文档未说明的延续响应时停止。

重复游标需要特别注意。如果第三页返回的 `next_cursor` 与代理提交的相同，继续下去可能造成无限循环。将每个新游标与已提交游标以及历史游标集合进行比较。除非服务文档说明存在极少见的预期重复情况，并且已经用服务专属规则处理，否则一旦重复就停止。

代理应保留足够的响应元数据来诊断重试，但不要保存秘密。可以记录状态码、请求路径、选定的非敏感标头、页面序号、游标指纹、时间戳，以及在策略允许时记录响应体摘要。不要因为请求失败，就把授权标头、完整 bearer 令牌或包含凭据的 URL 粘贴到日志中。

## 一个失败过程说明了为什么第一页很危险

假设代理收到任务，要禁用指定日期之前所有处于非活动状态的集成。集成接口默认每页返回 25 项，按最近更新时间排序，只有存在更多页面时才提供 `next_cursor`。代理获取第一页，发现三个非活动集成并将其禁用，然后报告已经清理了非活动集成。

这个报告有两个错误。代理没有检查所有集成，而且在发现阶段采取了行动。发现三个候选项，无法说明第二页及后续页面有什么。更糟的是，禁用对象会改变 `updated_at`，如果接口使用默认排序，就可能重新排列集合。代理让自己的遍历变得更不稳定。

更安全的运行会在 API 支持时使用明确过滤条件和稳定排序：

```http
GET /v1/integrations?status=inactive\u0026updated_before=2024-01-01\u0026limit=50\u0026sort=id HTTP/1.1
```

代理跨页收集 ID，不修改这些对象。只有服务器不再发出下一个游标，或发现预算被触发时，它才停止。随后报告完整候选集或部分候选集。单独的操作请求可以使用收集到的 ID，最好先让人工看到数量和范围。

如果列表接口不支持稳定排序，代理应明确说明这一点。它仍然可以收集候选项，但不应声称得到了完整且没有竞态的集合。“发现一个就处理一个”之所以显得高效，是因为省掉了第二次遍历。对于操作会改变排序位置、资格或权限的可变列表，这种做法是错误的。

同样的模式适用于安全发现、用户账户、部署记录和构建产物。先读取，确定范围，再修改。紧急控制也有例外，例如撤销一个明确点名的受损凭据，但那不是分页清理任务，而是针对已知标识符的定向操作。

## 将凭据和可观测性放在代理之外

代理不应仅仅为了分页就需要 API 密钥。执行 HTTP 请求的组件可以注入凭据，在需要时强制人工授权，并记录实际请求顺序。这样代理只需关注查询构造和结果解释，不必处理能够让它在其他地方执行无限调用的令牌。

对于使用 Sallyport 的团队，HTTP 调用可以通过其操作网关，凭据留在加密保险库中，Activity journal 会记录每次调用。这些记录有助于审查人员将代理声称的页面数与实际运行的请求进行比较，但它们不能替代代理指令中的页面预算。

让遍历策略靠近任务定义。明确允许的接口、过滤条件、字段、最大页面数、重试行为和必需的边界声明。通用授权层无法判断查找一个代码仓库是否一页后就完成，也无法判断合规库存是否需要所有页面。

Sallyport 的 session journal 和逐次调用的 activity trail 可以让代理运行偏离方向时的撤销和审查变得可行。不过，仍然必须告诉代理，在游标形状无法识别、预算用尽，或 API 响应与接口文档契约相矛盾时停止。事后记录一次错误爬取总比没有证据好，但它无法撤销不必要的调用或错误操作。

## 用有挑战性的分页样例测试遍历

正常路径分页会隐藏真正重要的缺陷。在信任代理工作流之前，应使用以下样例进行测试：第一页为空但带有游标、页面较短但还有结果、出现重复对象、游标重复、游标过期，以及两个正常页面之间出现速率限制响应。

预期行为应当具体而平淡。只有文档规定的延续状态表示应继续时，代理才会在空页面之后继续。它会根据任务的一致性要求，对重复 ID 去重或停止。游标过期后，它绝不会自行编造游标；安全限制结束运行时，它会报告覆盖范围不完整。

审查工具调用循环时，可以使用这张验收表：

| 样例 | 预期结果 |
| --- | --- |
| 12 条记录，没有下一个游标 | 一次请求后完成 |
| 12 条记录，存在下一个游标 | 尽管页面较短仍继续 |
| 同一个游标返回两次 | 停止并报告分页循环 |
| 带有 `Retry-After` 的 `429` | 只在截止时间允许时等待，并重试当前页面 |
| 游标被拒绝为已过期 | 只通过文档说明的检查点重启，否则报告不完整 |

除了最终文字，也要检查原始调用。经过润色的报告可能掩盖跳过某个游标，或在停止条件之后多发了一次请求。执行日志应显示一次初始请求、每次延续请求、对同一游标的任何重试，以及终止响应之后没有新的调用。

为探索性工作设置较小的默认预算，并要求通过明确的任务变更才能进行大范围枚举。一个限制就能避免最浪费时间的两种失败：代理无限扫描，以及代理悄悄把第一页误认为完整答案。
