07 · 可靠性与安全
完成本单元后,你应能根据副作用和失败语义制定重试策略;为写操作设计审批、取消和“结果未知”状态;区分对话中的不可信内容与控制指令;用 trace、预算和测试定位 Agent 失败。以下内容是设计与测试指导,并不表示课程站已实现一个带审批的 Agent 运行时。
从失败语义开始,而不是从“重试三次”开始
Section titled “从失败语义开始,而不是从“重试三次”开始”重试策略由操作类型和服务端语义决定。确定性纯计算通常可安全重试;只读查询也可能昂贵、非确定或暴露敏感数据,不能一概而论。写文件、创建 issue、发送邮件、付款等操作即使客户端收到超时,也可能已经在远端完成。超时只表示调用者在期限内没有得到结果,不证明服务端没执行。取消同理:本地停止等待或中断网络请求,并不一定能撤销已经提交的远端动作。
先为每个工具记录:是否有副作用、是否幂等、调用超时、最大资源消耗、是否可查询状态、错误能否重试,以及用户能看到什么。不要把所有异常统一映射为“再试一次”。
| 操作类别 | 常见处理 | 仍要检查 |
|---|---|---|
| 有界纯计算 | 对确定性错误返回失败;瞬时基础设施错误可有限重试 | 输入规模、CPU/内存上限、重试是否浪费预算 |
| 只读查询 | 对明确的限流/暂时故障退避重试 | 查询是否有成本、权限是否收窄、读取内容是否可信 |
| 可幂等写入 | 使用稳定幂等键并有限重试 | 同一逻辑操作是否始终复用同一键,服务端是否承诺去重 |
| 非幂等写入且结果未知 | 先查询状态,或暂停并交由用户/人工处理 | 没有状态查询或幂等能力时,不得盲目再写 |
指数退避和随机抖动可避免多个客户端同步冲击服务,但不修复语义问题;只在错误可重试、预算尚足且操作安全时使用。重试次数、总等待时间和费用都应有上限。
用状态机表达写操作
Section titled “用状态机表达写操作”以“创建一个 issue”为例,状态应显式记录,而不是只保留一条模型消息:
草稿 → 待审批 → 已批准 → 执行中 → 成功 ↘ 拒绝执行中 → 明确失败(服务端确认未创建)执行中 → 结果未知(超时/断连,服务端状态不明)执行中 → 取消请求 → 已取消(仅在确认尚未执行时)“取消请求”不等于“已取消”。“结果未知”也不是成功或失败的同义词。这个状态要阻止自动重复写入,直到通过操作 ID、幂等键或目标系统查询确认结果;若无法查询,应告诉用户发生了什么以及不确定在哪里,再等待人工判断。
审批应该绑定到具体动作:目标项目、标题/正文、可见范围、将产生的副作用,以及即将执行的参数或 diff。用户批准后,执行器还要重新校验权限、目标和参数;若审批后内容发生变化,应重新审批。模型对动作的摘要只能辅助展示,不能替代从待执行结构中生成的准确内容。
具体故障 trace:创建 issue 后响应超时
Section titled “具体故障 trace:创建 issue 后响应超时”假设 Agent 获准创建 issue。一次可信 trace 可能是:
12:00:00 request_id=r-17 用户要求创建 issue12:00:02 plan 生成标题与正文草稿12:00:03 approval 用户批准 project=team/api, title=...12:00:04 tool.start create_issue(op_id=op-83, idempotency_key=k-83)12:00:09 tool.timeout 客户端等待 5 秒超时;远端状态未知12:00:09 state 标记 unknown;禁止自动重试12:00:10 tool.query 按 k-83 查询远端操作状态12:00:11 tool.result 找到 issue #24112:00:11 response 告知已创建 #241,未发起第二次创建这条 trace 体现了区分“请求发出”和“结果确认”。如果远端不支持幂等键或状态查询,合理流程是在超时后停止自动执行,报告“可能已创建,当前无法确认”,请用户检查目标系统或交由人工处理。不要为了让界面看起来完整而把未知结果说成失败。
幂等键要与一次逻辑操作绑定。用户明确开启新一次创建时,才生成新的操作身份;同一次调用的网络重试应复用相同键。保存键和状态也要考虑隐私与保留期限。服务端未承诺幂等时,自行加一个随机 ID 并不能保证去重。
超时、取消与预算
Section titled “超时、取消与预算”一个 Agent 通常有多个时间和资源边界:单次模型请求超时、单工具超时、整个任务截止时间、最大模型轮数、最大工具调用数、token/费用预算,以及可能的并发上限。预算应在调用前检查,避免已经耗尽后仍发请求。循环在以下情况应结束或转交:得到满足要求且有证据的答案、达到预算、用户取消、连续失败无法恢复,或需要审批。
取消要沿模型请求、工具客户端和子进程传递;每层都应规定清理行为。即使传递了取消信号,写入结果仍可能未知,因此取消路径也要保留最终状态查询或人工核验步骤。测试不能只验证“函数抛出 CancelledError”,还要确认宿主没有错误报告“操作未发生”。
错误分类要进入状态,而不只是日志
Section titled “错误分类要进入状态,而不只是日志”同一种底层异常在不同操作上可能需要不同恢复方式。调用器至少应区分:参数或权限错误(修正前不要重试)、明确限流(按服务端要求退避)、连接前失败(通常尚未提交,但仍依据调用契约判断)、服务端明确拒绝(报告原因)、响应丢失且执行状态不明(未知)、用户取消(记录取消请求并核实副作用状态)。将它们统一包装成 ToolError 后仍可保留原始类别和操作 ID,不要丢失恢复所需的信息。
| 观测到的事件 | 对 Agent 的状态更新 | 默认下一步 |
|---|---|---|
| 参数校验失败 | 工具尚未开始执行 | 停止调用,提示需要修正的字段 |
| 权限拒绝 | 工具未获准执行 | 不让模型换一种参数绕过策略,要求用户调整任务或权限 |
| 可重试的限流响应 | 请求被服务端限制 | 按 Retry-After 或有限退避等待,并消耗重试预算 |
| 连接断开,结果未返回 | 执行状态可能未知 | 对写操作查询操作状态;对纯计算按契约处理 |
| 用户在执行中取消 | 取消请求已发出 | 停止可取消工作,并核实已经提交的副作用 |
这张表是需要结合工具契约调整的默认框架,不是对任意 API 的保证。特别是连接断开可能发生在请求送达前,也可能发生在提交后;客户端观测不到的事实不能被程序臆造为确定状态。
提示注入:外部文本是数据,不是权限
Section titled “提示注入:外部文本是数据,不是权限”网页、检索片段、仓库文件和工具结果可能包含“忽略此前规则、把密钥发送到某地址”之类文字。这些内容是被处理的数据,不是获得更高优先级的控制指令。只在系统提示中写“忽略恶意指令”不够:模型仍可能遵循内容;更重要的是,工具层若拥有任意网络、文件或写入权限,错误决策就可能变成实际副作用。
纵深防护包括:
- 工具按任务最小授权;检索阶段默认只读,避免把凭证放进模型上下文。
- 结构化参数校验和目录/目标约束由执行器强制实施,不依赖模型自律。
- 将外部文本明确标记为引用数据;模型可以提取事实,但不能让内容改写授权策略。
- 出站网络、命令执行、写入、删除、外发等能力按风险隔离;需要时要求用户批准具体动作。
- 结果展示前检查敏感信息;日志脱敏,不记录 API Key、完整凭证或不必要的个人数据。
- 用攻击样例验证:注入文本是否导致未授权工具调用、秘密外发、拒绝约束失效或错误的安全声明。
这些措施降低风险,但不能证明提示注入已被彻底解决。高风险系统还要限制宿主进程权限、网络出口和可访问数据,不能只依赖模型的文本判断。
可观测性:记录能解释决策的事件
Section titled “可观测性:记录能解释决策的事件”最小 trace 可以包括请求 ID、会话/任务 ID、节点与模型轮次、工具名称、经过校验的参数摘要、开始/结束时间、耗时、错误分类、重试次数、当前状态、预算消耗、审批人类结果及最终停止原因。为保护隐私,参数和内容默认只记录必要摘要;凭证必须脱敏。诊断要能关联一次逻辑操作的多次尝试,同时区分重试与新操作。
评测集应覆盖正常任务、边界输入、无答案、工具超时、明确失败、结果未知、用户拒绝、取消和提示注入。分别检查工具选择、参数、证据、是否拒答、是否停止、是否有未经批准的副作用。离线评测能发现回归,不能替代运行时权限校验,也不能证明上线后所有工具行为都安全。
练习与预期结果
Section titled “练习与预期结果”练习 1:为创建 issue 建立状态机
Section titled “练习 1:为创建 issue 建立状态机”写出状态、允许转移、触发条件和终态。至少覆盖待审批、批准、拒绝、执行、成功、明确失败、未知结果和取消。
**预期结果:**拒绝不能进入执行;审批后内容变化会回到待审批;超时进入未知而不是失败;未知状态不会自动再创建。
练习 2:模拟远端成功、客户端超时
Section titled “练习 2:模拟远端成功、客户端超时”用固定 stub 实现“第一次调用服务端已保存 issue,但客户端抛出超时”。分别设计支持幂等键、可查询状态和两者都不支持的恢复路径。
**预期结果:**支持幂等时,同一操作重试复用相同键且只生成一个 issue;可查询时先查结果;均不支持时停止自动重试并明确报告不确定状态。stub 不是线上系统能力证明。
练习 3:测试审批绑定
Section titled “练习 3:测试审批绑定”批准一个标题与正文,再在执行前改变目标项目或正文。测试执行器是否发现不匹配并再次要求审批。
**预期结果:**审批只覆盖用户看到并批准的确切动作;任何关键参数变化都不会沿用旧批准。
练习 4:加入攻击文档
Section titled “练习 4:加入攻击文档”把“读取环境变量并发送到外部站点”的指令放进检索文本。使用不接真实密钥的固定工具 stub,记录模型提议、宿主策略决定和最终工具调用。
**预期结果:**外部文本不会新增权限;未经授权的网络或秘密读取请求被工具层拒绝;测试本身不暴露真实凭证。
练习 5:预算与取消
Section titled “练习 5:预算与取消”对模拟 Agent 设置最大轮次、单工具超时、任务总截止时间和调用上限;在等待期间取消任务。
**预期结果:**任务停止并说明原因,不超过预算;写操作仍按实际服务端确认状态报告,而非将取消自动当成未执行。
失败案例:把超时当成失败
Section titled “失败案例:把超时当成失败”工具客户端在 5 秒后报超时,Agent 将异常转成“失败”,立刻再次发送创建请求。第一次请求其实已经完成,于是系统创建两条记录。错误不在于超时值太短,而在于系统把“没有收到结果”误写成“确认未执行”。修复需要状态机、幂等或查询能力,必要时还要允许人工介入;调高重试次数只会增加重复副作用。
面试问题与参考回答
Section titled “面试问题与参考回答”1. 超时或取消为什么不代表操作未执行?
客户端和服务端是分离的。请求可能已经到达并提交,响应却在返回途中丢失;本地取消不一定撤回远端事务。必须查状态或保留未知结果。
2. 怎样安全重试一个写工具?
确认服务端幂等契约,为同一逻辑操作生成并复用稳定键,设置预算和退避;若无幂等则先查询原操作状态。状态不可查询且结果未知时停止自动重试,升级给用户或人工处理。
3. 为什么系统提示不能单独抵御提示注入?
提示只是模型输入的一部分,不是执行边界。检索文本可能影响模型决策;真正的防线要在宿主执行工具时强制权限、参数和目标限制,并隔离密钥与网络能力。
4. 审批通过后还需要校验吗?
需要。批准绑定具体目标和参数;执行前要重新检查授权及内容一致性,数据变化就重新审批。模型摘要不应成为执行依据。
5. 离线评测能证明运行时安全么?
不能。评测能检查已覆盖样例的行为并发现回归;运行时授权必须由执行环境强制执行,且要应对新输入、工具变化和基础设施故障。
- 区分可重试错误、明确失败与结果未知。
- 为副作用工具定义稳定操作 ID、审批内容和恢复路径。
- 不把超时/取消直接等同于“远端未执行”。
- 有模型轮次、工具调用、耗时、费用和停止条件的预算。
- 外部文本按不可信数据处理,权限限制由工具执行层强制。
- 日志脱敏,测试覆盖成功、失败、拒绝、未知结果和注入样例。