智能体授权过期后的恢复设计
文章将 OAuth 失效视为需要显式处理的工作流状态,区分令牌刷新、用户重新授权与任务恢复。提出避免无限重试、暴露凭据和重复执行副作用的通用恢复思路。
AI 深度解读
AI Agent 把任务规划得再好,也可能卡在账号授权过期这一步。真正危险的不是一次 API 调用失败,而是它把失效授权当成普通报错,反复重试、消耗预算,甚至悄悄换个账号继续操作。
作者给出的结论很明确:OAuth 恢复应该由模型之下的连接管理和工作流系统负责。模型可以解释任务为什么暂停,但凭证刷新、重新授权、任务恢复都需要明确的状态与规则。这套方案值得看的地方,是把「重新登录」拆成了可控的工程流程。
连接失效以后,先判断该重试还是该找用户
access token 有效期较短,通常可以通过 refresh token 换取新的访问凭证。但用户撤销授权、管理员调整策略,或者服务端返回 invalid_grant,都可能让原来的授权彻底失效。
这些情况不能统一塞进 tool_failed,再交给 Agent 猜下一步。
作者建议把连接与任务分开保存。任务记录用户意图和执行进度,连接记录授权健康状态,通过连接 ID 关联。连接状态包括 active、refreshing、reauth_required、provider_unavailable 和 disabled,分别对应可用、刷新中、需要重新授权、服务商不可用和已禁用。
错误分类决定恢复方式:
- 超时或
5xx可以进行有上限的重试。 invalid_grant、无效 refresh token 或用户撤销授权,应转入reauth_required。- 缺少 scope,需要重新连接,必要时重新征求授权。
- 资源 API 返回
403,可能是账号缺少服务商侧权限,重新登录未必有用。
恢复授权是确定性的系统职责,不该成为模型的自由发挥。
连接还必须绑定租户和预期账号。多账号产品里,「随便找个可用日历」不是可靠兜底。重新授权时,要让用户确认服务商、账号和 scopes,再决定是否更新原来的持久连接 ID。
刷新凭证为什么还需要一把锁
两个请求同时发现 token 过期,就可能一起刷新。如果服务商会轮换 refresh token,其中一个请求成功后,另一个拿着旧凭证刷新便可能失败。处理不当,正常连接也会被误判为失效。
作者用数据库租约选出唯一的刷新者,其余请求等待结果。这段 TypeScript 伪代码展示了核心流程:
async function getUsableConnection(id: string) {
const connection = await db.connection.findUniqueOrThrow({ where: { id } });
if (connection.state === "disabled") return { kind: "blocked", reason: "disabled" };
if (connection.state === "reauth_required") return { kind: "reauth", reason: connection.reauthReason };
const claimed = await db.connection.updateMany({
where: { id, state: "active", refreshLeaseUntil: { lt: new Date() } },
data: { state: "refreshing", refreshLeaseUntil: plusSeconds(20), version: { increment: 1 } }
});
if (claimed.count === 0) return waitForConnectionResolution(id, 2500);
try {
const secret = await vault.read(connection.credentialRef);
const refreshed = await oauth.refresh(secret.refreshToken);
await vault.rotate(connection.credentialRef, refreshed);
await db.connection.update({ where: { id }, data: { state: "active", lastVerifiedAt: new Date(), refreshLeaseUntil: null } });
return { kind: "ready" };
} catch (error) {
return classifyAndPersistRefreshFailure(id, error);
}
}
重点看 updateMany 的条件更新:抢占必须是原子的。这里设置了 20 秒租约,未抢到的请求进入参数为 2500 的等待函数。它们是示例配置,原文没有给出适用不同服务商的测量依据。
凭证保存在 vault 中,数据库只存 credentialRef 和状态元数据;凭证轮换也要保证原子性。遇到终止性刷新失败,只记录一次 reauth_required,后续排队任务读取这个状态,避免每个任务再失败一遍。
不过,这段代码不能直接当生产实现:它没有展示有效 token 的直接返回路径,也没交代 refreshLeaseUntil 为 null 时如何再次抢占,以及刷新者崩溃后如何接管 refreshing 状态。数据库和 vault 之间的部分失败如何恢复,同样需要补齐。
用户重新连接后,旧任务还该不该执行
重新授权只证明连接恢复了,不证明旧任务仍然合适。
作者要求在外部写入前、展示重连界面前保存检查点。示例任务 run_84a 等待连接 conn_29,状态为 waiting_for_connection,需要的能力是 crm.contacts.update,停在 validated_contact_change。检查点还记录 intent_hash、最低连接版本 18,以及时间 2026-11-04T10:15:00Z。
intent_hash 用来帮助发现用户离开期间的意图变化,但不能代替业务检查。恢复时仍需读取当前记录、比较相关字段、重新执行策略判断。原文没有说明哈希具体包含哪些字段,落地时要自行定义。
另一类问题更棘手:请求超时了,但写入可能已经成功。此时直接重放,可能重复修改或发送。应先用幂等键或操作回执确认结果。
重连界面也要交代清楚:哪个服务商、哪个安全展示的账号、哪项能力受阻,以及任务尚未开始、已安全暂停,还是需要核验结果。重连 URL 由服务端生成,短期有效,并绑定租户、连接和 CSRF/session 状态;授权码在后端兑换,凭证进入 vault。模型不接触授权 URL、授权码或 refresh token,只收到类似 connection_version: 19 和允许能力的信息。
恢复后可以发布 connection.restored,但调度器不能因此唤醒全部历史写任务。原文用下面的条件做第一层筛选:
function mayResume(run: Run, connection: Connection) {
return run.state === "waiting_for_connection" &&
run.connectionId === connection.id &&
connection.state === "active" &&
connection.version >= run.resumeAfterConnectionVersion &&
run.tenantId === connection.tenantId;
}
注意 connection.version >= run.resumeAfterConnectionVersion 只是恢复门槛。账号、scopes、目标数据、限流和审批仍要重新检查。切换账号或扩大 scopes 后,旧审批不能自动沿用。
连接恢复,默认应该让任务进入待复核状态,而不是立即补执行积压的写操作。
我的判断
这篇最有价值的地方,是把普通的 OAuth 异常放回了 Agent 的执行语境:失败后不仅要恢复连接,还要确认原来的动作是否仍有权执行、是否已经执行过。
和「遇到 401 就刷新一次再重试」相比,这套设计能覆盖撤销授权、并发刷新、多账号和长时间暂停。单纯的刷新拦截器无法独立解决这些问题。不过,已有成熟 OAuth SDK、工作流引擎或连接管理服务的团队,可以复用其能力,不必从零搭齐所有组件。
原文是一套设计建议,没有提供线上故障率、恢复耗时或对照实验。「更可靠」有清楚的机制依据,但不能写成经过量化验证的收益。20 秒租约和等待参数也只是示例。
还要分清凭证保密与执行正确性:token 没进模型上下文,不代表任务就不会重复执行。连接版本校验、幂等处理和业务复核,分别解决不同问题,不能互相替代。
能拿来做什么
- 给 Node 工具调用层增加结构化授权错误,将可重试故障、需要用户重连和权限不足分别返回;终止性失败应阻断依赖该连接的后续调用。
- 给前端重连卡片展示账号、受阻能力和任务进度。无法确认写入结果时明确展示「需要核验」,避免直接宣称操作未发生。
- 给暂停任务保存租户、连接 ID、授权版本和操作意图;恢复写入前重新读取目标数据,并通过幂等键或操作回执排除重复执行。
- 为 OAuth 集成补上并发刷新、
invalid_grant、更换账号、scope 变化、写入后超时和等待期间删除连接等测试,并监控状态分布、刷新成功率和重连等待时间,日志排除原始凭证及含秘密的错误载荷。