Agent 的 timeout 为什么会失效?把一条总预算传到底

给 Agent 配一个 timeout: 30_000,不代表它真的会在 30 秒内结束。Agent 外层、模型请求、工具、重试退避和持久化只要有一层重新计时,用户看到的 30 秒就可能变成 60 秒甚至更久;更危险的是,前端已经显示超时,后台工具仍在写数据。

2026-08-08 的官方版本核验给出了三个值得组合的信号:Vercel AI SDK 7.0.58 修复 ToolLoopAgent 没有遵守 Agent 超时设置;Pydantic AI 2.26.0 把取消提升为一级 Runtime 事件;Google GenAI Python 2.17.0 增加 TOO_MANY_TOOL_CALLS 终态。本文不比较框架 API,而是用一个通过 7 项测试的 TypeScript 最小实验,拆开一条通用超时契约。

一、最常见的错误,是每一层都从头计时

下面的代码看起来给模型和工具都加了 10 秒超时,实际却允许整轮至少跑 20 秒;再加两次重试,时间会继续膨胀。

ts 复制代码
await withTimeout(() => callModel(), 10_000)
await withTimeout(() => callTool(), 10_000)
await retry(() => callModel(), { attempts: 2, timeoutMs: 10_000 })
失效方式 表面配置 实际风险
每层重新计时 Agent、模型、工具各 30 秒 总时长叠加
只中止 HTTP 请求结束 子进程、队列或数据库仍继续
重试重置预算 每次都拿到完整 timeout 故障时运行时间最长
只返回 error 所有失败一个状态 无法判断能否重试或是否有副作用
前端取消不下传 UI 显示"已停止" 后台仍花费 Token 或执行工具

真正的入口约束应该是一条绝对截止时间 deadlineAt。后续每一层只能消费剩余预算,不能创建新的完整预算。

二、把相对 timeout 转成绝对 deadline

入口接到用户请求时只计算一次截止时间,同时保留父级取消信号。模型、工具和退避拿到的是同一个上下文。

ts 复制代码
type RunBudget = {
  deadlineAt: number
  parentSignal: AbortSignal
  perStepCapMs: number
  reserveMs: number
}

const budget: RunBudget = {
  deadlineAt: Date.now() + 30_000,
  parentSignal: request.signal,
  perStepCapMs: 8_000,
  reserveMs: 1_000,
}

reserveMs 不是给业务继续运行,而是给终态持久化、Trace 收尾和用户响应留空间。没有这段余量,系统可能在最后一毫秒完成工具,却来不及写入"工具是否执行成功"。分布式系统还要处理跨机器时钟;本文实验只在单进程使用 Date.now()

三、每一步只能拿到剩余预算与单步上限的较小值

实验在启动步骤前计算:min(perStepCap, deadlineAt - now - reserve)。结果小于等于零时,步骤根本不应启动。

ts 复制代码
const allocatedMs = Math.min(
  perStepCapMs,
  Math.max(0, deadlineAt - Date.now() - reserveMs),
)

if (allocatedMs <= 0) {
  return { terminal: "deadline-exceeded" }
}

单步上限用于防止一个挂起工具吃完全部时间;总截止时间负责限制整个 run。生产实现还要把信号真正接到 Provider SDK、fetch、数据库驱动、子进程和队列消费者。只在外层 Promise.race 一个计时器,通常不会停止底层工作。

四、用户取消和预算耗尽必须是两种终态

"用户点击停止"表达主动意图;"deadline exceeded"表达系统没在承诺时间内完成。二者都要向下传播,但重试、提示和指标不同。

ts 复制代码
const controller = new AbortController()
const cancel = () => controller.abort(new BudgetAbort("cancelled"))
parentSignal.addEventListener("abort", cancel, { once: true })

const timer = setTimeout(
  () => controller.abort(new BudgetAbort("deadline-exceeded")),
  allocatedMs,
)

父信号中止时,模型流、工具、并行分支和后续状态提交都应看到取消。Pydantic AI 2.26.0 的一级取消事件说明这不是 UI 细节,而是 Runtime 契约;本文没有安装该框架,只吸收这一通用判断。

五、重试花的是同一笔钱,不能刷新预算

重试前至少检查:错误是否允许重试、是否还有 attempt、剩余预算是否覆盖退避。若连退避都放不下,应立即结束。

ts 复制代码
const remaining = deadlineAt - Date.now() - reserveMs

if (!failure.retryable || attempt >= maxAttempts) return fail(failure)
if (remaining <= retryDelayMs) return { terminal: "deadline-exceeded" }

await waitFor(retryDelayMs, parentSignal)
// 下一次仍使用原来的 deadlineAt

对写操作还要增加幂等键和结果回查。连接在提交后断开时,"超时"不等于"没有执行";不先查询真实结果就重试发布、支付或写库,可能制造重复副作用。

