超时不是一道防线,是三道。工具级、单轮模型调用、任务级,分开设;只挂一个总超时,等于把整个任务的预算交给最不靠谱的那次工具调用。这周 TV4 那个 agent 把整季素材删掉的帖子让我更确信一件事:超时管的从来不只是时间,是破坏半径。
只设一个总超时,等于把预算交给最不靠谱的那次调用
想象一个很常见的现场:总超时十分钟,agent 调了个 HTTP 工具,TCP 连上了,对端就是不回 body,也不报错。模型没在思考,工具没在跑,就这么挂着。十分钟后你把任务杀掉,用户看到“任务超时”,日志里只有一条 task timeout,你根本不知道是哪一层出的问题。
分层之后责任就清楚了。工具级超时让你拿到一个“这个工具失败了”的明确信号,模型可以据此重试、换路径、或者告诉用户这条路走不通;单轮模型调用超时管的是模型侧——流式响应卡住、推理服务排队;任务级是兜底,也是预算上限。三层的时间尺度差着数量级,硬塞进一个值里必然有一层是错的。
有个细节容易漏:工具超时失败要作为可观察事件回到模型上下文里,别静默重试三次然后整个任务失败。模型不知道刚才发生了什么,它只会再犯一次。
任务级超时管的不只是省钱,是破坏半径
TV4 那事儿,agent 把整季素材抹了,几百天的工作没了。这当然不是超时导致的,是权限和边界问题。但两者的底层逻辑一样:一个跑得越久、权限越大的 agent,破坏半径随时间增长。
所以任务级超时不能只当成本控制看。你得把它跟“这个 agent 能干什么”绑在一起——不可能给一个 agent 无限时间加无限权限。这周还有个 Agent37 的沙箱,低成本的 agent 沙箱。超时负责“停”,沙箱负责“停得住”,这俩是一对,缺一个都不完整。
顺带说一句,这周还有篇 AI coding agent 产出更多代码但不是更多软件 的讨论,其实是一回事。没有边界的长时间运行,产出的是量,不是收敛。任务级超时是逼收敛的一种土办法,但有效。
定值别抄默认值,先量自己这条链路的 P95
框架给的默认值基本是拍脑袋的,而且往往是给聊天场景拍的。端侧 agent 的延迟预算是另一个量级——LFM2.5 这类为端侧快跑设计的模型,工具超时按毫秒算;你把云端的值搬过去,或者反过来,都是灾难。
我的做法很朴素:给每个工具的调用延迟打点,跑一段时间,看 P95。阈值不直接取 P95,往上抬一截,抬多少取决于这条链路的抖动有多野——外部 API 和本地函数不是一回事。这是起点,不是终点。
真正定值靠误杀率。拿历史 trace 回放,统计某个阈值下有多少本来会成功的调用被砍掉。这个可以工程化,比拍脑袋强。而且分布会漂:模型换了、prompt 变长了、工具背后的服务扩容了,P95 就变了。定期重测,别一次定完就当圣旨。
设太短照样出事:长文档场景必死
反面必须讲。长文档场景——一次工具调用是把几百页 PDF 丢进解析器,或者对全仓库跑一遍索引——P95 本来就长,尾部更离谱。你按聊天场景的 P95 定阈值,这类任务会稳定失败。失败还不是干脆的失败,是跑到一半被砍,留下一堆半成品状态,重跑成本翻倍。
我的处理是按工具类别分组设超时,长任务单独一档,别用全局值。更重要的是显式告知用户:不是悄悄放宽,而是在入口处就说清楚“这一步可能要跑几分钟”。预期管理比阈值本身重要得多。长跑任务还得有进度心跳,否则用户分不清是卡住还是在干活。
反面里的反面也得说一句:有人会说那就设很长呗。长超时等于没有超时,那个卡住的工具挂二十分钟,用户早关页面了。所以取消信号——用户关页面、发新消息、点停止——得真能传到正在跑的工具上,而不是只在 UI 上变个图标。
我自己落地的几个习惯
每个工具的超时写在工具定义里,不写全局配置。离代码近,改的时候不会漏。
超时触发时打一条结构化日志,带工具名、耗时、入参大小。不打这个,你永远不知道该往哪调。
任务级超时按“用户愿意等多久”倒推,不是按“技术上多久能跑完”。
按我现在看,这三层里最容易做错的是工具级——大家往往只设了总超时,觉得够了。也许半年后回头看,框架会把这些都做成默认行为,那这篇就白写了。但至少现在,还得自己动手。