☰
ClawRouter 模型排除功能(Exclude Models)深入解析:从 `/exclude` 命令到路由回退链的完整实现指南
2026/10/9 5:22:25 网站建设 项目流程

【免费下载链接】ClawRouter

The agent-native LLM router for autonomous agents. Every frontier model behind one wallet, <1ms local routing, USDC payments on Base & Solana via x402.

项目地址:https://gitcode.com/gh_mirrors/cl/ClawRouter
点击查看免费下载

导读

本文基于 ClawRouter 仓库中的 exclude-models 实现计划,系统讲解"模型排除"这一用户级路由控制能力的完整设计与落地:如何通过 Telegram 的/exclude命令将指定模型从智能路由的候选回退链中剔除、如何持久化到磁盘并实现热加载,以及代理层在构建回退链时如何安全地应用排除过滤。读完本文,你将掌握该功能的存储格式、过滤语义、命令交互与源码级实现路径,并可以直接照搬到自己的 Agent 路由系统中。

功能概述:为什么需要"排除模型"

ClawRouter 是一个面向自主 Agent 的 LLM 路由器,会根据请求复杂度为每次调用挑选最合适的模型,并维护一条多级回退链(fallback chain)以保证请求总能被服务。但自动路由并不总能满足所有用户的偏好——某些模型可能价格不透明、质量不稳定、不兼容特定工具调用,或者用户就是不想用某个特定供应商。

排除模型功能正是为此设计的用户级控制面:用户在运行时通过/exclude命令把指定模型加入黑名单,路由器在构建回退链时将其过滤掉,并持久化到磁盘,重启后依然生效。

从 实现计划 的架构说明可以看到,其核心设计由四部分组成:

  1. 持久化模块:exclude-models.json文件存放于~/.openclaw/blockrun/目录;
  2. 过滤函数:filterByExcludeList()过滤回退链(与既有过滤器同一套安全模式);
  3. 命令入口:/exclude命令通过 add / remove / clear 子命令管理列表;
  4. 代理接入:代理在启动时加载列表,并在每次请求时重新读取(热加载)。

持久化模块:exclude-models.json 的存储与读写

文件位置与格式

排除列表以 JSON 数组形式存储在用户主目录下:

~/.openclaw/blockrun/exclude-models.json

默认路径由 exclude-models.ts 中的DEFAULT_FILE_PATH常量定义:

const DEFAULT_FILE_PATH = join(homedir(), ".openclaw", "blockrun", "exclude-models.json");

写入时模型 ID 会排序后以缩进 JSON 格式落盘,保证文件内容稳定、可 diff、可人工编辑。例如:

[ "anthropic/claude-sonnet-4.6", "openai/gpt-4o" ]

exclude-models.test.ts 中的测试验证了这一点:写入两个模型后,文件内容必须是排序后的数组["anthropic/claude-sonnet-4.6", "openai/gpt-4o"]。

核心 API:load / add / remove / clear

模块导出四个函数(见 exclude-models.ts),每个函数都接受可选的filePath参数以便测试注入临时路径:

函数签名行为
loadExcludeList(filePath?) => Set<string>读取文件并返回Set;文件不存在或解析失败时返回空Set
addExclusion(model, filePath?) => string解析别名后加入列表并持久化,返回解析后的模型 ID
removeExclusion(model, filePath?) => boolean解析别名后从列表删除;存在并删除返回true,否则false
clearExclusions(filePath?) => void清空整个列表并落盘(写入空数组)

关键实现细节:

  • 容错读取:loadExcludeList使用 try/catch,文件缺失、JSON 损坏都静默返回空Set,不会让排除功能导致整个代理崩溃(exclude-models.ts);
  • 类型过滤:解析数组时仅保留字符串元素(typeof x === "string"),防止脏数据污染模型 ID 集合;
  • 目录自动创建:saveExcludeList在写入前用mkdirSync(dirname(filePath), { recursive: true })递归创建父目录,对应测试 验证了深层嵌套路径也能直接写入;
  • 去重:底层使用Set,重复添加同一模型只会保留一条记录(见 测试)。

热加载与 mtime 缓存优化

计划文档要求"代理在启动时加载,每次请求重新读取(热加载)"。实际实现还额外做了一层优化:由于loadExcludeList运行在代理的热请求路径上,exclude-models.ts 引入了一个基于 mtime 的缓存:

/** mtime-validated cache — loadExcludeList runs on the proxy's hot request * path, so skip the read+parse when the file hasn't changed. */ const loadCache = new Map<string, { mtimeMs: number; set: Set<string> }>();

每次加载先statSync取文件的mtimeMs,与缓存比对;文件未变则直接返回缓存的拷贝(拷贝是为了防止调用方修改污染缓存),文件变了才重新 read + parse。而saveExcludeList写入后主动删除缓存条目,避免"同毫秒写入+读取"时 mtime 相同导致读到旧数据(见 exclude-models.ts 注释)。

这样既实现了"改文件即时生效"的热加载语义,又把磁盘 I/O 开销降到接近零——这正是计划文档所说的"文件很小,成本可忽略"。

别名解析:写前统一归一到真实模型 ID

addExclusion和removeExclusion在操作列表前都会调用resolveModelAlias()将用户输入的别名解析为真实模型 ID。该函数定义在 models.ts,规则包括:

  • 小写化 + 去空白后查MODEL_ALIASES表:例如"claude"→"anthropic/claude-sonnet-4.6"(在计划文档的测试中是"free"→"nvidia/gpt-oss-120b",实际仓库中的别名目标随目录更新演进);
  • 自动剥离blockrun/、openai/、openai-codex/等前缀(OpenClaw 以openai-completionsAPI 类型发送虚拟模型时会带openai/前缀);
  • 已知虚拟路由配置(eco / auto / premium 等)剥离前缀后保留裸 ID。

别名归一化带来的实际好处:用户用free、claude、nvidia这类友好名称排除,与用完整模型 ID 排除的效果完全一致,且文件里只存规范 ID,杜绝同一模型被多个别名重复记录的隐患。测试 exclude-models.test.ts 验证了别名写入、别名删除(nvidia加入、lightning删除同一模型)等场景。

过滤函数:filterByExcludeList 与安全网语义

过滤语义

过滤函数的作用是把排除列表应用到候选模型数组上。按计划文档的设计(并在集成测试中固化),核心逻辑为:

export function filterByExcludeList(models: string[], excludeList: Set<string>): string[] { if (excludeList.size === 0) return models; const filtered = models.filter((m) => !excludeList.has(m)); return filtered.length > 0 ? filtered : models; }

三个行为要点(对应 exclude-models.test.ts 单元测试场景):

  1. 空排除列表:原样返回模型列表,零开销;
  2. 部分排除:剔除命中的模型,其余模型保持原顺序(顺序对回退链的优先级至关重要);
  3. 全部被排除时回退原列表(安全网):如果某条链上的模型全被用户排除,filterByExcludeList会返回原始完整链而不是空数组——这与filterByToolCalling、filterByVision等既有过滤器一致,确保任何情况下回退链都不会为空,请求永远不会因为"无模型可用"而失败。

在仓库中该函数实际由@blockrun/router-core包提供,通过 router/index.ts 这个兼容导出层转发给@blockrun/clawrouter的 SDK 消费者;proxy.ts 直接从该包导入filterByToolCalling、filterByVision、filterByExcludeList三个过滤器。

集成测试:覆盖真实路由层级

exclude-models.integration.test.ts 将过滤函数与真实的回退链生成器getFallbackChain组合验证:

  • 从 eco SIMPLE 链中排除nvidia/gpt-oss-120b后,该模型不再出现,且过滤后仍有可用模型;
  • 跨 SIMPLE / MEDIUM / COMPLEX / REASONING 四个 eco 层级同时排除多个模型,每个层级都正确生效;
  • 排除整条链的全部模型时,filtered等于原始链(安全网验证);
  • 在 auto 层级(DEFAULT_ROUTING_CONFIG.tiers)上同样生效。

这套测试证明了排除逻辑不是"纸上过滤",而是与 ClawRouter 的 tier 分层路由(简单/中等/复杂/推理)真实结合,在任何层级上都保持安全网语义。

代理接入:把排除过滤织入回退链构建流程

ProxyOptions 新增配置项

代理层在ProxyOptions中新增了excludeModels选项(proxy.ts):

/** * Set of model IDs to exclude from routing. * Excluded models are filtered out of fallback chains. * Loaded from ~/.openclaw/blockrun/exclude-models.json */ excludeModels?: Set<string>;

请求级热加载

在每次请求构建回退链时(proxy.ts),代理读取排除列表:

const excludeList = options.excludeModels ?? loadExcludeList();

即:显式传入的excludeModels优先(主要用于测试注入),否则从磁盘实时加载——这就是计划文档要求的"每次请求重新读取(热加载)"。

回退链过滤流水线

回退链的构建在 proxy.ts 附近,排除过滤被精确插入在上下文容量过滤之后、工具调用过滤之前:

  1. 组装完整链:路由决策的selectedModel置顶,其后跟随按能力排序的候选(routingDecision.candidates或按 tier 生成的getFallbackChain),同时把会话粘性模型(sticky explicit model)前置;
  2. 上下文容量过滤(filterCandidatesByCapacity):根据估算 token(输入按 4 字符/token + maxOutput)剔除放不下上下文的模型;
  3. 排除过滤(filterByExcludeList):
// Filter out user-excluded models const excludeFiltered = filterByExcludeList(contextFiltered, excludeList); const excludeExcluded = contextFiltered.filter((m) => !excludeFiltered.includes(m)); if (excludeExcluded.length > 0) { console.log( `[ClawRouter] Exclude filter: excluded ${excludeExcluded.join(", ")} (user preference)`, ); }
  1. 工具调用过滤:filterByToolCalling(excludeFiltered, hasTools, supportsToolCalling)——注意这里输入的是排除过滤后的excludeFiltered,即过滤管线是链式串联的,后续过滤器只会看到用户允许的模型(proxy.ts);
  2. 视觉能力过滤:filterByVision(toolFiltered, hasVision, supportsVision)。

每一级过滤都会计算"被剔除的模型"并打印[ClawRouter] ...日志,运维时可以直接从日志看到"某个模型是因为用户偏好被排除的",而不是被误认为容量或能力问题。

免费模型回退也尊重排除列表

回退链之外,代理还维护一条"免费模型兜底"路径。钱包余额不足、跳过路由等场景会直接走免费模型,此时排除列表同样生效:

  • pickFreeModel(excludeList)会在FREE_MODELS列表中跳过被排除的模型(proxy.ts);
  • 若第一遍按网关可用性过滤后全部被排除,还会做第二遍"未过滤走查"兜底,防止目录数据陈旧把免费层整个关掉——只有所有免费模型都被用户排除时才返回undefined;
  • 余额不足短路场景中,const freeFallback = pickFreeModel(excludeList) ?? FREE_MODEL;(proxy.ts)保证兜底模型也不会踩中用户的排除偏好。

计划文档第 3 步要求的"免费模型是最后兜底,除非用户明确排除它",在实现中升级为对整个 FREE_MODELS 列表的逐个跳过,语义更完整。

/exclude 命令:Telegram 侧的交互与输出

命令结构与注册

计划文档最初把命令内联在src/index.ts,实际仓库将实现抽取为独立模块commands/exclude.ts,index.ts只负责导入并注册(见 index.ts 与 index.ts 的api.registerCommand(createExcludeCommand()))。命令定义为标准的 OpenClaw 插件命令:

