上一篇写查词接口时,我讲到一个取舍:默认查词要先快,不要为了例句、音频和补充解释把用户卡住。
但查词还有另一个问题:有些词,词典真的查不到。
这类词不一定生僻,反而经常出现在真实网页里。比如我读香港银行、金融科技或产品说明时,会遇到:
WeLabMoxZA Bank- 某个服务名
- 某个缩写
- 某个产品型号
传统词典当然不一定收这些。可在当前文章里,用户又确实需要知道它们是什么意思。
这时候最容易想到的方案是:词典查不到,就交给 LLM。
这个方向对,但不能这么粗暴地做。
先解释一下什么是 fallback
fallback 可以理解成“主路径失败后的兜底方案”。
比如查词时:
先查正式词库 -> 查不到 -> 再尝试另一个来源
后面这个“另一个来源”就是 fallback。
在 Parallel Translate 里,正式词库主要解决普通英文词、短语、音标、词性和中文释义。LLM 则更适合解释那些词库没收录、但上下文很重要的内容。
问题在于:如果把 LLM fallback 挂到所有未命中请求上,会马上带来三个问题。
第一是慢。LLM 调用需要网络往返和生成时间。所有查不到的词都等 LLM,查词接口就不再稳定。
第二是贵。用户可能选中 URL、邮箱、代码、乱码、无意义片段。如果这些都触发模型,成本会被放大。
第三是脏。LLM 生成的内容不应该直接写进正式词库。正式词库应该是相对稳定、可重复使用的数据;模型根据某个上下文猜出来的解释,不一定适合所有语境。
所以我最后采用的不是“查不到就问 AI”,而是“只有特定场景才允许 AI 兜底”。
为什么必须带上下文
单独看一个词,很多时候是没有意义的。
比如 Mercury,它可能是水星,可能是汞,可能是公司,也可能是车品牌。Apple 可能是苹果,也可能是苹果公司。
如果用户只查:
{ "word": "Mox" }
LLM 可以给一个看似合理的答案,但它到底是在解释什么,其实不确定。
但如果请求里带上当前句子:
{ "word": "Mox", "context": "如果你已经开通了 ZA Bank、Mox、WeLab 或其他香港银行账户,可以直接复制开户地址。"}
模型就更容易判断:这里的 Mox 大概率是香港的数字银行品牌,而不是普通英文单词。
这就是“语境化查词”的核心:不是让 AI 猜一个词的百科定义,而是让它回答“这个词在这句话里是什么意思”。
触发条件要足够窄
我给 LLM fallback 设了几个边界。
它必须同时满足这些条件:
用户主动划词 -> 请求明确允许 contextual-llm fallback -> 请求带当前句子上下文 -> 正式词库和已有缓存都没有命中 -> 输入通过基本安全检查 -> 后台租约允许这次外部调用
不满足这些条件,就不触发。
特别是这几类请求不能触发:
- 页面自动扫描产生的批量生词标注
- 旧版插件只传
{ word }的查词请求 - 没有上下文的查词
- URL、邮箱、纯标点、特别长的文本
- 批量
/api/words接口
这里的目的很明确:LLM 兜底只服务于“用户主动遇到一个词,看不懂,且当前句子能帮助解释”的场景。
它不是通用补洞工具。
返回结构不能变成一段闲聊
LLM fallback 还有一个容易犯的错误:直接把模型回答当成文本塞给前端。
这样做短期很快,长期会很难维护。因为模型可能今天输出一段解释,明天输出几个项目符号,后天输出“当然可以,下面是解释”。前端无法稳定展示,也无法判断结果是否可靠。
所以我更希望模型输出固定结构。
大概是这样:
{ "entryType": "named_entity", "entitySubtype": "organization", "normalizedText": "WeLab Bank", "definitionZh": "香港数字银行/金融科技品牌,语境中指 WeLab Bank。", "confidence": 0.9, "shouldSaveToVocabulary": false}
这里有几个字段很重要。
entryType 表示它是什么类型。可能是普通词、短语,也可能是专有名词。
confidence 表示模型自己对判断的把握。把握太低,就不要硬返回。
shouldSaveToVocabulary 表示它是否适合进入普通生词本。像 WeLab 这种专有名词,用户需要理解,但不一定要当成英语学习词汇。
这个字段对产品很关键。否则用户查了几个银行名、公司名,生词本里很快就会混进一堆不该背的东西。
LLM 结果不能污染正式词库
我把 LLM fallback 的缓存和正式词库分开。
正式词库可以理解成“长期稳定的数据源”。里面的单词、词性、翻译和词形变化,应该尽量通用。
LLM fallback 更像“这个词在这个上下文里的解释”。它有用,但它不一定适合跨文章复用。
所以缓存 key 不能只用单词本身,而要带上上下文摘要:
normalized_query + target_lang + context_hash
context_hash 就是把上下文做一个稳定摘要。这样 Apple 在水果文章里的解释,不会直接复用到科技新闻里。
这会减少缓存命中率,但换来的是语义安全。
对阅读工具来说,我宁可少复用一点,也不想让错误解释跨文章传播。
同步还是异步,要看用户是不是正在等
LLM fallback 会增加耗时,这是绕不开的。
所以我给它设想的预算很明确:只在用户主动查词时,可以同步等一小段时间。如果超时,就不要继续拖住用户。
可以理解成:
常见词 -> D1 或缓存命中,快速返回未收录词,没有上下文 -> 不调用 LLM,保持未命中或 pending未收录词,有上下文,用户主动查 -> 给 LLM 一小段预算 -> 成功就返回结构化解释 -> 超时或低置信就放弃
后续如果发现同步等待仍然影响体验,也可以改成另一种模式:
先返回 pending后台生成解释前端稍后再查一次
但第一版我还是倾向于在主动查词里给一个小预算。因为用户此刻就是为了这个词停下来的,等一个短暂的语境解释是可以接受的。
关键是不能让它影响所有查词请求。
插件端只传必要信息
还有一个隐私边界。
为了让 LLM 判断语境,确实需要传上下文。但这不代表要把整篇文章、完整 URL、页面里的所有内容都发出去。
我更倾向于只传:
- 用户查的词或短语
- 当前句子
- 可选的页面标题
- 源语言和目标语言
不传整篇文章,不传完整浏览历史,也不把页面 URL 当成必需字段。
这样模型获得足够判断语义的信息,但数据面仍然尽量小。
浏览器扩展处理网页内容时,技术上能拿到很多东西,但产品上不应该默认拿很多东西。
结语
LLM 很适合补传统词典的盲区,但它不能成为所有未命中请求的默认出口。
我现在更愿意把它当成一个受控能力:
只在用户主动查词时触发只在有上下文时触发只在正式词库未命中时触发只返回结构化短解释只写入独立缓存不自动污染生词本和正式词库
这样做看起来麻烦一些,但它能保住几个重要边界:查词接口仍然快,用户数据仍然少传,词库仍然干净,AI 只在真正需要语境判断的地方出现。
做语言学习工具时,我越来越不想把 AI 当成万能后备。
更好的方式是:让它在传统结构化系统不擅长的地方补一下,而不是让它接管整条链路。