一聚教程网:一个值得你收藏的教程网站

热门教程

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

时间:2026-08-25 19:46:50 编辑:袖梨 来源:一聚教程网

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

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

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 秒;再加两次重试,时间会继续膨胀。

awaitwithTimeout(() =>callModel(), 10_000)awaitwithTimeout(() =>callTool(), 10_000)awaitretry(() =>callModel(), { attempts: 2, timeoutMs: 10_000 })

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

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

二、把相对 timeout 转成绝对 deadline

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

typeRunBudget = {deadlineAt: numberparentSignal: AbortSignalperStepCapMs: numberreserveMs: number}constbudget: RunBudget = {deadlineAt: Date.now() + 30_000,parentSignal: request.signal,perStepCapMs: 8_000,reserveMs: 1_000,}

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

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

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

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”表达系统没在承诺时间内完成。二者都要向下传播,但重试、提示和指标不同。

const controller = newAbortController()constcancel = () => controller.abort(newBudgetAbort("cancelled"))parentSignal.addEventListener("abort", cancel, { once: true })const timer = setTimeout(() => controller.abort(newBudgetAbort("deadline-exceeded")),allocatedMs,)

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

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

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

const remaining = deadlineAt - Date.now() - reserveMsif (!failure.retryable || attempt >= maxAttempts) returnfail(failure)if (remaining <= retryDelayMs) return { terminal: "deadline-exceeded" }awaitwaitFor(retryDelayMs, parentSignal)// 下一次仍使用原来的 deadlineAt

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

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

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

终态是否可直接重试必须记录
completed结果、耗时、用量
cancelled通常否谁取消、已完成步骤
deadline-exceeded视幂等性决定截止点、剩余工作、副作用
too-many-steps实际次数、上限、最后工具
provider-rejected按状态码与策略Provider、模型、原始原因
tool-failed先查结果工具名、参数摘要、副作用状态

typeStepRecord = {name: stringkind: "model" | "tool"attempt: numberallocatedMs: numberoutcome: "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。它用本地计时器和模拟步骤验证总预算传播,不调用真实模型或外部工具。

Biome: Checked 6 files. No fixes applied.TypeScript --noEmit: passed7 pass, 0 fail, 13 expect() calls

{"operations":3,"retries":1,"terminal":"completed"}

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

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

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

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、是否计费和副作用状态,同时避免记录凭据与完整敏感正文。

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

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

官方来源:Vercel AI SDK [email protected]、Pydantic AI v2.26.0、Google GenAI Python v2.17.0

热门栏目