星火X2.6 端云一体的两种调用路径,短请求走端侧、长文本回云侧

文章导读
短请求走端侧、长文本回云侧,判断依据不该是“感觉这次应该不复杂”,而是输入长度、联网状态、延迟预算这三个可测字段;路由要收在一个调用入口里,业务代码只调一次,不关心最终落在哪条路径。下面给出可直接落地的判定表、调用骨架、日志验证步骤和配置文件结构;阈值全部留成待实测占位值,需要结合你的设备、模型版本和网络环境确认后再填。
📋 目录
  1. Ⅰ 把一次请求拆成可判断的三个字段:输入长度、联网状态、延迟预算
  2. Ⅱ 在调用层写一个统一入口,内部按判定结果分到端侧或云侧
  3. Ⅲ 跑三条样例请求,在日志里确认实际路径
  4. Ⅳ 端侧返回不完整时怎么回退到云侧
  5. Ⅴ 把分流规则写进配置文件而不是硬编码
A A

短请求走端侧、长文本回云侧,判断依据不该是“感觉这次应该不复杂”,而是输入长度、联网状态、延迟预算这三个可测字段;路由要收在一个调用入口里,业务代码只调一次,不关心最终落在哪条路径。下面给出可直接落地的判定表、调用骨架、日志验证步骤和配置文件结构;阈值全部留成待实测占位值,需要结合你的设备、模型版本和网络环境确认后再填。

端云分流适合放在调用层而不是业务层:先用输入长度、联网状态、延迟预算判定路径,再由一个统一入口执行端侧或云侧调用。落地时把阈值写进配置文件,用三条样例请求加一条端侧能力覆盖不到的输入跑通,在日志里确认实际路径和回退原因。边界是:端侧能力上限、超时值、回退是否重复计费都必须按官方接入文档和你自己的实测结果确认,不能照搬别处的数字。

把一次请求拆成可判断的三个字段:输入长度、联网状态、延迟预算

判定不需要模型自评,三个字段都能在调用前拿到。输入长度用分词后 token 数或字符数估算,两种取一种并保持全链路一致;联网状态用平台提供的网络检测接口或一次轻量探测结果,取布尔值即可;延迟预算由调用方传入,表示“这次请求能容忍多久”,交互式对话和后台批处理给的预算本来就不同。三个字段凑齐后,判定逻辑可以写成先看长度、再看联网、最后看预算的顺序。

// 判定函数骨架,阈值全部为待实测占位值
function decideRoute(input, budgetMs) {
  const len = estimateTokens(input);      // token 估算,或字符数,二选一固定
  const online = await checkNetwork();    // true / false
  if (!online) return { route: 'edge', reason: 'offline' };
  if (len > CFG.maxEdgeTokens) return { route: 'cloud', reason: 'too_long' };
  if (budgetMs < CFG.minBudgetForCloud) return { route: 'edge', reason: 'tight_budget' };
  return { route: 'edge', reason: 'default_short' };
}

这里的关键取舍是顺序:离线时无论多长都只能走端侧,因为云侧根本不可达;长度超过端侧上下文上限时必须回云侧,否则端侧只能截断;预算很紧的时候优先端侧,但要接受端侧回答质量可能不足,需要配合后文的手动回退。CFG.maxEdgeTokens 和 CFG.minBudgetForCloud 都不要先拍数字,先用几条真实输入跑一遍,看端侧在什么长度开始明显吃力,再回填。

在调用层写一个统一入口,内部按判定结果分到端侧或云侧

业务侧只调一个函数,参数里带上输入、延迟预算和幂等键,路由分支全部留在函数内部。端侧 SDK 与云侧 HTTP 接口的字段名各厂商不同,下面的参数名是占位写法,实际以各自的官方接入文档为准,替换时不要改动外层签名。

// 统一入口:业务只调这里
async function invoke(input, { budgetMs, requestId, traceId }) {
  const { route, reason } = await decideRoute(input, budgetMs);
  log({ traceId, event: 'route_decided', route, reason, len: estimateTokens(input) });
  if (route === 'edge') {
    return await callEdge({ prompt: input, requestId });   // 字段名以端侧 SDK 文档为准
  }
  return await callCloud({ messages: input, requestId });   // 字段名以云侧接口文档为准
}

