本节摘要:Router 是 laya 的调度员,工作流程两步:对输入做文字脚本检测(这段文本写的是拉丁字母、汉字还是西里尔字母),再按检测结果把请求分流到英语或多语言检查点;显式指定检查点可以跳过自动分流。本节先画出这条流程,再回答一个架构问题——为什么 laya 选择路由而不是训练一个大模型全包:答案是算力与精度的平衡,专用检查点在单一语言上更省更准,多语言检查点负责覆盖,路由让两者各得其所。最后讲路由的可观测性:每次返回里的 checkpoint 字段是观测路由行为的窗口,把它记进日志,路由对错就有数据可查。具体分流规则以官方实现为准,本节讲结构与逻辑。
官方 README 对 Router 的描述可以画成一张图(具体分流规则以官方实现为准):
Router 工作流程(结构示意) 请求(context + questions) │ ▼ ┌───────────────────┐ │ 1. 文字脚本检测 │ 看文本用什么书写系统: │ (script detect)│ 拉丁字母?汉字?西里尔?阿拉伯? └─────────┬─────────┘ 检测为英语/拉丁方向 │ 检测为其他脚本 ▼ ▼ ┌──────────────┐ ┌────────────────────┐ │ laya 421M │ │ laya-multilingual │ │ 英语主力 │ │ 322M · 100+ 语言 │ └──────┬───────┘ └─────────┬──────────┘ └──────────┬──────────────┘ ▼ 返回结果(带 checkpoint 字段, 标明本次实际由谁作答)
两个值得注意的设计。其一,检测的对象是「文字脚本」(书写系统)而不是「语种」:区分的是拉丁字母、汉字、西里尔字母这样的书写体系,不是英语与法语这样的语言细分——因为下游只有两个语言辖区(英语主力与多语言覆盖),按脚本分已经够用,更细的语种判断反而引入额外开销与错误面。其二,显式指定可以跳过检测:调用时直接点名检查点,Router 就不猜了——这是给「我已经知道流量是什么」的场景留的快车道(2.1 节的 preload 写法就是它的静态版)。
脚本检测的工程原理。 它不需要模型:Unicode 给每个字符划了区间,汉字、西里尔、阿拉伯字母各在各的区,检测就是数输入文本里各区间字符的占比——成本与一次正则相当(量级描述)。这与「语种检测」是两件事:
| 脚本检测(Router 用的) | 语种检测 | |
|---|---|---|
| 判什么 | 书写系统(汉字?拉丁?) | 具体语言(中文?英语?法语?) |
| 怎么判 | 字符区间统计 | 通常要统计模型 |
| 开销 | 极小 | 小但非零 |
| laya 需要哪个 | 够用:两个辖区按脚本已可分 | 不需要:下游没有按语种的分支 |
理解这一层就明白 2.3 节「混合语言」问题的来源:脚本检测判的是「字符长什么样」,判不了「这句拉丁字母写的是英语还是法语」——那本来也不归它管。
直觉上的替代方案是:训练一个又大又全的模型,什么语言都直接吃,何必分流。laya 没这么做,理由可以拆成四条:
| 维度 | 一个大模型全包 | 路由加专用检查点 |
|---|---|---|
| 精度 | 容量被上百种语言摊薄,单语表现受拖累 | 英语检查点容量全部给英语,主流量更准 |
| 延迟 | 为覆盖所有语言不得不更大,推理更慢 | 主流量走更小的专用检查点,更快 |
| 内存 | 一个大模型常驻,单点更重 | 两个 BERT 级检查点常驻,总量可控(421M 加 322M) |
| 演进 | 改任何语言都要动整个模型 | 检查点各自独立升级,英语版更新不影响多语言版 |
核心是第一条与第四条的组合:语言容量是零和的——多语言模型的参数要分给一百多种书写系统,专用模型的参数可以全部押在一种语言上;同时路由让「英语检查点升级」与「多语言检查点升级」解耦,迭代风险被切开。这与工程里「按流量特征分库」是同一种思路:不是造一个万能数据库,而是按访问模式拆开,各自优化。
顺带解释一个可能的疑问:脚本检测本身不慢——它看的是字符编码区间,不需要模型参与,开销与一次正则相当(量级描述)。路由这层不构成延迟瓶颈。
| 模式 | 写法(示意) | 适合 | 代价 |
|---|---|---|---|
| 自动路由 | Router() 默认创建后直接 predict | 流量语言构成未知或混杂 | 每次请求多一步检测;检测错了就送错辖区 |
| 显式指定 | Router(preload=…) 或调用时点名 | 流量构成已知且稳定 | 语料变了要人工改配置 |
工程上的常见组合是「静态预载加动态兜底」:按历史流量预载主力检查点(比如英语),同时保留 Router 逻辑接住预载辖区之外的请求。具体支持形式(是否允许逐请求指定检查点、参数名是什么)以官方文档为准,本章只锁结构:预载决定默认去向,脚本检测决定意外流量的去向。
选模式的判断还可以更口语化一点:你能回答「我的流量里百分之多少是英语」吗——答得出且数字稳定(比如九成以上),就显式预载,把那不到一成交给兜底;答不出,就先让 Router 全自动跑两周,攒出 checkpoint 分布再回来回答这个问题。路由策略本质是一份用数据写的配置,不是一次性的架构决定。
路由是自动行为,自动行为必须有观测窗口,否则出错只能靠猜。观测点就是返回结果里的 checkpoint 字段(第 1.2 节见过它):
# route_trace.py —— 把路由结果记进日志(写法示意) import json import time def decide_and_trace(router, context, questions): t0 = time.perf_counter() result = router.predict(context=context, questions=questions) event = { "ts": time.time(), "checkpoint": result.get("checkpoint", "unknown"), # 实际作答的检查点 "latency_ms": round((time.perf_counter() - t0) * 1000, 1), "context_head": context[:40], # 只记片段,避免日志膨胀与敏感信息外泄 } print(json.dumps(event, ensure_ascii=False)) return result
三个看数姿势。看分布:checkpoint 字段按天统计,英语与多语言的比例应当与业务的语言构成吻合——多语言占比异常升高,要么流量真的变了,要么脚本检测在误判。看延迟:两个检查点的延迟分开统计(421M 与 322M 大小不同),任何一边的延迟漂移都值得追。看质量:抽样对比「检测判定」与「样本真实语言」,错配率就是路由错误率的直接估计(2.3 节展开错配的代价与处理)。
日志字段的极简清单(在 route_trace.py 的基础上扩两列即可):
| 字段 | 来源 | 用途 |
|---|---|---|
| checkpoint | 返回结果 | 路由归属,分布统计的主角 |
| latency_ms | 本地计时 | 分检查点的延迟监控 |
| context_head | 输入截断 | 排查具体案例时的锚点 |
| input_length | 输入长度 | 解释延迟异常(长文本本就更慢) |
路由行为异常时,按这张清单从上往下排查(均为观测动作,不改代码):
| 症状 | 先查什么 | 常见原因 |
|---|---|---|
| 多语言占比突增 | 近期流量构成是否真的变了 | 混入新市场流量,属正常 |
| 中文样本落在英语检查点 | checkpoint 字段与原文抽查 | 脚本检测边界输入(混排、超短文本) |
| 某检查点延迟翻倍 | input_length 分布 | 长文本占比上升,不是路由问题 |
| 结果质量整体下滑 | 检查点版本与问题定义 | 检查点更新过,或选项表被改动 |
排查的原则与第 2.3 节一致:先拿数据定性(观测),再动配置(显式钉死或覆盖优先),最后才考虑预处理(拆分混合输入)——顺序反了容易把正常现象当故障修。
调度规则讲清楚了,但自动行为总有失手的时候:一段中英混排的文本会走向哪里?送错了辖区代价多大?下一节讨论默认路由的取舍与兜底。