端侧跑短请求、云侧跑长文这个方向没有问题,但团队里真正会产生分歧的地方,往往不在“选端侧还是云侧”,而在“谁来判定这条请求算短”。同一个功能,A 同事按输入字符数判断,B 同事按页面场景判断,最后就会变成两套调用方式。建议把判定依据固定成一张可查的表:任务类型与输入长度共同决定走哪一侧,判定逻辑只放在调用层一处,配置单独存放,改规则不改代码。
分流规则要能落地,关键是让“任务类型 + 输入长度”成为唯一判定依据,并且这条规则只写在一份配置里,调用层只做查表与兜底。适用场景是多入口、多同事共同接入同一模型服务的团队;操作动作是把类型收敛成有限枚举、阈值写进配置文件、未知类型统一兜底到云侧;验证方式是改配置后用边界样例看日志中的路由结果是否随之变化;风险边界是端侧能力与字段支持存在差异,切换路径后的参数映射必须以官方文档为准,不能用一套参数硬套两条路径。
先把任务类型固定成有限的几类
类型划分不要按业务模块分,业务模块会一直新增,规则表就会跟着无限膨胀。建议按三个维度切:输入是否需要长上下文理解、输出是否依赖强推理、以及是否对首字时延敏感。按这三点通常能收敛到五类以内,例如短问答与意图改写、结构化抽取、短文本摘要、长文理解问答、长文生成。
- 短问答 / 意图改写:一两句用户输入,输出几十字,典型输入长度在几十到一两百字之间。
- 结构化抽取:一段客服对话或表单文本,输出是 JSON 字段,长度中等但对格式敏感。
- 短文本摘要:单段文章或评论,输出长度可控。
- 长文理解问答:整篇文档、多轮对话记录,必须带长上下文。
- 长文生成:多段写作、报告草稿,输出本身就很长。
类型标识建议在配置里统一写成小写下划线字符串,例如 short_qa、extract、long_qa,不要用中文、不要带版本号、不要在名字里写死路径(比如避免 short_qa_edge,因为路径会变)。把这几个类型整理成一份白名单常量,新增类型必须先过白名单,白名单之外一律视为未知类型。
写一份分流配置表:类型、长度上限、默认路径
把规则从代码里挪出来的目的很单纯:让不熟悉调用层的同事也能读懂,改阈值不用改代码。建议每条规则至少包含下面几个字段。
| 字段 | 含义 | 写法建议 |
|---|---|---|
| task_type | 任务类型标识 | 小写下划线,必须在白名单内 |
| max_chars | 走端侧的输入长度上限(字符数) | 整数;不设则写 null |
| route | 该类型的默认路径 | edge 或 cloud |
| overflow_route | 超过长度上限后的路径 | 通常为 cloud |
| note | 规则备注 | 写清为什么这么设,方便后人改 |
| config_version | 配置版本号 | 手动递增,便于日志核对 |
{
"config_version": "v3",
"default_route": "cloud",
"rules": [
{ "task_type": "short_qa", "max_chars": 300, "route": "edge", "overflow_route": "cloud", "note": "阈值待实测占位" },
{ "task_type": "extract", "max_chars": 800, "route": "edge", "overflow_route": "cloud", "note": "阈值待实测占位" },
{ "task_type": "short_summary", "max_chars": 1200, "route": "edge", "overflow_route": "cloud", "note": "阈值待实测占位" },
{ "task_type": "long_qa", "max_chars": null, "route": "cloud", "overflow_route": "cloud", "note": "必须长上下文,直接云侧" },
{ "task_type": "long_gen", "max_chars": null, "route": "cloud", "overflow_route": "cloud", "note": "输出长,直接云侧" }
]
}
上面的 300 / 800 / 1200 只是结构示例,不是结论。真实阈值需要在目标端侧设备上按输入长度和响应时间做一次实测,用实测结果替换占位值,并把替换日期和依据写在 note 里。
调用层实现查表与兜底分支
调用层只做两件事:算出输入长度、查表决定路径。规则怎么定义,调用层不关心。下面是一段与具体 SDK 无关的骨架,替换成项目语言即可。
def resolve_route(task_type, prompt, cfg):
input_len = len(prompt)
rule = find_rule(cfg["rules"], task_type)
if rule is None:
log("route_unknown_type", task_type=task_type, input_len=input_len,
route=cfg["default_route"], config_version=cfg["config_version"])
return cfg["default_route"], "fallback_unknown_type"
if rule["max_chars"] is None:
return rule["route"], "rule_no_limit"
if input_len <= rule["max_chars"]:
return rule["route"], "rule_hit"
return rule["overflow_route"], "rule_overflow"
命中与未命中要分开处理,不要合成一个三元表达式。命中规则时直接走对应路径;未命中分两种:类型不在白名单里,走 default_route 并打 warn 日志;类型在但超长,走 overflow_route,这是正常业务分支,日志用 info 级别即可,避免告警噪音。
路径切换时参数并不通用。端侧通常对上下文长度、并发方式、流式输出的支持更受限,云侧则可能多出系统提示、工具调用、采样参数等字段。推荐在调用层下面再加一个薄适配层,把统一入参映射成两侧各自的请求字段,字段名与可取值以官方文档为准,不要凭印象硬写。缺少适配层,最容易出现的情况是端侧请求被静默丢弃某个参数,而返回结果看起来正常,很难排查。
用边界样例验证配置真的被读到
配置写完不等于生效,很多“改了半天没反应”都是因为代码里还留着硬编码分支。用三类样例做一次验证,看日志里的路由结果是否随配置变化。
- 刚好等于上限:构造长度正好等于
max_chars的输入。按上面的骨架,判定用<=,期望走端侧,日志应出现rule_hit。 - 略超上限:长度取
max_chars + 1。期望走云侧,日志应出现rule_overflow。这一条最能暴露边界写法不一致的问题——有人写<,有人写<=,同一个上限就会裂成两种行为。 - 未知类型:传一个不在白名单里的
task_type,例如short_qa_v2。期望兜底到default_route,日志应出现fallback_unknown_type且为 warn 级别。
日志检查点建议统一打印四个字段:task_type、input_len、命中原因、config_version。有了 config_version,把配置里的阈值临时改大一点再跑一次样例,如果路由结果没变、版本号却变了,基本可以确定规则没有被真正读取,需要回头检查加载时机和缓存。
补一条新规则时的操作清单
新增任务时按固定顺序走,避免规则表和代码各改一半。
- 先判断能否复用已有类型。能复用就不新增,只在调用处传对
task_type。 - 确需新增时,把类型标识加进白名单常量,命名沿用现有风格,不加路径、不加版本后缀。
- 在配置表的
rules里补一行,写全task_type、max_chars(先用待实测占位值)、route、overflow_route、note。 - 同步递增
config_version,这样日志能区分新旧规则。 - 回归验证跑四个样例:新类型一条、刚好等于上限一条、略超上限一条、未知类型一条。前三条看是否命中预期路径,第四条确认兜底没被新规则破坏。
- 最后更新对外的那份类型与阈值说明(表格或文档页面),让业务侧知道新类型的存在和适用输入。端侧实测完成后,把占位阈值替换为实测值,并在 note 里注明替换依据。
这套做法本身不复杂,价值在于把“什么时候走端侧”从口头共识变成一份能被读、被改、被日志验证的规则。团队里再有新同学接入,只要照着类型白名单和配置表填一行,就不会出现两种调用方式并存的情况。