【免费下载链接】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.
导读
本文基于 ClawRouter 仓库中的 exclude-models 实现计划,系统讲解"模型排除"这一用户级路由控制能力的完整设计与落地:如何通过 Telegram 的/exclude命令将指定模型从智能路由的候选回退链中剔除、如何持久化到磁盘并实现热加载,以及代理层在构建回退链时如何安全地应用排除过滤。读完本文,你将掌握该功能的存储格式、过滤语义、命令交互与源码级实现路径,并可以直接照搬到自己的 Agent 路由系统中。
功能概述:为什么需要"排除模型"
ClawRouter 是一个面向自主 Agent 的 LLM 路由器,会根据请求复杂度为每次调用挑选最合适的模型,并维护一条多级回退链(fallback chain)以保证请求总能被服务。但自动路由并不总能满足所有用户的偏好——某些模型可能价格不透明、质量不稳定、不兼容特定工具调用,或者用户就是不想用某个特定供应商。
排除模型功能正是为此设计的用户级控制面:用户在运行时通过/exclude命令把指定模型加入黑名单,路由器在构建回退链时将其过滤掉,并持久化到磁盘,重启后依然生效。
从 实现计划 的架构说明可以看到,其核心设计由四部分组成:
- 持久化模块:
exclude-models.json文件存放于~/.openclaw/blockrun/目录; - 过滤函数:
filterByExcludeList()过滤回退链(与既有过滤器同一套安全模式); - 命令入口:
/exclude命令通过 add / remove / clear 子命令管理列表; - 代理接入:代理在启动时加载列表,并在每次请求时重新读取(热加载)。
持久化模块: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 单元测试场景):
- 空排除列表:原样返回模型列表,零开销;
- 部分排除:剔除命中的模型,其余模型保持原顺序(顺序对回退链的优先级至关重要);
- 全部被排除时回退原列表(安全网):如果某条链上的模型全被用户排除,
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 附近,排除过滤被精确插入在上下文容量过滤之后、工具调用过滤之前:
- 组装完整链:路由决策的
selectedModel置顶,其后跟随按能力排序的候选(routingDecision.candidates或按 tier 生成的getFallbackChain),同时把会话粘性模型(sticky explicit model)前置; - 上下文容量过滤(
filterCandidatesByCapacity):根据估算 token(输入按 4 字符/token + maxOutput)剔除放不下上下文的模型; - 排除过滤(
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)`, ); }- 工具调用过滤:
filterByToolCalling(excludeFiltered, hasTools, supportsToolCalling)——注意这里输入的是排除过滤后的excludeFiltered,即过滤管线是链式串联的,后续过滤器只会看到用户允许的模型(proxy.ts); - 视觉能力过滤:
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 都是"先写失败测试 → 最小实现 → 验证通过 → 提交"),沉淀出三层测试:
- 单元测试exclude-models.test.ts:覆盖空文件读取、增删清、去重、别名解析(含跨别名删除)、排序落盘、目录自动创建等 12 个场景,全部使用
mkdtempSync临时目录隔离,不污染真实用户目录; - 过滤函数测试:验证安全网语义(全部排除时返回原链)与顺序保持;
- 集成测试exclude-models.integration.test.ts:用真实的
getFallbackChain+DEFAULT_ROUTING_CONFIG覆盖 eco 四层级与 auto 层级,证明排除过滤与 ClawRouter 路由层级真实协同。
此外,index.lifecycle.test.ts 也涉及排除相关生命周期验证,确保启动加载、命令注册与代理生命周期集成无回归。
手动验证与运维要点
验证排除效果
- 添加排除:向 Telegram 机器人发送
/exclude add nvidia/gpt-oss-120b,确认回显Excluded: nvidia/gpt-oss-120b; - 检查落盘:
cat ~/.openclaw/blockrun/exclude-models.json,应看到排序后的 JSON 数组; - 观察路由日志:发起请求后,代理日志会出现
[ClawRouter] Exclude filter: excluded nvidia/gpt-oss-120b (user preference),确认过滤实际生效; - 热加载验证:直接编辑 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.
相关推荐
OpenTofu 的 -exclude 排除资源定向(Exclude Flag)设计与实现解析
OpenTofu 的 exclude 排除资源定向(Exclude Flag)设计与实现解析 exclude 是 OpenTofu 提供的一种“反向资源定向”(
云原生DevOps基础设施彻底掌握Type Challenges中的Exclude类型:从入门到实战的联合类型排除指南
彻底掌握Type Challenges中的Exclude类型:从入门到实战的联合类型排除指南 你是否在使用TypeScript时遇到过需要从复杂联合类型中筛选特
示例工程masscan 排除列表(exclude list)FAQ 深度解析:为什么不能申请加入,以及如何正确使用 --exclude / --excludefile
masscan 排除列表(exclude list)FAQ 深度解析:为什么不能申请加入,以及如何正确使用 exclude / excludefile 导读 本
网络安全渗透测试CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考