统一入口还承担两件附带工作:一是把 requestId 透传给两条路径,回退时用它做去重依据;二是统一整理返回值,端侧和云侧的返回结构通常不一致,在入口里归一成 {text, route, fallback},业务层就不用再判断来源。返回值里的 route 直接写进日志,是后面验证分流是否生效的依据。

跑三条样例请求,在日志里确认实际路径

样例要能覆盖三个分支:一条明显很短的输入,期望 route=edge;一条明显超过端侧上限的长输入,期望 route=cloud;一条在断网或飞行模式下发起的长输入,期望 route=edge 且 reason=offline。三条都用同一个脚本触发,不要手动点界面,否则没法保证输入长度和预算字段一致。

星火X2.6 端云一体的两种调用路径,短请求走端侧、长文本回云侧
// 日志必须带的字段
traceId, event, route, reason, len, online, budgetMs, elapsedMs, fallback, fallbackReason

// 期望看到的路径标记
route=edge  reason=default_short
route=cloud reason=too_long
route=edge  reason=offline

如果路径和期望不符,按这个顺序排查:先看 len 的估算方式在两条路径上是否一致(字符数和 token 数混用会导致判定漂移);再看 online 的取值是否被缓存住了旧状态;然后确认配置文件是否被正确加载,很多“改了阈值没生效”其实是读了默认值;最后检查回退逻辑是否在端侧失败后立刻转云侧,把原本的 edge 标记覆盖成了 cloud。

端侧返回不完整时怎么回退到云侧

端侧能接住的输入一定有上限,判定不可能覆盖所有情况,所以要在调用入口里留回退通道。触发条件建议取三类:端侧调用超时、返回内容为空或明显截断、实际输入长度超过端侧上限(判定时估算偏小会漏掉这类)。三类条件任一命中就转云侧,并且在返回值里标记 fallback=true 和 fallbackReason。

const edgeResult = await withTimeout(callEdge(...), CFG.edgeTimeoutMs);
if (!edgeResult || !edgeResult.text || edgeResult.truncated) {
  log({ traceId, event: 'fallback_to_cloud', fallbackReason: 'edge_incomplete' });
  return await callCloud({ messages: input, requestId });  // 复用同一个 requestId
}

避免重复请求和重复计费的关键是同一个 requestId 只对应一次实际生成:回退只发生一次,不要写成循环重试;云侧调用沿用端侧已经生成的 requestId,服务端如需幂等可据此去重;端侧如果已经产生了计费或配额消耗,需要在日志里留下两条记录以便事后核对。如果端侧返回只是慢但内容完整,建议等待而不是回退,否则同一输入会在两条路径各跑一遍。

把分流规则写进配置文件而不是硬编码

阈值改一次就要发一次版,是分流逻辑最难维护的地方。把判定参数抽到配置文件,代码只读配置,改完重启或热加载即可复跑样例验证。下面这份结构里的值都是占位,按你自己的实测结果填写。

routing:
  maxEdgeTokens: PLACEHOLDER        # 端侧可接受的最大输入长度,待实测
  minBudgetForCloudMs: PLACEHOLDER  # 低于该预算优先端侧,待实测
  edgeTimeoutMs: PLACEHOLDER        # 端侧调用超时,触发回退
  fallbackOnEmpty: true             # 端侧返回为空时回退
  fallbackOnTruncate: true          # 端侧返回被截断时回退
  maxFallbackTimes: 1               # 回退次数上限,避免重复请求

字段之外建议再加一个 enabled 开关和一份 version 标记,改配置时能对照日志确认加载的是哪一版。改完阈值后不要只看成功跑通,要把前面那三条样例原样复跑一遍,确认 route 和 reason 的取值符合新配置;如果端侧上限调大,还要补一条刚好卡在边界附近的输入,观察它是落在端侧还是回退到云侧。