Skip to content
Go back

查词接口不能为了完整,把用户卡在原地

上一篇写划词翻译面板时,我主要讲的是前端交互:选区怎么判断、面板怎么定位、发音和生词状态怎么同步。

但划词面板好不好用,还有一个更直接的问题:点开一个词之后,它多久能返回。

刚开始做查词接口时,我的想法比较朴素:既然用户查一个词,那就尽量一次返回完整一点。最好有中文释义、音标、词性、例句、近义词,能补的都补上。

这个方向听起来没错,但实际体验很快暴露问题:为了追求“一次完整”,接口会等太多东西。

查词不是资料页,先要快

Parallel Translate 的查词主要发生在阅读中。用户不是专门打开词典研究一个单词,而是在文章里被某个词卡住了,顺手划一下。

这种场景里,第一优先级不是“结果最丰富”,而是:

用户查词  -> 尽快看到基本意思  -> 阅读可以继续  -> 例句、音频、补充解释可以随后出现

我后来越来越觉得,查词接口不能按“资料库页面”的思路设计。

资料库页面可以等一会儿,因为用户预期是在看完整信息。阅读中的查词不一样,等待本身就是打断。等待超过一两秒,用户的注意力已经从文章转移到工具上了。

所以这次改造的核心不是换了哪个模型,也不是加了什么高级功能,而是把查词接口拆成两层:

这里的 fast 不是说结果粗糙,而是说它不等慢服务。

旧链路的问题

我的查词服务跑在 Cloudflare Worker 上。Worker 可以理解成部署在 Cloudflare 边缘节点上的轻量后端函数,适合处理这种 API 请求。

词典数据主要放在 D1 里。D1 是 Cloudflare 提供的 SQLite 风格数据库,可以用 SQL 查询。它很适合放 ECDICT 这类英中词库。

旧版查词接口大概是这样:

POST /api/word  -> 查 D1 词库  -> 查 Free Dictionary API 补音频和英英释义  -> 查 Wiktionary 补词性或定义  -> 必要时调用 LLM 生成例句或解释  -> 一次性返回

问题出在后面几步。D1 查询很快,真正拖慢的是外部服务。

Free Dictionary API 可能慢,Wiktionary 可能慢,LLM 更不用说。只要其中一个变慢,用户主动查词就会被一起拖住。

这在工程上很常见:一个接口为了“更完整”,把多个可选能力串成了同步流程。最终结果是所有请求都按最慢的环节来结算。

对阅读工具来说,这是不划算的。

fast 路径只做一件事:先给可用答案

改造后,默认的 /api/word 不再等待所有富化结果。

它做的事情变成:

收到查词请求  -> 查 D1 词库  -> 查已有富化缓存  -> 立刻返回基础释义、音标、已有例句  -> 缺的部分放到后台补

这里有一个关键点:返回结果仍然保持原来的字段,不因为内部变快就破坏旧客户端。

也就是说,旧插件还可以继续读:

只是新增了类似 enrichmentStatus 这样的可选字段,用来告诉新客户端:这个词的补充数据是已经完整,还是还在后台补。

旧客户端不知道这个字段,也没关系。

这类兼容性很重要。浏览器扩展发布出去以后,不可能假设所有用户都立刻升级。服务端接口一旦被线上版本依赖,就不能因为内部重构随便改返回结构。

后台富化不是另一个接口

那缺的例句、音频、补充释义怎么办?

Cloudflare Worker 有一个能力叫 ctx.waitUntil()。简单说,它允许主请求先返回,后台继续执行一段任务。

我把它理解成:

用户正在等的事:先返回查词结果用户不用等的事:后台补例句、音频、补充释义

这样用户第一次查一个词时,先看到基础答案。后台如果成功补齐,下一次查同一个词,就能直接命中缓存。

这比“第一次就等到最完整”更符合阅读场景。

当然,后台任务不是随便丢出去就完了。它也要有边界:

否则后台任务虽然不阻塞用户,但会拖垮自己的服务。

防止同一个词被并发打爆

还有一个隐藏问题:缓存没有的时候,很多请求可能同时进来。

比如某个词刚好被很多用户查,或者同一个页面里短时间触发了多次请求。如果每个请求都发现缓存缺失,然后都去调用外部服务,就会出现缓存击穿。

缓存击穿的意思是:平时缓存能挡住请求,但缓存一空,所有请求同时穿透到后端或外部服务。

我用 D1 做了一个很轻的租约表。可以把它理解成“后台任务锁”。

任务 key 类似:

free-dict:windowwiktionary:windowllm-examples:window

每个后台任务开始前,先尝试抢租约。只有抢到的人可以真的调用外部服务。其他请求发现已经有人在补这个词,就直接退出后台任务。

这不是为了保证绝对精确,而是为了避免同一个词在短时间内触发一堆重复外部调用。

租约还要记录几种状态:

这样既不会永久放弃某个词,也不会因为一个不存在的词反复消耗请求。

为什么还要保留 rich 模式

默认查词走 fast 路径后,我还是保留了 mode=rich

rich 模式就是旧思路:同步等待外部富化,尽量拿到完整结果。

它不适合作为用户主动查词默认路径,但在这些场景里还有价值:

这也是一个取舍:不要因为用户体验需要 fast,就把 rich 能力删掉。更好的方式是让它退到适合的位置。

默认查词:fast调试 / 预热:rich

这比在一个接口里用很多隐含条件判断要清楚。

快,不等于简单粗暴

查词接口变快以后,反而更需要认真处理边界。

因为它不再是“一次请求完整结束”的模型,而是变成了:

主请求返回基础结果后台任务补充数据缓存逐渐变丰富下次请求命中更多内容

这要求我区分几类数据:

这些东西不能混在一起。词库数据是基础事实,外部富化是增强结果,LLM 内容是生成结果,用户数据是用户资产。

一旦混在同一张表或同一个概念里,后面清理缓存、更新词库、解释隐私边界都会变得麻烦。

结语

这次查词接口改造给我的一个教训是:不要把“完整”当成所有场景的默认目标。

在阅读工具里,很多能力都应该先问一句:用户此刻是在等结果,还是可以之后慢慢补?

主动查词显然属于前者。用户划了一个词,只是想尽快跨过理解障碍,继续读下去。

所以接口设计也应该配合这个节奏:基础结果尽快返回,增强信息后台补齐,缓存慢慢变好,旧客户端继续可用。

这件事看起来像性能优化,实际更像产品取舍。

因为真正影响体验的,不是系统最终能给多少信息,而是它有没有在用户最需要的时候,先把最有用的那部分递出来。


Share this post on: