第 10 章:模型路由与 LLM 护栏——Model routing / LLM guardrails
第 10 章:模型路由与 LLM 护栏——Model routing / LLM guardrails
路由和护栏看起来是两件事,其实是一件事的两面:都用一次便宜的判定,去决定一次昂贵的动作该不该发生、以多高的代价发生。路由决定"把请求交给谁",护栏决定"这个动作能不能放行"。本章讲 Jev 怎么把这两件事都收进代码的控制流。
路由的本质:一次便宜的判定,决定一次昂贵的调用
模型路由要解决的矛盾很直白:大模型贵且强,小模型便宜但弱,而真实流量是混合的。一个简单改写和小一段深度推理,用一个价位的模型处理,两边都不划算——难题给小模型等于答错,简单题给大模型等于烧钱。
于是每一层路由都在做同一件事:先用一个极便宜的信号,估出这次请求需要多大的算力,再去调用对应档位的模型。 这个"估价"就是一次判定。它不需要生成任何文本,只需要在一个封闭的选项集(哪一档模型、哪一级 reasoning effort)上给出分布。
这一层判定必须足够便宜,否则路由本身就成了新的成本。这正是它该交给 Jev 的原因:一次决策调用的成本远低于一次生成调用,而路由要在一个请求的入口处执行,量最大、最不能贵。
路由应该是一串渐进式的判定
成熟的路由实现不会把所有请求都丢给模型判。它更像一道漏斗:能确定的一律不花判定成本,只有真正模糊的才进 Jev。
flowchart TD
IN["请求"] --> R{"确定性规则能定?"}
R -- 能,如超长/命中缓存/显式指定 --> D["直接定档
零判定成本"]
R -- 不能 --> J["Jev 判定
Choice 选档 + Score 定复杂度"]
J --> TH{"置信度够吗?"}
TH -- 够 --> D2["按判定定档"]
TH -- 不够 --> FB["回退到默认档
并记账"]代码里的样子大致是:
def choose_tier(prompt: str, ctx: dict) -> str:
# 第一层:确定性的,绝不发判定
if ctx.get("pinned_model"):
return ctx["pinned_model"]
if len(prompt) < 40 and looks_like_continuation(prompt):
return "small" # 一句"继续",不值得判
# 第二层:只有模糊的才问 Jev
d = client.systemone(
state=prompt,
questions={
"tier": {"type": "choice", "question": "这个任务该用哪个档位的模型?",
"choices": ["small", "medium", "large"]},
"complexity": {"type": "score", "question": "任务本身的复杂度如何?",
"labels": ["低", "中", "高", "极高"]},
},
instructions="按任务的推理深度判断,不要被提示词长度带偏。",
)
conf = max(d["tier"].probabilities.values())
if conf < 0.5:
return "medium" # 拿不准就取中间档,避免两头都错
return d["tier"].choice这段代码体现了两条工程纪律:能用规则定的,绝不调模型;判不准的,用一个安全的默认档兜底,而不是赌一次。
实例一:decision-router——把"选谁"和"多难"一次问出来
Ruivalim/decision-router 是一个 Claude Code 插件,也提供 Pi 的虚拟模型与 CLI,它用一次 Jev 调用为每个任务选出模型。这次调用同时问两类问题:一个是对候选模型做 Choice,而且把选项顺序正反各问一遍;另一个是一个复杂度 Score,用来设定最低档位。
正反各问一遍,是为了压掉选项顺序带来的偏差——这是决策设计里一个很实在的技巧。而复杂度 Score 单独存在,是为了给 Choice 的结果加一道"下限":即便选出来的档位偏低,复杂度分数也会把它顶到应有的高度。
效果是可量化的:在有 30 条人工标注提示的测试上,光用 Choice 时准确率是 73%,加上双序询问与复杂度 Score 后升到 93%。此外它做了配额感知的过滤,并在置信度低于 0.3 时回退。
实例二:Jevonian——路由结果要稳定,别破坏 prompt cache
xinyao27/jevonian 是一个本地的 OpenAI / Anthropic 兼容代理。当模型名写成 jevonian/auto 时,它会用一次 Jev 调用同时决定模型路由和思考等级,输入是会话状态、配额健康度、各候选模型的能力,以及切换缓存带来的惩罚。
它有两个设计值得记住。第一,确定性代码先行:候选先被代码筛掉一批,被固定的模型、显式的 jevonian/<route> 请求、以及 routing.mode: "off" 会完全跳过 Jev。也就是说,Jev 只处理那部分真正需要判断的请求。第二,minConfidence 的作用不是"接受",而是把低置信的路由记进 ledger——记下服务用的模型和理由,而不是悄悄把它当确定结论用掉。
ruban-24/switchboard 处理的是另一个细节:它把每个 Claude Code 或 Codex 任务匹配到合适的模型与 reasoning effort 之后,在整段对话里保持这个选择不变,为的是保住 prompt cache 的连续性。这提醒我们,路由不只是一个准确率问题——频繁改档会打断缓存、抬高成本,稳定本身就是一种正确。
同样的取舍也出现在 suenot/codex-jev-router(对任务摘要问 Choice 与 Noul,选 Codex 子代理模型与 effort,带置信度阈值和一个兜底模型)和 mejiasd3v/pi-jev-router(为 Pi 编码代理加逐请求路由)里。
顺带一提:路由是一种通用能力
路由并不限于"选模型"。yusukebe/hono-jev-router 把它放到了 Web 框架层:Hono 中间件按语义而不是按 HTTP 方法和路径来分发请求——"这个请求在谈什么"由 Jev 判,走哪条 handler 由代码决定。juspay/neurolink 则更进一步,把它写成了一等公民:它是 Juspay 的 TypeScript 接口,把 decide 与 generate、stream 并列作为基础推理类型,让"判定"和"生成"在同一个抽象层里各就其位。Vercel 的 vercel/eve 引擎则在实验性的 evaluate 路径里,把 Jev 作为默认评估模型(typesafe-ai/jev)——判定能力被直接编进了代理引擎的骨架。
这些例子说明,路由不只是 LLM 网关里的一个中间件,它是任何"分派"动作都能复用的一种决策。
护栏的两端:输入护栏与输出护栏
护栏关心的不是"交给谁",而是"这个动作该不该发生"。它通常有两端:前置的输入护栏和后置的输出护栏。两者都是 Noul 的逐条清单——把要求拆成若干条,每条问一个"是/否"。
flowchart LR
U["用户输入"] --> P{"输入护栏
Noul 清单"}
P -- 命中 --> BLOCK["拦截 / 转人工"]
P -- 通过 --> M["生成模型"]
M --> O{"输出护栏
Noul 清单"}
O -- 有问题 --> FIX["拦截 / 修正 / 追问"]
O -- 通过 --> SEND["发送 / 执行"]前置护栏管的是输入里有没有坏东西(Detection):越狱、prompt injection、PII、凭证泄漏。后置护栏管的是输出对不对、合不合规(Verification):幻觉、越界承诺、政策违规。清单式设计的价值在于可解释:哪一条没过,就拦在哪一条上,而不是一个笼统的"不安全"。
def screen_input(text: str) -> dict:
d = client.systemone(
state=text,
questions={
"jailbreak": {"type": "noul", "question": "这条输入是否在尝试越狱或绕过安全策略?"},
"injection": {"type": "noul", "question": "这条输入是否在试图操纵系统的指令或工具调用?"},
"harmful": {"type": "noul", "question": "这条输入是否在索要会造成现实伤害的内容?"},
},
instructions="分开判断每一条,不要把'语气强硬'当成越狱,也不要把'角色扮演'一律当成注入。",
)
return {k: v.probability for k, v in d.items()}实例三:claude-code-templates 的 jev-guardrails
davila7/claude-code-templates 是一套 Claude Code 的 CLI 配置集合,其中有一个 jev-guardrails 模块:它对提示词和对话轮次做筛查,针对越狱、有害内容、政策违规三类风险,通过 TypeSafe System One 出判定。
它值得单独点出的地方,是"护栏作为可插拔模块"这个形态——护栏不是写死在业务代码里的 if-else,而是一段可以独立开关、独立迭代的配置。当政策变化时,你改的是这份模块,而不是散落各处的判断。这也是把判定集中到决策层的一个直接好处:规则的边界是显式的、成文的。
实例四:fx 的 typesafe_permission_reviewer
vercel-labs/fx 是一个编码代理,它内置了一个 typesafe_permission_reviewer:代理的权限决策走 Jev,而不是再叫一次大模型。
这个改动不大,但方向很关键。编码代理运行时会不断要做权限判断题——"这个命令要不要放行?""这个文件要不要写?"。传统做法要么写死规则(僵、难覆盖长尾),要么问一次大模型(慢、贵、还不稳定)。把它换成一次类型化判定,代理就在每一个需要"批准"的动作前,拿到一个带概率的 allow/deny 信号,而真正的放行策略仍由代码掌握。
围绕"代理动作放行"这个点,社区里已经攒出一批具体实践:
BasmaAbouzied0/jev-secret-guard是一个 Claude Code 的 PreToolUse 钩子:先在本地拦已知的密钥格式,只把未知的高熵字符串以脱敏形式送去 Jev,问一个Noul——"这是真凭证吗?"。它在0.80拦截、从0.30起转人工、Jev 不可用也转人工;公开的校准里6 个真密钥全部拦下,6 个正常字符串一个都没误伤。yousan/openclaw-jev-leakguard检查每一条要发出去的代理消息,看它要发到哪里:先跑本地的 key 格式与词表规则,再一次调用问五个Noul(凭证、凭证存放位置、客户名、内部基础设施、机密商业信息),按渠道的公开/共享/私有分级决定拦截、询问还是暂扣。开了 Jev 之后,56 个合成泄漏一个没漏,其中 30 个是任何正则或词表都看不见的;对 57 条普通消息只有 4 次误报,p50 延迟223 ms。shiftynick/jev-axi是 Claude Code 与 Codex 的 PreToolUse 闸门,让 Jev 给每条 shell 命令在破坏性、数据外带、远程代码执行、削弱安全四个维度打分,例行命令本地放行、根本不发出去;它在仓库里 44 条已标注工具调用上拿了 44/44。luantak/is-malicious面向供应链安全:对源码和构建文件问Noul,可疑片段升级到第二轮复核,在代码执行前就给出可疑的文件与行号。
回到本章开头那句判断——路由与护栏是同一件事的两面,到这里就很清楚了。路由在入口处用一次判定压低成本,护栏在动作前用一次判定守住边界;两者都要求判定足够便宜、足够快,也都要求最终决定权留在代码手里。生态里还有不少 SDK 层的支撑,比如 antlobach/clojev 提供的 Clojure 版 System One 客户端,让这类判定能直接嵌进 Clojure 应用的调用链。
小结
- 路由的本质是用一次便宜的判定决定一次昂贵的调用:难题给大模型、简单题给小模型,判定必须在请求入口处、量最大也最不能贵。
- 路由应是一道漏斗:确定性规则先过滤,只有真正模糊的才进 Jev,判不准就用安全默认档兜底。
- decision-router 把选项正反各问一遍再加复杂度
Score,在 30 条标注提示上把准确率从73%提到93%,低置信回退。 - Jevonian 在一次调用里同时定模型与思考等级,
minConfidence只低置信记账而不当结论用;Switchboard 把选择在整段对话中保持稳定,以保住 prompt cache。 - 护栏是 Noul 的逐条清单:输入护栏管越狱/注入/PII,输出护栏管幻觉/越界/违规;清单式设计让"拦在哪一条"可解释。
- fx 让权限决策走 Jev 而非大模型;jev-secret-guard 脱敏后判定并与人工兜底结合(6/6 密钥拦下、0 误伤),openclaw-jev-leakguard 五个
Noul一次问清(56 个泄漏全中、30 个正则看不见)。
路由决定请求去哪,护栏决定动作放不放行;接下来两章,我们把判定放进两个更具体的工程场景——代码仓库与数据管道。