一次请求该在手机上算完还是转出去,判断依据通常不是模型规模,而是这一层请求在当下的可用条件:输入是否落在端侧能保证处理的范围内、端侧能力此刻是否就绪、超时预算够不够、失败之后有没有可走的兜底路径。常见的顺序是应用层先做意图与约束判定,再看端侧能力状态,不满足才交给联网路径。出错时不要从头猜,先看每层日志里同一条请求 ID 断在哪一层。
把分发判断放在应用层,端侧只接它能保证处理的输入,联网作为超时与失败后的兜底。落地顺序建议是:先画清层与层之间的调用顺序,再写带超时和降级分支的判断骨架,最后在每层打一条带同一请求 ID 的日志。验证用断网和人为超时两种条件各走一遍,确认降级真的被触发;边界在于端侧拒绝与网络失败必须能分开识别,否则排错会长期卡在同一层。
画出应用层到端侧能力的调用顺序
先确定一次请求会经过哪几层,再谈分发。下面的骨架用占位符表示接口名,接入时替换成你项目里的实际名称;具体命名与调用方式以官方文档为准。
应用层(UI / 业务路由)
└─ 意图与约束判定 APP_ROUTER.decide(req)
├─ 端侧能力层 LOCAL_CAPABILITY.is_ready() / run()
│ └─ 端侧引擎 LOCAL_ENGINE.run()
└─ 联网层 CLOUD_GATEWAY.send()
└─ 远端服务 REMOTE_SERVICE.handle()
关键点是:判定逻辑只放在应用层一处,端侧层不自己做“要不要转云”的决定。这样断点最多只有三个——判定处、端侧调用处、联网调用处,排查面被压小。
用通用伪代码写出本地优先、联网兜底的分支
下面这段不含任何具体 SDK 方法名,只描述结构与分支。LOCAL_TIMEOUT 与 CLOUD_TIMEOUT 需要结合机型和网络环境确认,不建议写死成一个很小的值。
function handle(request):
request.id = new_request_id()
log(request.id, 'app', 'start')
if not APP_ROUTER.decide(request): # 输入超范围 / 端侧不适用
return cloud_fallback(request, reason='not_applicable')
if not LOCAL_CAPABILITY.is_ready(): # 未加载 / 未授权 / 资源不足
return cloud_fallback(request, reason='local_not_ready')
result = LOCAL_ENGINE.run(request, timeout=LOCAL_TIMEOUT)
if result.status == TIMEOUT or result.status == REJECTED:
return cloud_fallback(request, reason=result.status)
if result.status == OK:
return result
function cloud_fallback(request, reason):
log(request.id, 'fallback', reason)
try:
return CLOUD_GATEWAY.send(request, timeout=CLOUD_TIMEOUT)
except NETWORK_ERROR:
return degraded_result(request) # 最后一级降级,不要静默失败
三个分支各自对应一种可观察状态:判定不过、端侧不就绪、端侧超时或被拒。写代码时把 reason 透传到日志里,后面排查就不用靠猜。
在每层打一条带请求标识的日志
日志的作用是让同一次请求在三个层里串起来。建议固定这些字段:request_id、layer、event、cost_ms、status、reason、net_state。打印位置放在应用层入口、端侧调用前后、兜底触发点、联网返回处。
{'request_id':'r-8f2a','layer':'app','event':'start'}
{'request_id':'r-8f2a','layer':'local','event':'invoke'}
{'request_id':'r-8f2a','layer':'local','event':'done','cost_ms':412,'status':'TIMEOUT'}
{'request_id':'r-8f2a','layer':'fallback','reason':'TIMEOUT'}
{'request_id':'r-8f2a','layer':'cloud','event':'done','cost_ms':980,'status':'OK','net_state':'online'}
cost_ms 用来判断是否踩到超时阈值,net_state 用来区分是链路问题还是本地逻辑问题。字段名可以按你们现有日志规范调整,但同一请求 ID 必须贯穿所有层。
用断网和超时两种条件验证兜底分支
- 断网验证:关闭 Wi-Fi 与蜂窝数据后发起一次请求。预期日志出现 fallback,cloud 层返回 NETWORK_ERROR,界面走降级提示或端侧结果;如果该输入本应由端侧处理,则不应出现 fallback 记录。
- 超时验证:测试期间把 LOCAL_TIMEOUT 临时改小,让端侧稳定超时。预期本地层 status 为 TIMEOUT,fallback 的 reason 为 TIMEOUT,联网可用时由云端返回 OK。
- 区分端侧拒绝与网络失败:端侧拒绝一般发生在 local 层且 status 为 REJECTED,说明输入或权限不满足;网络失败发生在 cloud 层,net_state 为断开或请求根本没发出。测试完成后记得把超时配置改回正常值。
按层排查失败归属
固定一条从应用层到端侧再到网络层的顺序,避免跳层猜原因。同一 request_id 的日志里,最后一条记录所在的 layer 通常就是断点所在层。
- 应用层:请求 ID 是否生成、decide 的返回是什么、是否进入 fallback、入参是否被截断或改写。判断依据是 app 层日志是否完整。
- 端侧能力层:is_ready 状态、权限授予情况、模型或资源是否加载完成、本地超时值是否被误改、run 返回的 status。若最后一条是 local + REJECTED,先查输入约束与权限,不要先去动网络配置。
- 网络层:请求是否真的发出、连接是否建立、返回的 HTTP 状态码、是否命中超时或重试上限。若最后一条是 cloud 且 net_state 断开,才回到链路侧处理。
按这个顺序走一遍,多数情况下十分钟内能定位到层,而不是在应用层反复改逻辑、实际问题却卡在端侧权限或超时阈值上。