阅读需 8 分钟

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

API 结果截断可能让代理执行不安全的后续变更。设计工具响应,明确暴露数据不完整、警告、限制和范围。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

看一下这个响应:

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

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

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

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

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

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

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

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

{
  "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 描述了一种重要的不完整来源,但不应成为万能字段。权限筛选不是截断,联邦搜索超时也不是分页。如果一个标志承载了所有含义,调用方就会失去安全恢复所需的原因。

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

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

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

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

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

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

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

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

{
  "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 的代理封装,并丢弃它在第十页停止这一事实。此时,封装层成了不完整性的来源。最外层工具契约必须负责公开这个信息。

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

中止错误的运行
调查偏离方向时,从 Sessions 日志中立即撤销代理运行。

搜索可能已经完成 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 则是工具可以强制执行的条件。

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

证据失效时停止操作
在 Mac 上重新打开保险库之前,锁定保险库并拒绝所有操作。

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

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

{
  "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,同时附带数据过时警告。这样的结果可能完整列出了索引中的每个项目,但仍不适合需要实时状态的变更。

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

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

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

隔离 SSH 清理命令
通过内置的 sp-ssh 辅助工具发送 SSH 命令,不要向代理暴露 SSH 密钥。

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

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

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

{
  "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 封装,然后在其最外层响应中加入明确的完整状态。先从空结果和有上限的搜索开始。正是在这些地方,自信的代理最容易制造出看起来干净、实际错误的答案。

常见问题

什么是部分 API 结果?

部分结果是指只覆盖了请求范围一部分的响应,例如一页记录、一个代码仓库目录、因超时而中止的搜索,或经过筛选的查询。当工具用与完整答案相同的形式呈现这个子集时,问题就出现了。代理会把未出现的内容当成不存在的证据。

空的 API 响应是否意味着不存在匹配记录?

不一定。空列表只表示服务器在实际搜索到的范围内没有返回任何项目。如果分页、时间限制、权限或失败的分片缩小了这个范围,工具必须单独说明。

工具响应不完整时,代理应该执行操作吗?

最安全的默认做法是:当完整性未知时,停止具有破坏性或范围过大的后续操作。如果工具契约明确允许,代理仍可以执行可撤销且范围很小的操作。不要让模型从响应中的一段说明文字自行推断风险规则。

API 应如何报告被截断的结果?

使用 completetruncatedwarningsnext_cursor 以及机器可读的 incomplete_reasons 等明确字段。每种成功响应都应包含这些字段,空结果也不例外。埋在文本摘要里的警告,代码和代理都很容易忽略。

分页能保证代理看到了所有记录吗?

只有当客户端沿着每个游标继续请求,直到 API 报告不存在下一页时,分页才能说明代理看到了全部记录。增大页面大小只能减少调用次数,不能证明结果完整。游标过期、查询参数变化和不稳定的排序,仍可能让扫描不可靠。

工具应如何处理带有部分数据的超时?

有时间限制的查询必须同时公开截止时间,以及哪些工作仍未完成。在截止时间前返回已找到的匹配项很有用,但把它们称为完整答案是不准确的。对于依赖未发现内容来执行变更的操作,代理应把超时视为前置条件未满足。

HTTP 200 是否足以说明 API 搜索已经完成?

不能。HTTP 200 表示服务器成功发送了这次 HTTP 响应,并不表示响应包含调用方所需的全部结果。应在响应正文或有文档说明的响应头中加入完整性元数据,并让所有端点保持一致的含义。

代理什么时候可以在搜索后执行变更?

可以,但前提是调用方能够证明自己检查了正确范围,并在稳定快照下收到了完整答案。例如,读取一条完整记录后移除过期标签,与在一次受限搜索后删除所有被认为未使用的账户,性质完全不同。操作必须与证据相匹配。

重试能解决不完整的 API 结果吗?

传输重试可以处理临时连接故障,却不能修复分页、查询限制、权限筛选或服务器提前停止工作造成的语义不完整。工具必须报告这些情况,调用方再决定是否重试,以及如何重试。

数据不完整时,人工审批应显示什么?

审批界面应显示拟执行的操作、受影响的目标,以及代理无法获得完整证据的原因。这样人工审核者可以选择缩小查询范围、补充缺失权限,或批准例外。笼统的审批提示会掩盖真正需要做出的决定。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov