Refly Auto Model 智能路由架构解析:虚拟模型层设计与五级路由引擎实战
【免费下载链接】reflyThe first open-source agent skills builder. Define skills by vibe workflow, run on Claude Code, Cursor, Codex & more. Build Clawdbot 🦞· APIs for Lovable · Bots for Slack & Lark/Feishu · Skills are infrastructure, not prompts.项目地址: https://gitcode.com/GitHub_Trending/re/refly
Refly 通过 Auto Model(自动模型)为用户构建了一层虚拟模型层:用户在界面上始终看到并选择 "Auto",系统则根据场景、工具集、用户上下文与可用的真实模型,在后台静默路由到最合适的 LLM。本文以specs/current/auto-model/AUTO_MODEL.md为核心骨架,结合apps/api/src/modules/provider/下的路由服务源码与packages/utils/src/auto-model.ts工具实现,完整拆解 Auto 虚拟 Provider 的数据结构、五级路由优先级链、试用期计数、显示与计费的解耦设计,以及auto_model_routing_rules与auto_model_routing_results两张数据表的使用方式,帮助读者掌握如何通过环境变量和数据库规则低成本地控制 Refly 的模型调度策略。
一、设计理念:为什么需要 Auto Routing
Auto Model Routing 的本质是虚拟模型层(virtual model layer):它把"用户选择的模型"与"实际执行请求的模型"彻底解耦。用户看到的是固定的 "Auto",系统按规则、场景、工具集和用户上下文静默选择真实模型。这样做带来的收益包括:
- 成本优化而不暴露细节:Refly 可以在后端灵活切换低成本/高性价比模型,用户无需感知底层模型变化;
- 按场景、按条件路由:无需用户重新配置,即可在不同场景(Agent、Copilot)或不同工具集(如图像生成)下选择不同模型;
- UI 稳定:后台模型升级、轮换时前端界面保持不变,"Auto" 始终位于模型列表首位;
- 新用户体验钩子:通过试用期(trial period)规则,把新用户的前 N 次 Auto 调用路由到更强模型,形成体验正反馈。
从源码结构看,整个路由体系被拆成三个层次:路由引擎(auto-model-router.service.ts)、试用期服务(auto-model-trial.service.ts)和通用工具(auto-model.ts),三者职责单一、通过依赖注入组合,便于独立测试与演进。
二、虚拟 Provider 与 Item:Auto 如何"假装"成一个普通模型
Auto 模型不是一个特殊分支,而是一个真实存在于数据库中的provider_item,由系统内置 provider 支撑,因此它完整参与现有的模型查询管道,无需任何特殊 case。
| 字段 | 值 |
|---|---|
providers.provider_id | pr-system |
providers.provider_key | system |
provider_items.item_id | pi-asnj62w0h0acxi34lkfg4ijn |
provider_items.name | Auto |
config.modelId | auto |
config.tooltip | Smart Routing |
config.contextLimit | 200000 |
config.capabilities.vision | true |
credit_billing.inputCost | 80credits/1M tokens |
credit_billing.outputCost | 400credits/1M tokens |
| UI 位置 | 列表首位(名称Auto字典序排在最前) |
需要特别注意credit_billing的设计含义:Auto 虚拟项本身带有 80/400 的计费费率,这个费率正是"路由后按 Auto 费率计费"机制的落点(详见第七章)。
全代码库通过isAutoModel()工具识别该虚拟项,实现在 packages/utils/src/auto-model.ts:它接受字符串或对象形式的ProviderItemConfig,先safeParseJSON解析字符串,再检查'modelId' in config && config.modelId === 'auto'。核心常量AUTO_MODEL_ID = 'auto'定义在同一文件的第 8 行,路由引擎、计费注入(originalModelId: AUTO_MODEL_ID)都引用它,保证单点定义。
三、默认模型配置:环境变量与优先级链
每个场景(scene)的默认模型由环境变量控制:
| 环境变量 | 场景 | 说明 |
|---|---|---|
DEFAULT_MODEL_AGENT | agent | 设为auto(或 Auto 的 itemId)即可让新建 Agent 节点默认使用 Auto 模型 |
DEFAULT_MODEL_COPILOT | copilot | 设为auto(或 Auto 的 itemId)即可让 Copilot 默认使用 Auto 模型 |
DEFAULT_MODEL_CHAT | chat | 已废弃,不要设为Auto |
findDefaultProviderItem()是默认模型的解析入口,实现在 provider.service.ts,其解析优先级为:
- 管理员覆盖:用户偏好中的
defaultModelOverride(需defaultModelOverrideEnabled === true),按场景字段(chat/copilot/agent/titleGeneration/queryAnalysis/image/video/audio)取对应itemId; - 用户偏好:用户偏好中的
defaultModel,同样按场景字段解析; - 全局环境变量默认值:通过
configService.get('defaultModel')读取,匹配规则同时支持itemId与modelId两种写法(源码中体现为item.itemId === defaultModel.agent || config.modelId === defaultModel.agent这类双匹配); - 用户 LLM 列表首个可用项:兜底保证永远有默认模型。
此外,prepareModelProviderMap()(同文件第 951 行起)会为chat、copilot、agent、titleGeneration、queryAnalysis、image、video、audio八个场景分别调用findDefaultProviderItem,产出一份完整的"场景 → provider item"映射表,供路由与执行使用。
四、模块地图:路由体系的代码布局
| 文件 | 职责 |
|---|---|
| apps/api/src/modules/provider/auto-model-router.service.ts | 核心路由引擎:规则缓存、规则匹配、工具路由、回退链、结果持久化 |
| apps/api/src/modules/provider/auto-model-trial.service.ts | 跟踪每个用户前 N 次 Auto 调用,支撑试用期条件 |
| apps/api/src/modules/provider/provider.service.ts | prepareModelProviderMap()、findDefaultProviderItem(),为路由提供 provider item |
| apps/api/src/modules/skill/skill.service.ts | 路由调用点:构建RoutingContext、调用AutoModelRoutingService.route()、向 config 注入routedData |
| apps/api/src/modules/skill/skill-invoker.service.ts | 读取routedData用于 token 用量统计与计费 |
| packages/utils/src/auto-model.ts | isAutoModel()、AUTO_MODEL_ID、AUTO_MODEL_ROUTING_PRIORITY、环境变量读取器 |
| packages/utils/src/models.ts | getModelSceneFromMode():将AgentMode映射为场景字符串 |
路由引擎的行为有对应的单元测试覆盖,见 apps/api/src/modules/provider/auto-model-router.service.spec.ts,测试通过 mock 的ProviderItemModel(如 Claude Sonnet、Gemini Flash)验证路由选择逻辑,可作为理解各优先级行为的辅助阅读材料。
五、调用链:一次 Auto 请求的完整旅程
用户在界面选择 Auto 模型后,一次请求经历以下流程(以 skill 调用为例,代码位于 skill.service.ts 的skillInvokePreCheck):
用户提交任务(选择了 Auto 模型) ↓ skill.service.ts: skillInvokePreCheck() 1. Fetch llmItems(用户可用的 LLM provider items) 2. autoModelTrialService.checkAndUpdateTrialStatus(uid) → inAutoModelTrial 3. 构建 RoutingContext { llmItems, userId, actionResultId, mode, inputPrompt, toolsets, inAutoModelTrial } 4. providerService.prepareModelProviderMap(user, param.modelItemId) → 为每个场景解析 originalProviderItem 5. param.modelItemId 保持不变(Auto itemId),用于 UI 展示 6. autoModelRoutingService.route(originalProviderItem, routingContext) → 返回 routedProviderItem(真实模型) 7. 将 routedData = { isRouted: true, originalItemId, originalModelId: 'auto' } 注入 routedProviderItem.config ↓ skill-invoker.service.ts: 用 routedProviderItem 调用 LLM - token 用量记录包含 routedData,用于计费计算 ↓ 计费:使用 Auto 模型的费率(80/400),而非真实模型的费率 ↓ action_results.provider_item_id = Auto itemId → 前端展示 "Auto" action_results.model_name = 真实模型 ID → Langfuse 追踪 auto_model_routing_results 写入一行 → 可观测性两个实现细节值得注意:
- 只路由主模型:源码中仅将
modelProviderMap[primaryScene]替换为routedProviderItem,其余辅助模型(titleGeneration、queryAnalysis、image、video、audio)保持原样; - routedData 注入条件:
providerItem.itemId !== param.modelItemId时才注入routedData,即真实模型与 Auto 项不同才标记为已路由(见 skill.service.ts)。随后actualProviderItemId与isAutoModelRouted会被用于信用计费预检查与结果记录。
六、路由引擎:五级优先级链
AutoModelRoutingService.route()(auto-model-router.service.ts)实现了五级优先级链:
1. 规则路由(Rule-based routing) ← DB 规则,按场景 + 条件匹配 2. 工具路由(Tool-based routing) ← 针对特定工具集 key 的环境变量覆盖 3. 随机选择(Random selection) ← AUTO_MODEL_ROUTING_RANDOM_LIST 环境变量 4. 内置优先级列表(Built-in priority) ← AUTO_MODEL_ROUTING_PRIORITY 硬编码列表 5. 首个可用模型(First available) ← context.llmItems[0]若输入不是 Auto 模型(isAutoModel()返回 false),则原样返回该项,不执行任何路由——这是整个引擎的短路保护。
路由前会通过buildModelMap()(同文件第 668-683 行)构建候选模型索引:同时以config.modelId和item.itemId作为 key 建 Map,并且排除capabilities.reasoning === true的推理模型(推理模型不参与自动路由)。双 key 索引意味着路由规则中的目标既可以是模型 ID,也可以是 provider item ID,对模型版本升级更鲁棒。
每次成功路由都会以genRoutingResultID()生成结果 ID,并在每个优先级命中后调用saveRoutingResult()异步写入auto_model_routing_results表(fire-and-forget,写失败仅记日志不阻塞主流程)。
6.1 优先级 1:规则路由(主路径)
规则从auto_model_routing_rules表加载,按scene过滤且enabled=true,按priority DESC排序(同优先级按ruleId ASC稳定排序,见RuleCache.fetchAndCache的orderBy)。
规则匹配(所有已定义条件必须同时满足——AND 逻辑,见RuleRouter.matchCondition):
condition.toolsetInventoryKeys:请求中任一激活工具集的 key 出现在该列表中即命中(按toolset.toolset.key匹配);condition.inAutoModelTrial:用户必须处于前 N 次 Auto 调用的试用期内。
规则目标决定目标模型(target 内部优先级):
target.model— 固定单一模型 ID(字符串)target.models— 从数组中随机选取target.weights— 按权重加权随机,来自[{ model, weight }]
对应的选择逻辑在RuleRouter.selectModelFromTarget:固定路由直接查 Map;随机路由会先过滤掉modelMap中不存在的模型再随机;加权路由则过滤weight <= 0或不可用的项后,按"随机数落在累积权重区间"的经典加权随机算法选择。所有目标模型 ID 都基于context.llmItems构建的modelMap匹配。
规则缓存(RuleCache):按场景做内存缓存,5 分钟 TTL + 3 分钟后台刷新;缓存过期时返回旧值并在后台异步刷新(stale-while-revalidate),刷新失败回退到已有缓存或空数组;首次访问才阻塞查询。NODE_ENV=development时完全绕过缓存、每次直查数据库,方便开发调试。服务启动时会预热agent、copilot两个常用场景,且定时器unref()不会阻止进程退出。
6.2 优先级 2:工具路由(已废弃)
完全由环境变量控制,且仅当mode === 'node_agent'(即场景为agent)时生效(见routeByTools的前置判断)。命中逻辑为:请求工具集 key 与TARGET_TOOLS有交集 → 使用MATCHED_MODEL_ID;否则使用UNMATCHED_MODEL_ID。
| 环境变量 | 说明 |
|---|---|
AUTO_MODEL_ROUTING_TOOL_BASED_ENABLED | true时启用 |
AUTO_MODEL_ROUTING_TOOL_BASED_TARGET_TOOLS | 逗号分隔的工具集 inventory keys,命中则触发匹配模型 |
AUTO_MODEL_ROUTING_TOOL_BASED_MATCHED_MODEL_ID | 存在任一目标工具时使用的模型 ID |
AUTO_MODEL_ROUTING_TOOL_BASED_UNMATCHED_MODEL_ID | 不存在目标工具时使用的模型 ID |
6.3 优先级 3:随机选择(已废弃)
AUTO_MODEL_ROUTING_RANDOM_LIST配置逗号分隔的模型 ID 列表,每次调用随机选一个。底层实现是selectAutoModel()(auto-model.ts):解析环境变量、按逗号切分、Math.random()取随机下标;列表为空返回null,从而让位给下一优先级。
6.4 优先级 4:内置优先级列表(已废弃)
硬编码在 packages/utils/src/auto-model.ts 的AUTO_MODEL_ROUTING_PRIORITY:
global.anthropic.claude-opus-4-5-20251101-v1:0 ← 主选 global.anthropic.claude-sonnet-4-5-20250929-v1:0 ← 回退 1 us.anthropic.claude-sonnet-4-5-20250929-v1:0 ← 回退 2按顺序在用户可用项中查找,第一个在modelMap中存在的模型胜出。
6.5 优先级 5:首个可用(最终回退)
直接取context.llmItems[0]。只要用户至少拥有一个 LLM item 就必定成功;若llmItems为空,则抛出ProviderItemNotFoundError("Auto model routing failed: no model available")。
七、Auto Model Trial:新用户体验钩子
AutoModelTrialService(auto-model-trial.service.ts)为规则路由提供inAutoModelTrial条件:新用户的前 N 次 Auto 调用可被一条带inAutoModelTrial: true条件的规则路由到更强大的模型。
其实现要点(checkAndUpdateTrialStatus):
- 计数器存 Redis,30 天 TTL:key 前缀为
auto-model-trial:+ userId;DB 仅在缓存未命中时查询,查得后写回 Redis(setex)避免后续穿透; - 阈值由
AUTO_MODEL_TRIAL_COUNT控制,默认20,读取逻辑见getAutoModelTrialCount(); - 计数器 fire-and-forget 自增:通过
redis.incr(cacheKey, ttlSeconds)原子完成INCR + EXPIRE,不阻塞主链路; - DB 查询用上限截断:
findMany只取trialCount + 1条记录(按createdAt ASC)来判定是否超出阈值,避免全表count(*); - Redis 出错时默认
inTrial = false(安全降级):即使试用计数不可用,也绝不阻塞正常请求,路由会继续走向其余优先级。
注意一个细微语义:currentCount表示"本次请求之前的累计次数",inTrial = currentCount < trialCount,即第 N 次请求时currentCount === N-1,仍在试用期内。
八、场景映射:AgentMode 到 Scene
getModelSceneFromMode(mode)(models.ts)将AgentMode映射为场景字符串:
| mode | scene |
|---|---|
copilot_agent | copilot |
node_agent | agent |
| 其他 | chat |
场景有两个用途:
- 过滤 DB 中的路由规则(
auto_model_routing_rules.scene列); - 决定
modelProviderMap中哪一项参与路由——只有主场景模型会被路由;titleGeneration、queryAnalysis、image、video、audio 等辅助模型永不路由,保证标题生成、查询分析等内部调用使用固定低成本模型。
九、显示与计费分离:关键不变量
整个体系最重要的不变量是:param.modelItemId从不被修改,始终持有 Auto itemId。这样前端展示与后台计费可以完全解耦:
| 字段 | 值 | 用途 |
|---|---|---|
action_results.provider_item_id | Auto itemId | 前端展示——始终显示 "Auto" |
action_results.model_name | 真实模型 ID | Langfuse 追踪、可观测性 |
routedData.originalItemId | Auto itemId | 计费时用于取 Auto 的信用费率 |
routedData.originalModelId | 'auto' | Langfuse 标记,区分路由流量 |
routedData.isRouted | true | 计费分支触发器 |
计费规则:当routedData.isRouted为 true 时,计费使用Auto 模型的creditBilling(输入 80 / 输出 400,每 1M tokens),而不是真实模型的费率;token 数量则来自真实模型的实际用量。该逻辑在 skill-invoker.service.ts 中体现:读取config.routedData,命中isRouted后取originalItemId对应的 Auto 项费率计费。这意味着运营商可以预先锁定 Auto 的固定费率,把底层模型切换带来的成本波动吸收在路由层。
十、数据库表:规则存储与路由观测
10.1auto_model_routing_rules
存放路由规则,规则变更通过 DB 管理,无需发版部署代码。
| 列 | 类型 | 说明 |
|---|---|---|
rule_id | String | 唯一规则 ID |
rule_name | String | 可读名称 |
scene | String | "copilot"或"agent" |
condition | JSON | 匹配条件(AND 逻辑)。空{}匹配一切 |
target | JSON | 目标模型选择配置 |
priority | Int | 越大越先评估 |
enabled | Boolean | 软禁用,无需删除 |
Condition schema:
{ "toolsetInventoryKeys": ["fal_image", "fal_video"], "inAutoModelTrial": true }Target schema(target 内部优先级:model > models > weights):
{ "model": "claude-sonnet-4-5-20250929" } { "models": ["claude-sonnet-4-5", "gemini-flash"] } { "weights": [{ "model": "claude-haiku", "weight": 70 }, { "model": "gemini-flash", "weight": 30 }] }本地开发库当前状态:无任何规则(表为空),所有路由均落到环境变量回退链。
巡检查询—— 列出已启用的规则,并输出人类可读的模型名(同时覆盖三种 target 类型,权重按大小降序拼接):
WITH weight_labels AS ( SELECT r.pk, string_agg( COALESCE(pi_item.name, pi_model.name, elem->>'model') || ' (' || (elem->>'weight') || ')', ', ' ORDER BY (elem->>'weight')::int DESC ) AS label FROM refly.auto_model_routing_rules r, jsonb_array_elements(r.target::jsonb->'weights') AS elem LEFT JOIN refly.provider_items pi_item ON pi_item.item_id = elem->>'model' LEFT JOIN refly.provider_items pi_model ON pi_model.config::jsonb->>'modelId' = elem->>'model' AND pi_model.deleted_at IS NULL WHERE r.target::jsonb ? 'weights' GROUP BY r.pk ), models_labels AS ( SELECT r.pk, string_agg( COALESCE(pi_item.name, pi_model.name, elem#>>'{}'), ', ' ) AS label FROM refly.auto_model_routing_rules r, jsonb_array_elements(r.target::jsonb->'models') AS elem LEFT JOIN refly.provider_items pi_item ON pi_item.item_id = elem#>>'{}' LEFT JOIN refly.provider_items pi_model ON pi_model.config::jsonb->>'modelId' = elem#>>'{}' AND pi_model.deleted_at IS NULL WHERE r.target::jsonb ? 'models' GROUP BY r.pk ) SELECT r.rule_id, r.rule_name, r.scene, r.priority, r.enabled, r.condition, CASE WHEN r.target::jsonb ? 'model' THEN COALESCE(pi_item.name, pi_model.name, r.target::jsonb->>'model') WHEN r.target::jsonb ? 'weights' THEN wl.label WHEN r.target::jsonb ? 'models' THEN ml.label END AS target_model FROM refly.auto_model_routing_rules r LEFT JOIN refly.provider_items pi_item ON pi_item.item_id = r.target::jsonb->>'model' LEFT JOIN refly.provider_items pi_model ON pi_model.config::jsonb->>'modelId' = r.target::jsonb->>'model' AND pi_model.deleted_at IS NULL LEFT JOIN weight_labels wl ON wl.pk = r.pk LEFT JOIN models_labels ml ON ml.pk = r.pk WHERE r.enabled = true ORDER BY r.scene, r.priority DESC;10.2auto_model_routing_results
每次 Auto 模型调用写一行记录,异步(非阻塞)写入,为运营与可观测性提供完整的路由决策审计。
| 列 | 说明 |
|---|---|
routing_result_id | 唯一结果 ID(前缀rrt-) |
user_id | 触发调用的用户 |
action_result_id | 关联action_results,可全链路追踪 |
action_result_version | action result 版本号 |
scene | "copilot"或"agent" |
routing_strategy | 使用的策略(见下) |
matched_rule_id/matched_rule_name | 命中的规则(仅 rule_based) |
original_item_id/original_model_id | Auto item ID +"auto" |
selected_item_id/selected_model_id | 实际路由到的模型 |
created_at | 时间戳 |
路由策略枚举值(与RoutingStrategy枚举一一对应):
rule_based— 命中 DB 规则tool_based— 命中环境变量工具路由fallback_random_selection— 从AUTO_MODEL_ROUTING_RANDOM_LIST随机fallback_built_in_priority— 命中AUTO_MODEL_ROUTING_PRIORITY列表fallback_first_available— 取llmItems[0]
唯一约束:(action_result_id, action_result_version)——一次执行只有一次路由决策。同时该表还承担了试用期计数的 DB 来源:试用服务缓存未命中时按(userId, createdAt)索引统计该表记录数。
十一、环境变量汇总
| 变量 | 默认值 | 说明 |
|---|---|---|
DEFAULT_MODEL_AGENT | — | agent 场景的默认 item ID 或模型 ID |
DEFAULT_MODEL_COPILOT | DEFAULT_MODEL_CHAT | copilot 场景的默认值 |
DEFAULT_MODEL_CHAT | — | chat 基础默认值(已废弃,勿设 Auto) |
AUTO_MODEL_TRIAL_COUNT | 20 | 视为"试用期"的前 N 次调用数 |
AUTO_MODEL_ROUTING_RANDOM_LIST | — | 随机回退的逗号分隔模型 ID 列表 |
AUTO_MODEL_ROUTING_TOOL_BASED_ENABLED | false | 启用工具路由 |
AUTO_MODEL_ROUTING_TOOL_BASED_TARGET_TOOLS | — | 触发匹配模型的工具集 keys |
AUTO_MODEL_ROUTING_TOOL_BASED_MATCHED_MODEL_ID | — | 存在目标工具时的模型 |
AUTO_MODEL_ROUTING_TOOL_BASED_UNMATCHED_MODEL_ID | — | 不存在目标工具时的模型 |
十二、当前实现状态与路线图
截至文档记录的 2026-02-28,各能力状态如下:
| 功能 | 状态 |
|---|---|
| 虚拟 Auto provider + DB provider_item | ✅ 已完成 |
isAutoModel()检测工具 | ✅ 已完成 |
prepareModelProviderMap()集成 | ✅ 已完成 |
param.modelItemId保留用于展示 | ✅ 已完成 |
| 规则路由引擎 + 缓存 | ✅ 已完成 |
| 工具路由(环境变量控制) | ✅ 已完成 |
| 随机选择回退 | ✅ 已完成 |
| 内置优先级列表回退 | ✅ 已完成 |
| Auto 试用服务(Redis 计数器) | ✅ 已完成 |
auto_model_routing_results持久化 | ✅ 已完成 |
routedData计费注入 | ✅ 已完成 |
| 计费使用 Auto 费率(而非真实模型费率) | ✅ 已完成 |
| 生产环境 DB 路由规则 | ✅ 通过 DB 管理 |
| 本地开发 DB 路由规则 | ⚠️ 表为空——落到环境变量回退链 |
规则目标使用itemId(升级鲁棒) | ❌ 当前为modelId——升级期间脆弱 |
| 语义路由 | ❌ 未实现(中期路线图) |
| ML 路由 | ❌ 未实现(长期路线图) |
| 级联路由 / 失败重试 | ❌ 未实现 |
值得关注的一点:buildModelMap已经同时索引了itemId与modelId,这意味着路由规则的 target 字段在技术上可以填itemId(规则目标查找会同时命中两种 key),文档所指的"规则 target 使用 itemId"改造更多是规范层面的收尾工作。
结语
Refly 的 Auto Model 是一套"以小博大"的工程方案:不引入独立的路由中间件,而是把 Auto 注册为一个普通 provider item,复用既有模型查询与计费管道;路由决策本身则收敛为一条五级回退链,从 DB 规则到环境变量再到硬编码列表逐级兜底,保证任何配置缺失时都有可用模型。对于希望在自己的 Refly 部署中落地智能模型调度、控制模型成本的开发者而言,核心操作路径非常清晰:通过DEFAULT_MODEL_AGENT/DEFAULT_MODEL_COPILOT把默认模型指向 Auto,通过AUTO_MODEL_TRIAL_COUNT调节新用户体验阈值,再通过auto_model_routing_rules表按场景和工具集精细控制路由目标——全程无需改代码、无需重新部署,这正是该设计最大的工程价值。
【免费下载链接】reflyThe first open-source agent skills builder. Define skills by vibe workflow, run on Claude Code, Cursor, Codex & more. Build Clawdbot 🦞· APIs for Lovable · Bots for Slack & Lark/Feishu · Skills are infrastructure, not prompts.项目地址: https://gitcode.com/GitHub_Trending/re/refly
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考