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 取消到账或线上延迟改善。

相关推荐
蓝速科技1 小时前
蓝速科技 AI 数字人一体机:RTX5060Ti 16G 旗舰渲染实测大纲
人工智能·科技
tdtsmt1 小时前
2026年AI眼镜核心主板PCBA供应商深度评测:天地通电子SMT贴片加工实测
人工智能·旅游
txg6661 小时前
VULOC:基于汇编切片的漏洞定位框架,精准锁定二进制中的危险代码
汇编·人工智能·深度学习·安全·网络安全
会编程的吕洞宾1 小时前
LangChain4j Tool Calling 实战:让大模型自动查数据库发邮件,一句话搞定复杂任务
数据库·人工智能
xcLeigh1 小时前
编程语言的 AI 友好度排名:Python、JavaScript、TypeScript 谁更适合 AI 辅助
javascript·人工智能·python·ai·typescript·ai编程
玖妍呐1 小时前
做小红书封面图,用哪个AI绘图工具生成出来好看?
人工智能
明源云2 小时前
资产管理软件哪个好?不动产资产管理系统的选型逻辑与标杆实践
大数据·人工智能·架构
谷哥的小弟2 小时前
TypeScript中的any
javascript·typescript
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-08-08
人工智能·经验分享·深度学习·神经网络·产品运营