Nakodo 的大部分功能都在后台跑,只有一个界面例外:品牌方新建推广活动时,输入一个域名,然后盯着屏幕等系统读完网站、写出一份简报。这是整个产品里唯一让用户对着加载圈等上几十秒的地方,也是唯一一处超时时间靠算出来、而不是靠猜出来的地方。
这套时间预算从外往里拆,一共分四层。
最外层:120秒的天花板
页面路由上直接声明了上限:maxDuration = 120。这个数字写在页面上而不是接口路由上,因为活儿是在这个页面调起的 Server Action 里干的。120秒不是对工作量的估算,而是团队愿意让用户干等的极限值的大约三倍——留出这么多余量,是为了让底下的每一层都能体面地失败,而不是被平台在半句话中间掐断。
被平台掐断是最难看的失败方式:自己的代码一行都没跑,没有报错,没有状态变更,也没有退款。这种情况得靠另一套机制兜底。
同一个数字还以注释形式出现在编排模块里,因为那是调用方最容易忘的地方。
爬取35秒,单页10秒
爬取环节拿到 35秒,每一次抓取单独限时 10秒。35加75等于110,落在120以内,剩下10秒留给数据库写入、套餐校验、审计记录这些杂事。
单次抓取10秒的超时写在 HTTP 模块里,但它故意不是唯一的限制——一个网站完全可以给你十个页面,每个都卡在9秒才回。
爬取内部还有两笔更小的预算,都是给"有更好、没有也能过"的东西准备的:站点地图 8秒,Logo 8秒。两者超时都直接落到 null。没有站点地图的品牌,或者站点地图由慢服务托管的品牌,照样能拿到简报。Logo 的请求在等待站点地图之前就已经发出,两者重叠执行;这场竞速输了,活动名字旁边就是没有 Logo 而已。
那个超时辅助函数只有九行,注释里写清了它不做什么:超时后 promise 本身并不会被取消。它在这里能用,只是因为底下每一步都有自己的 AbortSignal.timeout,被放弃的活儿会在自己的时限内死掉,而不是一直跑到函数冻结。把一个没有内部超时的东西包进 withTimeout,等于给泄漏起了个让人安心的名字。
模型调用75秒,而且只试一次
应用里每一次 AI 调用都走同一个函数,各任务的限额是一张带注释理由的表:
- 站点研究:75秒,重试0次
- 关键词:30秒,重试1次
- 提案:25秒,重试1次
- 评论、理由、外联、回复、建议:各30秒,重试0次
- 客服:20秒,重试0次
表头的注释解释了这列看起来随意的数字背后的规则:每次调用单独限时,这样一次慢响应不会拖垮发起它的请求或定时任务。maxRetries 指的是 SDK 自身的重试,发生在超时、限流或服务器错误之后;一次超时的调用仍可能被计费。站点研究在用户眼皮底下跑,只给一次机会。评论、匹配说明、首封邮件和回复跑在任务队列里,队列本身已经会隔几分钟重试三次,所以它们每次尝试只给一次机会:最多三次调用而不是六次,限流也有时间缓过来。
两条可以搬到任何 LLM 代码库的经验
第一,重试只能属于一层。OpenAI 的 SDK 默认重试,任务队列也默认重试。放任不管,一次短暂的限流会变成六次快速连发,既费钱,又是对限流最糟糕的应对方式。所以跑在任务队列里的任务设成 maxRetries: 0,把队列那三次间隔几分钟的尝试当作重试策略;设成 maxRetries: 1 的,是外面完全没有重试的那些。
第二,超时是预算,不是愿望。客服回答给20秒,不是因为客服问题简单,而是因为有人在盯着一个小部件,超过20秒,诚实的做法是给他转人工。这个数字来自等待方的截止时间,不来自任务的难度。
喂给模型的输入同样有上限
超时只是延迟预算的一半,另一半是输入体积。页面读得短,已经存下来的页面在取出时还会再被限一次。单页文本上限 10000字符,关键页面最多 5个,首页 8000字符,普通页 5000字符,站点地图路径采样 150条,可读文本少于 300字符 就停手。
PAGE_TEXT_MAX 之所以和 PAGE_CHARS 分开存在,是因为踩过一个迟早会出的坑:爬取时页面被裁剪,但这些页面会被存下来,之后应用基于同一份研究写别的东西时还会再发一次。改了爬取的限额,旧记录永远保持旧尺寸。第二道上限在使用的那一刻才生效,所以几个月前写下的记录也撑不爆今天的预算。
MIN_SITE_TEXT 是反方向的一道限制。可读文本不足300字符,系统就停下,而不是花75秒把一片空白变成一份自信满满的简报。用户会收到一句具体的话:这个域名几乎没有我们能读到的文本,它可能需要 JavaScript 才能显示内容。
这只是八种爬取问题之一,每一种都有自己的说法:无效、未找到、被拦截、私有、超时、证书问题、内容为空、其他错误。首页抓取会依次尝试 https、带 www 的 https、以及纯 http,然后报告最有用的那个失败原因,而不是最后一个。
"我们找不到 acme.com,请检查拼写"和"acme.com 有安全证书问题",会把用户引向完全不同的动作。把两者都压成一句"研究失败",就是产品自己给自己挣来一张本不必要的客服工单。
超时必须免费
套餐里包含每月若干次网站研究,所以超时也是一次计费事件。每个计量动作都是账本里的一行,这让退款变成一次删除而不是一次扣减,普通失败路径就会花掉一次。
对已有活动重新跑研究则温和一些:失败会恢复活动之前的状态,包括暂停状态。品牌方对一个正在投放的活动重跑研究、结果撞上超时,不会丢掉已有的活动。
而当网站直接把我们拦在外面时,仍然有一条路可走——用户自己填的备注会作为额外一页加进去,在网站拦截我们的情况下,这些备注本身就够了。
没有报错的那种失败:三分钟
如果平台把函数杀掉,上面这一切都不会运行。请求消失,记录永远停在"研究中",用户盯着一个永远不会结束的加载圈。这种情况靠一个时钟而不是 catch 来处理:STALE_RESEARCH_MS = 3分钟。
三分钟就是120秒上限加一分钟余量,这是选这个数字唯一诚实的办法:它必须长于任何可能还活着的运行,否则恢复逻辑会去跟一个马上要成功的请求打架。恢复是一条 UPDATE,判断逻辑写成 SQL,一次语句就能把该用户所有卡住的活动处理完。然后是退款,一次带一小时窗口的删除,免得误伤更早的、不相关的运行。
这背后没有定时任务。它发生在用户下一次查看活动列表的时候,因为那正是卡住的活动开始变得重要、也正对着在意它的人的时刻。
整体形状
一个面向用户的截止时间,拆成若干命名常量,加起来小于它;每个"有更好"的东西允许单独失败;每个限额都配一句用户能照着做的话。这些数字迟早会过时。真正让它们改起来便宜的,是这笔账被写了下来。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.