export function createExcludeCommand(): OpenClawPluginCommandDefinition { return { name: "exclude", description: "Manage excluded models — /exclude add|remove|clear <model>", acceptsArgs: true, requireAuth: true, // handler: ... }; }

requireAuth: true意味着该命令需要鉴权,不能随意被未授权用户操控。

子命令与响应文案

命令解析ctx.args,按空格拆分出子命令与模型参数(模型名可能含空格,因此用parts.slice(1).join(" ")还原)。完整交互矩阵如下:

输入行为输出示例
/exclude(无参数)展示当前排除列表;列表为空时顺带给出用法提示Excluded models (2):\n • anthropic/claude-sonnet-4.6\n • openai/gpt-4o
/exclude add <model>解析别名后加入并持久化,回显解析后的 ID 与当前完整列表Excluded: openai/gpt-4o\n\nActive exclusions (1):\n • openai/gpt-4o
/exclude remove <model>解析别名后删除;不在列表中时给出提示Unblocked: openai/gpt-4o或Model "xxx" was not in the exclude list.
/exclude clear清空全部排除All model exclusions cleared.
未知子命令 / 缺参数返回isError: true并打印用法Usage:\n /exclude — show list\n /exclude add <model>\n /exclude remove <model>\n /exclude clear

所有列表展示都会.sort()排序并逐行•列出,保证在 Telegram 多行消息中清晰可读。

启动时日志回显

代理启动成功后(index.ts)会加载一次排除列表并记录:

const startupExclusions = loadExcludeList(); if (startupExclusions.size > 0) { api.logger.info( `Model exclusions active (${startupExclusions.size}): ${[...startupExclusions].join(", ")}`, ); }

这样每次重启运维人员都能在日志中确认当前生效的排除策略,避免"改了文件但忘了生效"的认知偏差。

测试体系:从 TDD 到集成验证

该功能遵循严格的 TDD 流程(计划文档每个 Task 都是"先写失败测试 → 最小实现 → 验证通过 → 提交"),沉淀出三层测试:

  1. 单元测试exclude-models.test.ts:覆盖空文件读取、增删清、去重、别名解析(含跨别名删除)、排序落盘、目录自动创建等 12 个场景,全部使用mkdtempSync临时目录隔离,不污染真实用户目录;
  2. 过滤函数测试:验证安全网语义(全部排除时返回原链)与顺序保持;
  3. 集成测试exclude-models.integration.test.ts:用真实的getFallbackChain+DEFAULT_ROUTING_CONFIG覆盖 eco 四层级与 auto 层级,证明排除过滤与 ClawRouter 路由层级真实协同。

此外,index.lifecycle.test.ts 也涉及排除相关生命周期验证,确保启动加载、命令注册与代理生命周期集成无回归。

手动验证与运维要点

验证排除效果

  1. 添加排除:向 Telegram 机器人发送/exclude add nvidia/gpt-oss-120b,确认回显Excluded: nvidia/gpt-oss-120b;
  2. 检查落盘:cat ~/.openclaw/blockrun/exclude-models.json,应看到排序后的 JSON 数组;
  3. 观察路由日志:发起请求后,代理日志会出现[ClawRouter] Exclude filter: excluded nvidia/gpt-oss-120b (user preference),确认过滤实际生效;
  4. 热加载验证:直接编辑 JSON 文件添加/删除模型(保持合法 JSON),下一次请求即生效,无需重启——因为请求路径每次都重新loadExcludeList(),且 mtime 缓存只在文件未变化时命中。

限制与前提

  • 排除列表是用户级配置,作用于整个代理的所有请求;它不改变模型目录,只是从回退链候选里剔除;
  • 若显式指定模型发起请求(不走路由决策),排除过滤不会拦截——它只作用于 fallback chain 构建(proxy.ts 注释明确"explicit model requests 没有回退");
  • 全部模型被排除时安全网会放行原始链,这是设计使然:宁可尊重路由决策也不让请求无模型可用。

总结

排除模型功能是 ClawRouter"智能路由 + 用户控制"哲学的一个缩影:~/.openclaw/blockrun/exclude-models.json负责持久化,resolveModelAlias负责别名归一,filterByExcludeList以"全部排除即回退原链"的安全网语义织入回退链过滤流水线(上下文 → 排除 → 工具调用 → 视觉),/exclude命令提供 add/remove/clear 三态管理,免费模型兜底同样尊重排除偏好。整套实现以 TDD 驱动、三层测试护航,既保证用户的"我不想用某个模型"诉求得到即时响应,又保证任何极端情况下请求链都不会为空——这是生产级 LLM 路由器中用户偏好控制的一个可复用的参考实现。

【免费下载链接】ClawRouter

The agent-native LLM router for autonomous agents. Every frontier model behind one wallet, <1ms local routing, USDC payments on Base & Solana via x402.

项目地址:https://gitcode.com/gh_mirrors/cl/ClawRouter
点击查看免费下载

相关推荐

上一篇:在 Kimi Code CLI 中使用 Wren AI:用 /wren 技能完成语义层安装、脚手架与首次查询
下一篇:Data Formulator 完全入门指南:微软开源的 AI 数据分析与可视化系统安装、架构与实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询