六、把失败终态建模清楚,恢复逻辑才有依据

实验没有把所有异常压成一个字符串,而是保留六种终态和每一步的审计记录。

终态 是否可直接重试 必须记录
completed 结果、耗时、用量
cancelled 通常否 谁取消、已完成步骤
deadline-exceeded 视幂等性决定 截止点、剩余工作、副作用
too-many-steps 实际次数、上限、最后工具
provider-rejected 按状态码与策略 Provider、模型、原始原因
tool-failed 先查结果 工具名、参数摘要、副作用状态
ts 复制代码
type StepRecord = {
  name: string
  kind: "model" | "tool"
  attempt: number
  allocatedMs: number
  outcome: "completed" | "retrying" | "failed" | "timed-out"
  sideEffectStarted: boolean
}

Google GenAI Python 2.17.0 把"工具调用次数耗尽"暴露为 TOO_MANY_TOOL_CALLS,说明步骤上限不是普通完成。Adapter 应保留 Provider 原始 finish reason,再映射到业务终态。

七、最小实验验证了什么

实验运行在 Bun 1.3.1、TypeScript 7.0.2、Biome 2.2.0。它用本地计时器和模拟步骤验证总预算传播,不调用真实模型或外部工具。

text 复制代码
Biome: Checked 6 files. No fixes applied.
TypeScript --noEmit: passed
7 pass, 0 fail, 13 expect() calls
json 复制代码
{"operations":3,"retries":1,"terminal":"completed"}

7 项测试覆盖共享 deadline、挂起工具超时、用户取消、瞬时错误重试、预算不足时拒绝退避、Step 上限,以及工具失败后的副作用标记。它不证明任何 SDK 当前版本已经正确实现。

八、接入现有 Agent Runtime 时怎么验收

用慢模型、永不返回的工具、429、数据库提交后断线做故障注入;再覆盖取消与完成同时发生、超时与工具回调同时发生、一个并行分支失败时同级分支是否停止。恢复 Worker 后还要检查旧 deadline、attempt、审批和副作用状态是否仍有效。

ts 复制代码
expect(trace.terminal).toBe("deadline-exceeded")
expect(trace.lastStep).toBe("publish")
expect(trace.sideEffectStarted).toBe(true)
expect(trace.retryStarted).toBe(false)

观测至少记录 remaining_ms_before_stepallocated_step_ms、取消来源、attempt、step count、原始 finish reason、是否计费和副作用状态,同时避免记录凭据与完整敏感正文。

九、上线前检查表与验证边界

  • 入口只生成一次绝对 deadline,后续层不重置;
  • 每步预算取剩余时间与单步上限的较小值;
  • 为持久化、Trace 和响应保留收尾时间;
  • 父级取消传播到模型、工具、并行任务和退避;
  • 取消、超时、Step 耗尽和工具失败是不同终态;
  • 重试不刷新 deadline,预算不足时不启动退避;
  • 写工具有幂等键、结果回查和副作用审计;
  • 用挂起、慢响应、取消竞态和提交后断线做故障注入。

官方来源:Vercel AI SDK ai@7.0.58Pydantic AI v2.26.0Google GenAI Python v2.17.0

验证边界:本文基于 2026-08-08 已核验的官方 Release,并完成框架无关的 TypeScript 实验;没有安装上述 SDK,没有调用真实模型、数据库、队列或发布工具,也没有验证分布式时钟、Provider 取消到账或线上延迟改善。

相关推荐
月光船幽幽37 分钟前
检测用户意图复杂性的关键方法
人工智能·python
Henry-SAP1 小时前
AI突破重塑未来科技格局
人工智能·云原生·sap·erp
空堂与归2 小时前
图像识别总是准确率低?用卷积神经网络CNN实现高效分类
人工智能·深度学习·分类·cnn
老猿AI洞察3 小时前
阿里云启用巴西数据中心,AI服务出海落子南半球
人工智能·阿里云·云计算
大金SEO3 小时前
GEO启动的正确顺序:先做信源清理,再谈内容扩张
大数据·人工智能
Bruce_Liuxiaowei3 小时前
从基础理论开始学习人工智能(三):盲目搜索——从状态空间图到生成-测试范式
人工智能·学习
浮生望3 小时前
TypeScript 高级工具类型实战:Pick、Omit、Partial 到 Record 的完整解析
typescript
qyz_hr3 小时前
拥抱AI,红海云筑牢企业组织与人力数据底层能力
人工智能
Capricorn19884 小时前
个人知识库接入大模型频现“幻觉引用”?排查 RAG 溯源失效问题,解析知芽构建可信第三大脑的底层架构
大数据·论文阅读·人工智能·笔记·架构·论文笔记