通过 API 响应最小化,让 AI 代理上下文更安全
API 响应最小化通过精简响应契约,将不必要的个人、财务和运营数据排除在 AI 代理上下文之外。

AI 代理不需要复制它们接触到的每个对象。它们只需要足够的信息来作出下一步决定、执行操作并报告结果。当 API 将完整的客户记录、发票、工单、代码库设置或事故对象返回给只需要 ID 和状态的代理时,API 已经扩大了数据暴露问题的范围。
这个问题很容易被忽略,因为请求可能是只读的,经过身份验证,并通过 TLS 发送。但这些条件都不会改变接下来发生的事情。响应可能进入代理记录、工具追踪、模型请求、本地缓存、错误报告或人工审核队列。只要代理能读到它,就应该假定它已经进入代理上下文。
实际的解决办法是 API 响应最小化:为每个代理任务定义最小的有用响应,让这种响应易于请求,并把广泛数据访问设为需要人工审查的例外路径。这不是为了让 JSON 看起来更漂亮,而是为了减少个人、财务和运营数据在一次普通自动调用之后出现在其他位置的机会。
经过身份验证的读取仍可能暴露过多信息
读取权限可以限制写入,但不能限制复制、摘要、引用,或意外地把数据发送给其他工具。团队常常把代理称为“只读”,仿佛这样就解决了风险。它只解决了一类风险。
设想一个代理负责找出逾期发票并创建后续任务。它需要发票 ID、账户 ID、到期日、金额、货币和催收状态。传统的发票接口还可能返回账单地址和收货地址、税务标识符、支付处理商引用、明细描述、财务用户的内部备注,以及完整的支付历史。每个额外字段都会增加一条代理可能重复说出的信息,即使任务根本不需要它。
运营数据也有同样的问题。检查部署是否完成的任务可能需要服务名称、构建标识符、状态和失败类别。它很少需要一份包含主机名、内部地址、命令输出、事故评论或无关配置的完整环境导出。
人们经常混淆的区别很重要:授权回答调用方是否可以访问某个资源;最小化回答针对这项具体工作,调用方应该收到该资源的多少内容。一个拥有读取 invoice:123 权限的令牌,可能完全通过授权检查,却仍然收到不安全的发票表示。
IETF 的 RFC 9110 将表示描述为用于反映资源当前状态或期望状态的信息。它并没有要求每个资源只能有一种最大化表示。这为设计留下了空间。只要 API 对每种契约都有清晰说明,一个资源就可以提供摘要表示、运营表示和财务表示。
不要依赖“忽略个人数据”这样的提示词指令。提示词会影响行为,响应结构才控制暴露。如果接口发送了家庭住址,代理在决定是否忽略它之前就已经收到了它。
从代理必须完成的工作开始
安全的响应契约应该从代理必须作出的决定开始,而不是从现有数据库模型开始。用一句话写下任务,然后列出会改变操作的事实。其他内容都必须说明为什么应该存在。
例如,重试失败构建任务的代理可能只需要下面的响应:
{
"job_id": "job_4821",
"state": "failed",
"retryable": true,
"failure_class": "transient_dependency",
"attempts_remaining": 1
}
它不需要完整构建日志来判断是否允许重试。如果之后有人需要诊断信息,可以提供单独的接口,并设置更小的受众范围,同时要求明确说明获取理由。日志接口也应该支持有边界的范围,因为完整日志中出现令牌、客户输入、路径和配置片段的频率,往往比人们承认的更高。
在修改接口之前,先建立一张小型任务矩阵。它会迫使团队讨论那些原本容易含糊带过的问题:
| 代理任务 | 决策字段 | 操作字段 | 默认排除的字段 |
|---|---|---|---|
| 创建支持后续任务 | 工单 ID、优先级、类别 | 账户 ID、分派队列 | 消息正文、附件、内部备注 |
| 重试任务 | 任务 ID、状态、是否可重试 | 重试令牌或任务 ID | 完整日志、环境变量 |
| 标记逾期发票 | 发票 ID、到期日、金额、状态 | 账户 ID | 地址、税务数据、支付引用 |
| 检查服务健康状况 | 服务 ID、状态、错误类别 | 事故 ID | 主机详情、原始诊断信息 |
只有在某个字段会改变代理采取的分支、出现在操作请求中,或必须出现在面向用户的报告中时,它才属于响应。“以后可能有用”不是充分理由。正是这句话让列表接口不断增加字段,最后没人知道谁依赖其中任何一个字段。
这个练习还会暴露出一些应该计算而不是披露的字段。代理不需要读取工资记录来判断费用审批是否需要经理批准。返回 approval_required: true 即可。它不需要查看所有权限来判断部署能否继续。返回 deployment_permitted: false 和稳定的原因代码即可。
这不是通过隐蔽性来实现安全,而是通过有意设计的 API 契约,让调用方得到所需结果,同时不把底层记录交给它。
默认对象应该是摘要,而不是数据库行
最可靠的设计是让普通列表和查询调用默认返回安全摘要。详细表示应当明确请求、单独授权,并且很少使用。如果要求每个调用方都记住一个限制性查询选项,这种设计最终会失败,尤其是某个库添加了省略该选项的便捷方法之后。
客户摘要可以是这样:
{
"id": "cus_7f31",
"display_name": "Northwind Parts",
"account_state": "active",
"open_invoice_count": 2,
"support_tier": "standard"
}
不要因为客户行中恰好有 email、phone、街道地址、税务标识符、支付工具元数据或自由文本备注,就把它们返回。某些字段可能是计费应用所必需的,但它们不属于运营代理使用的摘要契约。
有两种可行模式。单独的摘要接口,例如 GET /customers/{id}/summary,直接明了,也容易审计。投影参数,例如 GET /customers/{id}?view=summary,在视图集合固定且有文档说明时也可以使用。这两种方式都好过一个返回所有内容、再要求每个客户端自行忽略无用字段的接口。
不要为面向代理的凭据提供通用的 expand=* 或 include=all 开关。它会在调试时变成最省事的路径,之后因为移除它感觉有风险,就一直留在生产环境中。如果确实需要详细表示,应以任务命名,例如 view=collections、view=deployment_status 或 view=case_triage。任务名称会促使团队进行设计审查,“全部”则不会。
有人会反对说,单独的视图会重复代码。它们确实会重复一些映射代码,但这点成本远小于调查工具记录中为何出现税号或内部事故备注。映射层也是记录数据归属、测试代理视图是否排除敏感列的地方。
字段选择必须使用允许列表,而不是解析器技巧
fields 参数可以有效缩减响应,但前提是服务器将它当作严格的允许列表。宽松的解析器会把便利功能变成数据提取接口。
下面的请求是合理的:
GET /v1/invoices?state=overdue\u0026fields=id,account_id,due_date,amount,currency,collection_state\u0026limit=25
服务器应该只返回该接口和凭据允许的字段。如果调用方请求 billing_address 或 payment_reference,就应以清晰的错误拒绝请求。不要静默添加敏感字段,也不要接受 customer.* 这样的任意嵌套路径。
响应契约可以精确说明行为:
{
"error": {
"code": "unsupported_field",
"message": "Field 'payment_reference' is not available in the agent invoice view",
"allowed_fields": [
"id",
"account_id",
"due_date",
"amount",
"currency",
"collection_state"
]
}
}
错误本身也需要受到约束。绝不要包含被拒绝字段的值、附近记录数据、堆栈跟踪、来自其他服务的原始查询文本或数据库错误。错误正文经常意外变成第二个 API,尤其是在工程师为了加快事故处理而让错误信息变得过于详细时。
GraphQL 也需要同样仔细地审查。人们会认为客户端只能请求自己写出的字段,这确实有所帮助,但模式仍可能暴露敏感字段,嵌套关系可能使记录数量成倍增加,别名也可能让单个查询难以分析。设置深度和复杂度限制,在适合当前环境的情况下禁用或限制内省,并对字段而不只是顶层对象进行授权。更重要的是,为少数经过批准的任务创建代理专用模式或持久化查询。宽泛的模式加上一条礼貌的指令,并不能构成窄接口。
OWASP API Security Top 10 指出了对象属性级授权失效问题。它通常被描述为调用方读取了本不应访问的属性。代理场景还增加了另一种失败模式:调用方在技术上可能有权访问该属性,但任务并不需要它,也不应将它分发到模型上下文中。两项检查都要保留。先问“这个凭据可以读取它吗?”,再问“这项任务现在为什么需要它?”
分页控制数量,但筛选控制相关性
返回十条记录并不自动意味着响应很小。如果每条记录都包含大型嵌套对象或很长的文本字段,分页只是把泄露整齐地分成几页。
使用能够表达代理工作内容的筛选条件。催收代理应该查询处于指定状态和日期范围内的逾期发票,而不是列出所有发票后再在本地判断哪些相关。部署代理应该请求一个服务和当前版本,而不是查询每个环境后再从结果中搜索。
游标分页也需要谨慎设计响应结构。游标应该是不透明的,不应包含电子邮件地址、账户名称、未加密的筛选值,或暴露排序方式的内部数据库键。客户端会把游标放进日志和工单中,所以要把它视为会随数据传播的内容。
为代理凭据设置保守的页面上限。较小的限制不只是减少令牌使用量,还会提供一个暂停点,让代理先查看摘要,选择相关记录,再进行有针对性的后续调用。相比之下,仅仅因为任务以“调查这个客户”开头,就加载完整账户历史,风险更高。
不要把搜索接口与返回所有匹配详情的权限混为一谈。搜索通常应该返回结果卡片:稳定 ID、标签、状态,也可以加上匹配原因。调用方选定记录后,再获取有权限的详细视图。这种两次调用的模式不如大型结果对象方便,但能让敏感信息的传输变得可见且可审查。
自由文本和嵌套记录需要单独的边界
结构化字段比人类书写的文本更容易分类。自由文本字段会吸收姓名、电话号码、误粘贴的凭据、指控、健康信息、法律建议和内部意见。工单的 description 在模式审查中看起来可能无害,直到有人阅读一整周的真实工单。
对于自主工作流,应默认将评论、备注、描述、附件、日志和消息正文视为敏感内容。如果只需这些内容中的类别、简短的服务器生成分类或数量来选择操作,就返回这些摘要。例如,代理可能只需要 has_customer_reply: true 和 latest_message_at,而不是消息正文。
不要在检索任意文本后再要求模型进行脱敏。这种方法很受欢迎,因为它看起来可以保留一个宽泛的统一接口,但它会以两种方式失败。第一,原始内容在脱敏之前就已经进入代理上下文。第二,模型生成的脱敏结果具有概率性,部分姓名、账号或引用可能会留下。
如果任务确实需要文本,就为请求设置硬性边界。按 ID 获取一条消息,而不是完整线程。请求服务器强制执行的字符数上限。除非用户批准这次特定检索,否则不要返回附件内容。截断时要明确告诉客户端会收到什么,例如 content_truncated: true,这样代理不会臆造缺失的细节。
嵌套数据会造成更隐蔽的同类问题。包含 customer、contacts、invoices、payments 和 events 的响应,在应用代码中看起来可能只是一个对象,但从暴露角度看,它其实是一组相互独立的数据集。要求每种关系使用单独接口,或为每种关系提供明确且受允许列表控制的展开方式。然后测试最糟糕的普通查询,而不只是返回一条稀疏记录的顺利路径。
错误处理和可观测性可能重新制造泄露
团队常常先缩小成功响应,然后又把原始负载复制到调试日志、追踪属性、重试队列和异常报告中。数据只是换了位置,并没有减少暴露。
检查完整的调用路径。至少要查看代理工具封装、HTTP 客户端调试模式、请求记录器、分布式追踪配置、错误报告服务、任务队列、本地会话存储和支持工作流。那些声称“只记录元数据”的位置,应当直接测试,而不是凭信任通过。
在非生产环境中运行一条金丝雀记录。为绝不能进入代理上下文的字段填入醒目的虚假值,例如 CANARY_BILLING_ADDRESS_927 和 CANARY_INTERNAL_NOTE_927。执行真实的代理任务,然后在所有允许使用的日志和追踪存储中搜索这些字符串。对失败请求、超时和格式错误的响应也重复测试。只测试成功路径,会漏掉大多数意外的负载捕获。
有用的调用日志应该记录操作,而不是复制内容:
{
"time": "2025-03-08T14:03:12Z",
"caller": "release-agent",
"operation": "GET /v1/jobs/{id}/retry-status",
"resource_id": "job_4821",
"response_view": "retry_status",
"field_set": ["job_id", "state", "retryable", "failure_class"],
"result_count": 1,
"outcome": "200"
}
只有在你自己的保留和访问规则允许的情况下,才记录标识符。在风险更高的系统中,可以改用带密钥的引用或短期关联 ID。如果原始值来自范围很小且容易猜测的集合,哈希本身也可能泄露信息,因此不要不加分析就把哈希称为脱敏。
在服务器边界清理对外错误消息。数据库驱动可能暴露失败的 SQL 片段,上游服务可能在错误封装中发送完整记录。你的 API 应将这些失败映射为稳定的公开代码,把详细诊断保存在受限存储中,并默认不在面向代理的错误中包含响应正文。
在代理网关中分离能力与披露
操作网关应该持有凭据并执行请求,但不能把该凭据可获得的任何响应都视为适合进入代理上下文。密钥隔离和响应最小化解决的是同一次调用中的不同问题。
Sallyport 会将 API 和 SSH 密钥保存在加密保险库中,并把操作结果返回给代理,而不是暴露密钥本身。这保护了凭据,但 API 所有者仍需判断结果中是否包含不必要的账户记录、命令输出或运营细节。
尽可能为每项代理任务提供命名请求模板。模板固定方法、主机、路径结构、允许的查询字段、页面上限和接受的响应视图。发布状态模板可以允许一个服务 ID,并返回简短的状态对象。它不应因为传递任意 URL 和任意 fields 表达式很容易,就接受这两者。
这正是宽泛代理思路会造成问题的地方。通用 HTTP 转发器在开发阶段可能有用,但它无法表达“检查这次部署”和“下载所有构建日志”之间的区别。应将意图放进可调用的操作中。当新任务需要更多数据时,要求修改 API 或新增模板。这个摩擦正是目的所在:必须有人说明为什么额外数据必须进入上下文。
对于例外情况,人工审批仍然有作用。如果代理需要一条支持消息的内容来解决工单,人员可以在看到目标和范围后,批准这一次具体调用。审批不应成为窄响应的常规替代品。人们在事故期间尤其容易快速批准熟悉的卡片,反复审批会让他们逐渐停止阅读。
将字段缺失作为契约的一部分进行测试
大多数 API 测试会断言预期字段存在。面向代理的 API 还需要测试禁止字段不存在,包括代码走备用路径时也一样。
为每个响应视图保留一项拒绝列表测试。使用真实的字段名,包括嵌套关系和自由文本。如果序列化过程后来通过 ORM 默认值、共享 DTO 或预加载关系添加了字段,测试就应该失败。
forbidden = {
"email",
"phone",
"billing_address",
"tax_id",
"payment_reference",
"internal_note",
"attachments",
}
body = get_invoice_agent_view("inv_1042")
assert forbidden.isdisjoint(body.keys())
assert "customer" not in body
assert "events" not in body
这个简单测试只能捕获顶层字段。还要增加遍历整个 JSON 树的序列化测试,并分别测试列表、搜索、错误和导出接口。最严重的泄露往往来自集合响应,因为有人为了少写几行代码,让它复用了完整详情序列化器。
契约测试还应断言响应大小边界。严格的字节上限不适合所有对象,但合理的上限可以在开发者添加无界文本字段或关系时发出提醒。测试数据中要包含长备注和许多子记录,否则测试会带来虚假的安全感。
审查变更时,直接问三个问题:哪个代理任务需要这个字段?哪个响应视图包含它?什么测试证明它在其他地方始终缺失?如果作者无法回答,就不要把字段合并到可被广泛调用的接口中。
让特殊详情检索可见且临时
有些工作确实需要敏感详情。欺诈审查、账户恢复、安全调查和复杂支持案件不可能完全依靠摘要完成。解决办法不是假装不存在这种需求,而是让详情检索明确、短期有效,并限制在确切记录范围内。
使用单独的接口或操作,接收稳定记录 ID 和声明的用途。只返回完成任务所需的最小切片,例如一项有争议的支付字段或一条选定的客户消息。不要因为同一个案件中存在一笔有争议的扣款,就授予完整账户导出的权限。
对于风险更高的检索,要求人员批准这一次具体调用,并记录调用方、用途、视图、记录引用和结果。将审计记录与敏感响应正文分开保存。你需要知道检索发生过,但不应因此又创建一份随手可得的信息副本。
成熟的 API 会让安全路径最容易使用。摘要视图应有清晰的名称、完善的文档和稳定的字段。广泛的详情接口则应让人明确感受到它承担了更多责任。如果代理反复需要某个敏感字段,不要把例外变成常态。重新审视任务设计,看看服务器端决定或经过脱敏的派生值是否已经足够。
第一次有价值的审计通常应从列表接口开始,而不是从大家已经害怕的接口开始。记录一个真实的代理任务,标出它使用的每个字段,再将这份列表与它收到的响应进行比较。未使用的部分,就是下一项 API 变更的起点。
常见问题
如何判断 AI 代理真正需要哪些 API 字段?
代理只需要选择并执行下一步操作所必需的信息。返回标识符、状态以及支持该操作的少量业务字段;只有在有明确理由时,才通过单独调用获取敏感详情。要把上下文窗口视为数据分发渠道,因为响应一旦进入其中,它就确实成了分发渠道。
分页足以保护敏感 API 响应吗?
分页可以限制数据量,但无法判断每条返回记录是否包含不该出现的字段。十条记录的一页仍可能暴露地址、支付引用或内部备注。应同时使用分页和字段选择。
应该从现有 API 接口中删除敏感字段吗?
通常不应直接修改。通用读取接口往往有太多调用方,也会被未来的软件继续使用,缩减字段可能破坏合法应用。可以新增面向具体任务的投影,或增加可选的 fields 参数,然后有计划地迁移代理调用方。
只读 API 凭据对自主代理安全吗?
只读令牌仍然可以让数据离开原系统,进入提示词、日志、会话记录以及模型提供商的处理流程。读取权限限制的是修改,不是披露。应将读取范围限制在能够完成任务的最小资源和投影上。
如何安全设计 fields 参数?
使用明确的允许列表,拒绝未知字段名;客户端省略参数时,返回有文档说明的默认投影。不要接受任意对象路径或未经严格验证的通用 include 表达式。响应结构必须足够稳定,便于审查和测试。
可以向 AI 编程代理暴露内部备注吗?
内部批注往往包含最有风险的信息,例如事故细节、客户投诉、风险标记、升级评论以及复制来的消息。应将它们标记为仅限员工使用的字段,排除在代理投影之外,除非某个边界清晰的工作流确实需要它们。
代理调用敏感 API 时应该记录什么?
需要记录请求路径、调用方身份、响应投影、结果大小,以及特殊访问是否经过审批。没必要把每个敏感值复制到可观测性系统中。元数据可以证明控制措施,同时避免重新制造数据泄露。
模型提供商会保留放入代理上下文的 API 数据吗?
许多服务商会根据套餐和配置,以不同条款保留提示词或输入;你自己的代理运行器也可能保留会话记录。不要根据对数据保留方式的假设作出隐私承诺。在这些条款真正产生影响之前,就先阻止不必要的数据进入上下文。
应该先审计哪些响应过于宽泛的接口?
先审计返回列表、搜索结果、账户对象、发票、工单、导出数据和错误负载的接口。将完整响应与代理最终操作实际使用的字段进行比较。包含复制备注或嵌套关联记录的大型对象,通常最容易带来明显的缩减空间。
隔离凭据能解决 API 响应数据暴露吗?
让凭据留在代理之外,同时保持响应内容精简。凭据网关可以阻止密钥泄露,但代理收到过大的响应后,网关无法让这些内容重新变得安全。两种控制措施都需要。